diff --git a/AaxDecrypter/AaxDecrypter.csproj b/AaxDecrypter/AaxDecrypter.csproj index a3eeee41..a7bf3b44 100644 --- a/AaxDecrypter/AaxDecrypter.csproj +++ b/AaxDecrypter/AaxDecrypter.csproj @@ -4,6 +4,10 @@ net5.0 + + + + lib\taglib-sharp.dll @@ -42,9 +46,6 @@ Always - - Always - diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index f853cbee..e8ee0a0f 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -41,12 +41,12 @@ namespace AaxDecrypter public string outDir { get; private set; } public string outputFileName { get; private set; } public ChapterInfo chapters { get; private set; } - public string Title => aaxcTagLib.Tag.Title?.Replace(" (Unabridged)", "") ?? "[unknown]"; - public string Author => aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"; - public string Narrator => aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator ?? "[unknown]"; + public string Title => aaxcTagLib.TitleSansUnabridged ?? "[unknown]"; + public string Author => aaxcTagLib.FirstAuthor ?? "[unknown]"; + public string Narrator => aaxcTagLib.Narrator ?? "[unknown]"; public byte[] CoverArt => aaxcTagLib.Tag.Pictures.Length > 0 ? aaxcTagLib.Tag.Pictures[0].Data.Data : default; - private TagLib.Mpeg4.File aaxcTagLib { get; set; } + private AaxcTagLibFile aaxcTagLib { get; set; } private StepSequence steps { get; } private DownloadLicense downloadLicense { get; set; } private FFMpegAaxcProcesser aaxcProcesser; @@ -90,7 +90,7 @@ namespace AaxDecrypter var networkFile = await NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl)); - aaxcTagLib = await Task.Run(() => TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average) as TagLib.Mpeg4.File); + aaxcTagLib = await Task.Run(() => new AaxcTagLibFile(networkFile)); var defaultFilename = Path.Combine( outDir, @@ -239,21 +239,8 @@ namespace AaxDecrypter /// public bool Step3_RestoreMetadata() { - var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average); - - var destTags = outFile.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag; - destTags.Clear(); - - var sourceTag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag; - - //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. - foreach (var stag in sourceTag) - { - destTags.SetData(stag.BoxType, stag.Children.Cast().ToArray()); - } + var outFile = new AaxcTagLibFile(outputFileName); + outFile.CopyTagsFrom(aaxcTagLib); outFile.Save(); return true; diff --git a/AaxDecrypter/DecryptLib/taglib-sharp.dll b/AaxDecrypter/DecryptLib/taglib-sharp.dll deleted file mode 100644 index 87ec4bf1..00000000 Binary files a/AaxDecrypter/DecryptLib/taglib-sharp.dll and /dev/null differ diff --git a/AaxDecrypter/NFO.cs b/AaxDecrypter/NFO.cs index ab652f45..eca864ff 100644 --- a/AaxDecrypter/NFO.cs +++ b/AaxDecrypter/NFO.cs @@ -3,32 +3,31 @@ namespace AaxDecrypter { public static class NFO { - public static string CreateContents(string ripper, TagLib.File aaxcTagLib, ChapterInfo chapters) + public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters) { - var tag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple); - - string narator = string.IsNullOrWhiteSpace(aaxcTagLib.Tag.FirstComposer) ? tag.Narrator : aaxcTagLib.Tag.FirstComposer; + var tag = aaxcTagLib.AppleTags; var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours; var myDuration - = (_hours > 0 ? _hours + " hours, " : "") + = (_hours > 0 ? _hours + " hours, " : string.Empty) + aaxcTagLib.Properties.Duration.Minutes + " minutes, " + aaxcTagLib.Properties.Duration.Seconds + " seconds"; var header = "General Information\r\n" + "===================\r\n" - + $" Title: {aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")}\r\n" - + $" Author: {aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"}\r\n" - + $" Read By: {aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator??"[unknown]"}\r\n" - + $" Copyright: {aaxcTagLib.Tag.Year}\r\n" - + $" Audiobook Copyright: {aaxcTagLib.Tag.Year}\r\n"; - if (!string.IsNullOrEmpty(aaxcTagLib.Tag.FirstGenre)) - header += $" Genre: {aaxcTagLib.Tag.FirstGenre}\r\n"; + + $" Title: {aaxcTagLib.TitleSansUnabridged ?? "[unknown]"}\r\n" + + $" Author: {aaxcTagLib.FirstAuthor ?? "[unknown]"}\r\n" + + $" Read By: {aaxcTagLib.Narrator ?? "[unknown]"}\r\n" + + $" Release Date: {aaxcTagLib.ReleaseDate ?? "[unknown]"}\r\n" + + $" Book Copyright: {aaxcTagLib.BookCopyright ?? "[unknown]"}\r\n" + + $" Recording Copyright: {aaxcTagLib.RecordingCopyright ?? "[unknown]"}\r\n"; + if (!string.IsNullOrEmpty(tag.FirstGenre)) + header += $" Genre: {aaxcTagLib.AppleTags.FirstGenre}\r\n"; var s = header - + $" Publisher: {tag.Publisher ?? ""}\r\n" + + $" Publisher: {aaxcTagLib.Publisher ?? string.Empty}\r\n" + $" Duration: {myDuration}\r\n" + $" Chapters: {chapters.Count}\r\n" + "\r\n" @@ -38,20 +37,20 @@ namespace AaxDecrypter + " Source Format: Audible AAX\r\n" + $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" - + $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + + $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n" + "\r\n" + " Lossless Encode: Yes\r\n" + " Encoded Codec: AAC / M4B\r\n" + $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" - + $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + + $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n" + "\r\n" + $" Ripper: {ripper}\r\n" + "\r\n" + "\r\n" + "Book Description\r\n" + "================\r\n" - + (!string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description); + + (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.LongDescription : aaxcTagLib.Tag.Comment); return s; } diff --git a/AaxDecrypter/TagLibMpeg4Ex.cs b/AaxDecrypter/TagLibMpeg4Ex.cs new file mode 100644 index 00000000..f3ffd3c7 --- /dev/null +++ b/AaxDecrypter/TagLibMpeg4Ex.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using TagLib; +using TagLib.Mpeg4; + +namespace AaxDecrypter +{ + public class AaxcTagLibFile : TagLib.Mpeg4.File + { + public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag; + + private static ReadOnlyByteVector naratorType = new ReadOnlyByteVector(0xa9, (byte)'n', (byte)'r', (byte)'t'); + private static ReadOnlyByteVector descriptionType = new ReadOnlyByteVector(0xa9, (byte)'d', (byte)'e', (byte)'s'); + private static ReadOnlyByteVector publisherType = new ReadOnlyByteVector(0xa9, (byte)'p', (byte)'u', (byte)'b'); + public string Narrator { get; } + public string LongDescription { get; } + public string ReleaseDate { get; } + public string Publisher { get; } + //TagLib uses @ART, which is the Artist tag + public string[] Authors => AppleTags.Performers; + public string FirstAuthor { get; } + public string TitleSansUnabridged { get; } + public string BookCopyright { get; } + public string RecordingCopyright { get; } + + private string[] _copyright; + public AaxcTagLibFile(IFileAbstraction abstraction) + : base(abstraction, ReadStyle.Average) + { + _copyright = AppleTags.Copyright?.Replace("©", string.Empty).Replace("(P)", string.Empty)?.Split(';'); + + BookCopyright = _copyright is not null && _copyright.Length > 0 ? _copyright[0] : default; + + RecordingCopyright = _copyright is not null && _copyright.Length > 1 ? _copyright[1] : default; + + TitleSansUnabridged = AppleTags.Title?.Replace(" (Unabridged)", ""); + + FirstAuthor = Authors?.Length > 0 ? unicodeToAscii(Authors[0]) : default; + + string[] text = AppleTags.GetText(publisherType); + Publisher = text.Length == 0 ? null : text[0]; + + text = AppleTags.GetText("rldt"); + ReleaseDate = text.Length == 0 ? null : text[0]; + + text = AppleTags.GetText(descriptionType); + LongDescription = text.Length == 0 ? null : unicodeToAscii(text[0]); + + text = AppleTags.GetText(naratorType); + Narrator = text.Length == 0 ? null : unicodeToAscii(text[0]); + + } + + public AaxcTagLibFile(string path) : this(new LocalFileAbstraction(path)) + { + } + public void CopyTagsFrom(AaxcTagLibFile sourceFile) + { + AppleTags.Clear(); + + //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. + foreach (var stag in sourceFile.AppleTags) + { + AppleTags.SetData(stag.BoxType, stag.Children.Cast().ToArray()); + } + } + + + /// + /// Attempts to convert unicode characters to an approximately equal ASCII character. + /// + private string unicodeToAscii(string unicodeStr) + { + //Accents + unicodeStr = Regex.Replace(unicodeStr, "[éèëêð]", "e"); + unicodeStr = Regex.Replace(unicodeStr, "[ÉÈËÊ]", "E"); + unicodeStr = Regex.Replace(unicodeStr, "[àâä]", "a"); + unicodeStr = Regex.Replace(unicodeStr, "[ÀÁÂÃÄÅ]", "A"); + unicodeStr = Regex.Replace(unicodeStr, "[àáâãäå]", "a"); + unicodeStr = Regex.Replace(unicodeStr, "[ÙÚÛÜ]", "U"); + unicodeStr = Regex.Replace(unicodeStr, "[ùúûüµ]", "u"); + unicodeStr = Regex.Replace(unicodeStr, "[òóôõöø]", "o"); + unicodeStr = Regex.Replace(unicodeStr, "[ÒÓÔÕÖØ]", "O"); + unicodeStr = Regex.Replace(unicodeStr, "[ìíîï]", "i"); + unicodeStr = Regex.Replace(unicodeStr, "[ÌÍÎÏ]", "I"); + unicodeStr = Regex.Replace(unicodeStr, "[š]", "s"); + unicodeStr = Regex.Replace(unicodeStr, "[Š]", "S"); + unicodeStr = Regex.Replace(unicodeStr, "[ñ]", "n"); + unicodeStr = Regex.Replace(unicodeStr, "[Ñ]", "N"); + unicodeStr = Regex.Replace(unicodeStr, "[ç]", "c"); + unicodeStr = Regex.Replace(unicodeStr, "[Ç]", "C"); + unicodeStr = Regex.Replace(unicodeStr, "[ÿ]", "y"); + unicodeStr = Regex.Replace(unicodeStr, "[Ÿ]", "Y"); + unicodeStr = Regex.Replace(unicodeStr, "[ž]", "z"); + unicodeStr = Regex.Replace(unicodeStr, "[Ž]", "Z"); + unicodeStr = Regex.Replace(unicodeStr, "[Ð]", "D"); + + //Ligatures + unicodeStr = Regex.Replace(unicodeStr, "[œ]", "oe"); + unicodeStr = Regex.Replace(unicodeStr, "[Œ]", "Oe"); + unicodeStr = Regex.Replace(unicodeStr, "[ꜳ]", "aa"); + unicodeStr = Regex.Replace(unicodeStr, "[Ꜳ]", "AA"); + unicodeStr = Regex.Replace(unicodeStr, "[æ]", "ae"); + unicodeStr = Regex.Replace(unicodeStr, "[Æ]", "AE"); + unicodeStr = Regex.Replace(unicodeStr, "[ꜵ]", "ao"); + unicodeStr = Regex.Replace(unicodeStr, "[Ꜵ]", "AO"); + unicodeStr = Regex.Replace(unicodeStr, "[ꜷ]", "au"); + unicodeStr = Regex.Replace(unicodeStr, "[Ꜷ]", "AU"); + unicodeStr = Regex.Replace(unicodeStr, "[«»ꜹꜻ]", "av"); + unicodeStr = Regex.Replace(unicodeStr, "[«»ꜸꜺ]", "AV"); + unicodeStr = Regex.Replace(unicodeStr, "[🙰]", "et"); + unicodeStr = Regex.Replace(unicodeStr, "[ff]", "ff"); + unicodeStr = Regex.Replace(unicodeStr, "[ffi]", "ffi"); + unicodeStr = Regex.Replace(unicodeStr, "[ffl]", "f‌f‌l"); + unicodeStr = Regex.Replace(unicodeStr, "[fi]", "fi"); + unicodeStr = Regex.Replace(unicodeStr, "[fl]", "fl"); + unicodeStr = Regex.Replace(unicodeStr, "[ƕ]", "hv"); + unicodeStr = Regex.Replace(unicodeStr, "[Ƕ]", "Hv"); + unicodeStr = Regex.Replace(unicodeStr, "[℔]", "lb"); + unicodeStr = Regex.Replace(unicodeStr, "[ꝏ]", "oo"); + unicodeStr = Regex.Replace(unicodeStr, "[Ꝏ]", "OO"); + unicodeStr = Regex.Replace(unicodeStr, "[st]", "st"); + unicodeStr = Regex.Replace(unicodeStr, "[ꜩ]", "tz"); + unicodeStr = Regex.Replace(unicodeStr, "[Ꜩ]", "TZ"); + unicodeStr = Regex.Replace(unicodeStr, "[ᵫ]", "ue"); + unicodeStr = Regex.Replace(unicodeStr, "[ꭣ]", "uo"); + + //Punctuation + unicodeStr = Regex.Replace(unicodeStr, "[«»\u2018\u2019\u201A\u201B\u2032\u2035]", "\'"); + unicodeStr = Regex.Replace(unicodeStr, "[«»\u201C\u201D\u201E\u201F\u2033\u2036]", "\""); + unicodeStr = Regex.Replace(unicodeStr, "[\u2026]", "..."); + unicodeStr = Regex.Replace(unicodeStr, "[\u1680]", "-"); + + //Spaces + unicodeStr = Regex.Replace(unicodeStr, "[«»\u00A0\u2000\u2002\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200F\u205F\u3000]", " "); + unicodeStr = Regex.Replace(unicodeStr, "[«»\u2001\u2003]", " "); + unicodeStr = Regex.Replace(unicodeStr, "[«»\u180E\u200B\uFEFF]", ""); + + return unicodeStr; + } + } +}