Merge pull request #1223 from Mbucari/master

New features, including spatial audio support
This commit is contained in:
rmcrackan 2025-04-28 13:34:40 -04:00 committed by GitHub
commit 8232b2b5e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 16586 additions and 824 deletions

5
.cdmurls.json Normal file
View File

@ -0,0 +1,5 @@
{
"CdmUrls": [
"https://ollj0gz40d.execute-api.us-west-2.amazonaws.com/default/AudibleCdm"
]
}

View File

@ -89,14 +89,6 @@ jobs:
run: | run: |
$bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\" $bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
$delfiles = @( $delfiles = @(
"libmp3lame.x64.so",
"libmp3lame.arm64.so",
"libmp3lame.x64.dylib",
"libmp3lame.arm64.dylib",
"ffmpegaac.x64.so",
"ffmpegaac.arm64.so",
"ffmpegaac.x64.dylib",
"ffmpegaac.arm64.dylib",
"WindowsConfigApp.exe", "WindowsConfigApp.exe",
"WindowsConfigApp.runtimeconfig.json", "WindowsConfigApp.runtimeconfig.json",
"WindowsConfigApp.deps.json" "WindowsConfigApp.deps.json"

View File

@ -17,6 +17,9 @@ These templates apply to both GUI and CLI.
- [Conditional Tags](#conditional-tags) - [Conditional Tags](#conditional-tags)
- [Tag Formatters](#tag-formatters) - [Tag Formatters](#tag-formatters)
- [Text Formatters](#text-formatters) - [Text Formatters](#text-formatters)
- [Series Formatters](#series-formatters)
- [Series List Formatters](#series-list-formatters)
- [Name Formatters](#name-formatters)
- [Name List Formatters](#name-list-formatters) - [Name List Formatters](#name-list-formatters)
- [Number Formatters](#number-formatters) - [Number Formatters](#number-formatters)
- [Date Formatters](#date-formatters) - [Date Formatters](#date-formatters)
@ -32,32 +35,33 @@ These tags will be replaced in the template with the audiobook's values.
|Tag|Description|Type| |Tag|Description|Type|
|-|-|-| |-|-|-|
|\<id\> **†**|Audible book ID (ASIN)|Text| |\<id\> **†**|Audible book ID (ASIN)|Text|
|\<title\>|Full title with subtitle|Text| |\<title\>|Full title with subtitle|[Text](#text-formatters)|
|\<title short\>|Title. Stop at first colon|Text| |\<title short\>|Title. Stop at first colon|[Text](#text-formatters)|
|\<audible title\>|Audible's title (does not include subtitle)|Text| |\<audible title\>|Audible's title (does not include subtitle)|[Text](#text-formatters)|
|\<audible subtitle\>|Audible's subtitle|Text| |\<audible subtitle\>|Audible's subtitle|[Text](#text-formatters)|
|\<author\>|Author(s)|Name List| |\<author\>|Author(s)|[Name List](#name-list-formatters)|
|\<first author\>|First author|Text| |\<first author\>|First author|[Name](#name-formatters)|
|\<narrator\>|Narrator(s)|Name List| |\<narrator\>|Narrator(s)|[Name List](#name-list-formatters)|
|\<first narrator\>|First narrator|Text| |\<first narrator\>|First narrator|[Name](#name-formatters)|
|\<series\>|Name of series|Text| |\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|\<series#\>|Number order in series|Number| |\<first series\>|First series|[Series](#series-formatters)|
|\<bitrate\>|File's original bitrate (Kbps)|Number| |\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|\<samplerate\>|File's original audio sample rate|Number| |\<bitrate\>|File's original bitrate (Kbps)|[Number](#number-formatters)|
|\<channels\>|Number of audio channels|Number| |\<samplerate\>|File's original audio sample rate|[Number](#number-formatters)|
|\<account\>|Audible account of this book|Text| |\<channels\>|Number of audio channels|[Number](#number-formatters)|
|\<account nickname\>|Audible account nickname of this book|Text| |\<account\>|Audible account of this book|[Text](#text-formatters)|
|\<locale\>|Region/country|Text| |\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|\<year\>|Year published|Number| |\<locale\>|Region/country|[Text](#text-formatters)|
|\<language\>|Book's language|Text| |\<year\>|Year published|[Number](#number-formatters)|
|\<language\>|Book's language|[Text](#text-formatters)|
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text| |\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|\<file date\>|File creation date/time.|DateTime| |\<file date\>|File creation date/time.|[DateTime](#date-formatters)|
|\<pub date\>|Audiobook publication date|DateTime| |\<pub date\>|Audiobook publication date|[DateTime](#date-formatters)|
|\<date added\>|Date the book added to your Audible account|DateTime| |\<date added\>|Date the book added to your Audible account|[DateTime](#date-formatters)|
|\<ch count\> **‡**|Number of chapters|Number| |\<ch count\> **‡**|Number of chapters|[Number](#number-formatters)|
|\<ch title\> **‡**|Chapter title|Text| |\<ch title\> **‡**|Chapter title|[Text](#text-formatters)|
|\<ch#\> **‡**|Chapter number|Number| |\<ch#\> **‡**|Chapter number|[Number](#number-formatters)|
|\<ch# 0\> **‡**|Chapter number with leading zeros|Number| |\<ch# 0\> **‡**|Chapter number with leading zeros|[Number](#number-formatters)|
**†** Does not support custom formatting **†** Does not support custom formatting
@ -95,11 +99,28 @@ As an example, this folder template will place all Liberated podcasts into a "Po
|L|Converts text to lowercase|\<title[L]\>|a study in scarlet a sherlock holmes novel| |L|Converts text to lowercase|\<title[L]\>|a study in scarlet a sherlock holmes novel|
|U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET| |U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET|
## Series Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|\{N \| # \| ID\}|Formats the series using<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1, B08376S3R2|
## Series List Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|separator()|Speficy the text used to join<br>multiple series names.<br><br>Default is ", "|`<series[separator(; )]>`|Sherlock Holmes; Some Other Series|
|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<author[format({L}, {ID}) separator(; )]>`|Sherlock Holmes, 1; Some Other Series, 1<hr>herlock Holmes, B08376S3R2; Some Other Series, B000000000|
|max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes|
## Name Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|\{T \| F \| M \| L \| S \| ID\}|Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br>\{ID\} = Audible Contributor ID<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<first narrator[{L}, {F}]>`<hr>`<first author[{L}, {F} _{ID}_]>`|Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
## Name List Formatters ## Name List Formatters
|Formatter|Description|Example Usage|Example Result| |Formatter|Description|Example Usage|Example Result|
|-|-|-|-| |-|-|-|-|
|separator()|Speficy the text used to join<br>multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry| |separator()|Speficy the text used to join<br>multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|format(\{T \| F \| M \| L \| S\})|Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<author[format({L}, {F})`<br>`separator(; )]>`|Doyle, Arthur; Fry, Stephen| |format(\{T \| F \| M \| L \| S \| ID\})|Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above.|`<author[format({L}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F}`<br>`_{ID}_) separator(; )]>`|Doyle, Arthur; Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
|sort(F \| M \| L)|Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle| |sort(F \| M \| L)|Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle| |max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|

View File

@ -53,13 +53,7 @@ if [ $? -ne 0 ]
fi fi
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json') delfiles=('LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
if [[ "$ARCH" == "arm64" ]]
then
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
else
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
fi
for n in "${delfiles[@]}" for n in "${delfiles[@]}"
do do

View File

@ -82,18 +82,7 @@ echo "Set CFBundleVersion to $VERSION"
sed -i -e "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist sed -i -e "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist
delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.so' 'ffmpegaac.x64.so' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json') delfiles=('MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json')
if [[ "$ARCH" == "arm64" ]]
then
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.arm64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.arm64.dylib $BUNDLE_MACOS/libmp3lame.dylib
else
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.x64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.x64.dylib $BUNDLE_MACOS/libmp3lame.dylib
fi
for n in "${delfiles[@]}" for n in "${delfiles[@]}"
do do

View File

@ -38,14 +38,12 @@ fi
BASEDIR=$(pwd) BASEDIR=$(pwd)
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json') delfiles=('LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
if [[ "$ARCH" == "x64" ]] if [[ "$ARCH" == "x64" ]]
then then
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
ARCH_RPM="x86_64" ARCH_RPM="x86_64"
ARCH="amd64" ARCH="amd64"
else else
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
ARCH_RPM="aarch64" ARCH_RPM="aarch64"
fi fi

View File

@ -13,7 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="1.1.4" /> <PackageReference Include="AAXClean.Codecs" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,7 +8,7 @@ namespace AaxDecrypter
{ {
public event EventHandler<AppleTags> RetrievedMetadata; public event EventHandler<AppleTags> RetrievedMetadata;
protected AaxFile AaxFile { get; private set; } protected Mp4File AaxFile { get; private set; }
protected Mp4Operation AaxConversion { get; set; } protected Mp4Operation AaxConversion { get; set; }
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions) protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
@ -29,14 +29,34 @@ namespace AaxDecrypter
FinalizeDownload(); FinalizeDownload();
} }
private Mp4File Open()
{
if (DownloadOptions.InputType is FileType.Dash)
{
var dash = new DashFile(InputFileStream);
dash.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return dash;
}
else if (DownloadOptions.InputType is FileType.Aax)
{
var aax = new AaxFile(InputFileStream);
aax.SetDecryptionKey(DownloadOptions.AudibleKey);
return aax;
}
else if (DownloadOptions.InputType is FileType.Aaxc)
{
var aax = new AaxFile(InputFileStream);
aax.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return aax;
}
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
}
protected bool Step_GetMetadata() protected bool Step_GetMetadata()
{ {
AaxFile = new AaxFile(InputFileStream); AaxFile = Open();
if (DownloadOptions.AudibleKey?.Length == 8 && DownloadOptions.AudibleIV is null) RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey);
else
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
if (DownloadOptions.StripUnabridged) if (DownloadOptions.StripUnabridged)
{ {
@ -87,8 +107,6 @@ namespace AaxDecrypter
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]"); OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(AaxFile.AppleTags.Cover); OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
return !IsCanceled; return !IsCanceled;
} }

View File

@ -114,7 +114,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
DownloadOptions.LameConfig DownloadOptions.LameConfig
); );
void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback) void newSplit(int currentChapter, ChapterInfo splitChapters, INewSplitCallback newSplitCallback)
{ {
MultiConvertFileProperties props = new() MultiConvertFileProperties props = new()
{ {
@ -151,7 +151,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
{ {
return Mp4File.RelocateMoovAsync(filename); return Mp4File.RelocateMoovAsync(filename);
} }
else return Mp4Operation.CompletedOperation; else return Mp4Operation.FromCompleted(AaxFile);
} }
} }
} }

View File

@ -23,7 +23,7 @@ namespace AaxDecrypter
public bool IsCanceled { get; protected set; } public bool IsCanceled { get; protected set; }
protected AsyncStepSequence AsyncSteps { get; } = new(); protected AsyncStepSequence AsyncSteps { get; } = new();
protected string OutputFileName { get; } protected string OutputFileName { get; }
protected IDownloadOptions DownloadOptions { get; } public IDownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream; protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
protected virtual long InputFilePosition => InputFileStream.Position; protected virtual long InputFilePosition => InputFileStream.Position;
private bool downloadFinished; private bool downloadFinished;
@ -178,19 +178,33 @@ namespace AaxDecrypter
FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(jsonDownloadState);
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) && if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
DownloadOptions.RetainEncryptedFile) DownloadOptions.RetainEncryptedFile &&
DownloadOptions.InputType is AAXClean.FileType fileType)
{ {
string aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
FileUtility.SaferMove(tempFilePath, aaxPath);
//Write aax decryption key //Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key"); string keyPath = Path.ChangeExtension(tempFilePath, ".key");
FileUtility.SaferDelete(keyPath); FileUtility.SaferDelete(keyPath);
string aaxPath;
if (string.IsNullOrEmpty(DownloadOptions.AudibleIV)) if (fileType is AAXClean.FileType.Aax)
{
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}"); await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}");
else aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
}
else if (fileType is AAXClean.FileType.Aaxc)
{
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}"); await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
aaxPath = Path.ChangeExtension(tempFilePath, ".aaxc");
}
else if (fileType is AAXClean.FileType.Dash)
{
await File.WriteAllTextAsync(keyPath, $"KeyId={DownloadOptions.AudibleKey}{Environment.NewLine}Key={DownloadOptions.AudibleIV}");
aaxPath = Path.ChangeExtension(tempFilePath, ".dash");
}
else
throw new InvalidOperationException($"Unknown file type: {fileType}");
FileUtility.SaferMove(tempFilePath, aaxPath);
OnFileCreated(aaxPath); OnFileCreated(aaxPath);
OnFileCreated(keyPath); OnFileCreated(keyPath);
@ -217,6 +231,7 @@ namespace AaxDecrypter
} }
catch catch
{ {
nfsp?.Target?.Dispose();
FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFilePath); FileUtility.SaferDelete(tempFilePath);
return nfsp = newNetworkFilePersister(); return nfsp = newNetworkFilePersister();

View File

@ -105,7 +105,7 @@ public class AverageSpeed
public AverageSpeed() : this(TimeSpan.FromSeconds(15), Significance.P10, TimeSpan.FromSeconds(3), Significance.P01) { } public AverageSpeed() : this(TimeSpan.FromSeconds(15), Significance.P10, TimeSpan.FromSeconds(3), Significance.P01) { }
/// <param name="slowWindow">Total moving average time window</param> /// <param name="slowWindow">Total moving average time window</param>
/// <param name="slowSignificance">T-test signifance level at which the newest speed will be considered different from the slow window's mean speed.</param> /// <param name="slowSignificance">T-test significance level at which the newest speed will be considered different from the slow window's mean speed.</param>
/// <param name="fastWindow">A shorter moving window of the most resent speeds. The average speed in <paramref name="fastWindow"/> is compared to the average speed in the rest of <paramref name="slowWindow"/> to quickly detect large changes in speed.</param> /// <param name="fastWindow">A shorter moving window of the most resent speeds. The average speed in <paramref name="fastWindow"/> is compared to the average speed in the rest of <paramref name="slowWindow"/> to quickly detect large changes in speed.</param>
/// <param name="fastSignificance">T-test significance level at which the mean speed in <paramref name="fastWindow"/> will be considered different from the mean speed of the remainder of <paramref name="slowWindow"/>.</param> /// <param name="fastSignificance">T-test significance level at which the mean speed in <paramref name="fastWindow"/> will be considered different from the mean speed of the remainder of <paramref name="slowWindow"/>.</param>
public AverageSpeed(TimeSpan slowWindow, Significance slowSignificance, TimeSpan fastWindow, Significance fastSignificance) public AverageSpeed(TimeSpan slowWindow, Significance slowSignificance, TimeSpan fastWindow, Significance fastSignificance)
@ -119,7 +119,7 @@ public class AverageSpeed
/// <summary>Add a new position to the moving average</summary> /// <summary>Add a new position to the moving average</summary>
public void AddPosition(double position) public void AddPosition(double position)
{ {
var now = DateTime.Now; var now = DateTime.UtcNow;
if (start == default) if (start == default)
start = now; start = now;

View File

@ -35,5 +35,6 @@ namespace AaxDecrypter
string GetMultipartFileName(MultiConvertFileProperties props); string GetMultipartFileName(MultiConvertFileProperties props);
string GetMultipartTitle(MultiConvertFileProperties props); string GetMultipartTitle(MultiConvertFileProperties props);
Task<string> SaveClipsAndBookmarksAsync(string fileName); Task<string> SaveClipsAndBookmarksAsync(string fileName);
public FileType? InputType { get; }
} }
} }

View File

@ -55,18 +55,23 @@ namespace AaxDecrypter
private CancellationTokenSource _cancellationSource { get; } = new(); private CancellationTokenSource _cancellationSource { get; } = new();
private EventWaitHandle _downloadedPiece { get; set; } private EventWaitHandle _downloadedPiece { get; set; }
private DateTime NextUpdateTime { get; set; }
#endregion #endregion
#region Constants #region Constants
//Download buffer size //Size of each range request. Android app uses 64MB chunks.
private const int DOWNLOAD_BUFF_SZ = 32 * 1024; private const int RANGE_REQUEST_SZ = 64 * 1024 * 1024;
//Download memory buffer size
private const int DOWNLOAD_BUFF_SZ = 8 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every //NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream. //DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024; private const int DATA_FLUSH_SZ = 1024 * 1024;
//Number of times per second the download rate is checkd and throttled //Number of times per second the download rate is checked and throttled
private const int THROTTLE_FREQUENCY = 8; private const int THROTTLE_FREQUENCY = 8;
//Minimum throttle rate. The minimum amount of data that can be throttled //Minimum throttle rate. The minimum amount of data that can be throttled
@ -110,10 +115,14 @@ namespace AaxDecrypter
/// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary> /// <summary> Update the <see cref="Dinah.Core.IO.JsonFilePersister{T}"/>. </summary>
private void OnUpdate() private void OnUpdate()
{ {
RequestHeaders["Range"] = $"bytes={WritePosition}-";
try try
{ {
Updated?.Invoke(this, EventArgs.Empty); if (DateTime.UtcNow > NextUpdateTime)
{
Updated?.Invoke(this, EventArgs.Empty);
//JsonFilePersister Will not allow update intervals shorter than 100 milliseconds
NextUpdateTime = DateTime.UtcNow.AddMilliseconds(110);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -135,7 +144,6 @@ namespace AaxDecrypter
throw new InvalidOperationException("Cannot change Uri after download has started."); throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile; Uri = uriToSameFile;
RequestHeaders["Range"] = $"bytes={WritePosition}-";
} }
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary> /// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
@ -151,39 +159,82 @@ namespace AaxDecrypter
if (ContentLength != 0 && WritePosition > ContentLength) if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
//Initiate connection with the first request block and
//get the total content length before returning.
using var client = new HttpClient();
var response = await RequestNextByteRangeAsync(client);
if (ContentLength != 0 && ContentLength != response.FileSize)
throw new WebException($"Content length of 0x{response.FileSize:X10} differs from partially downloaded content length of 0x{ContentLength:X10}");
ContentLength = response.FileSize;
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Hand off the open request to the downloader to download and write data to file.
DownloadTask = Task.Run(() => DownloadLoopInternal(response), _cancellationSource.Token);
}
private async Task DownloadLoopInternal(BlockResponse initialResponse)
{
await DownloadToFile(initialResponse);
initialResponse.Dispose();
try
{
using var client = new HttpClient();
while (WritePosition < ContentLength && !IsCancelled)
{
using var response = await RequestNextByteRangeAsync(client);
await DownloadToFile(response);
}
}
finally
{
_writeFile.Close();
}
}
private async Task<BlockResponse> RequestNextByteRangeAsync(HttpClient client)
{
var request = new HttpRequestMessage(HttpMethod.Get, Uri); var request = new HttpRequestMessage(HttpMethod.Get, Uri);
foreach (var header in RequestHeaders) foreach (var header in RequestHeaders)
request.Headers.Add(header.Key, header.Value); request.Headers.Add(header.Key, header.Value);
var response = await new HttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token); request.Headers.Add("Range", $"bytes={WritePosition}-{WritePosition + RANGE_REQUEST_SZ - 1}");
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
if (response.StatusCode != HttpStatusCode.PartialContent) if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal var totalSize = response.Content.Headers.ContentRange?.Length ??
//to the complete file length if requesting Range: bytes=0- throw new WebException("The response did not contain a total content length.");
if (WritePosition == 0)
ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
var networkStream = await response.Content.ReadAsStreamAsync(_cancellationSource.Token); var rangeSize = response.Content.Headers.ContentLength ??
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); throw new WebException($"The response did not contain a {nameof(response.Content.Headers.ContentLength)};");
//Download the file in the background. return new BlockResponse(response, rangeSize, totalSize);
}
DownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token); private readonly record struct BlockResponse(HttpResponseMessage Response, long BlockSize, long FileSize) : IDisposable
{
public void Dispose() => Response?.Dispose();
} }
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary> /// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
private async Task DownloadFile(Stream networkStream) private async Task DownloadToFile(BlockResponse block)
{ {
var endPosition = WritePosition + block.BlockSize;
var networkStream = await block.Response.Content.ReadAsStreamAsync(_cancellationSource.Token);
var downloadPosition = WritePosition; var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ; var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ]; var buff = new byte[DOWNLOAD_BUFF_SZ];
try try
{ {
DateTime startTime = DateTime.Now; DateTime startTime = DateTime.UtcNow;
long bytesReadSinceThrottle = 0; long bytesReadSinceThrottle = 0;
int bytesRead; int bytesRead;
do do
@ -218,14 +269,15 @@ namespace AaxDecrypter
#endregion #endregion
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0); } while (downloadPosition < endPosition && !IsCancelled && bytesRead > 0);
await _writeFile.FlushAsync(_cancellationSource.Token);
WritePosition = downloadPosition; WritePosition = downloadPosition;
if (!IsCancelled && WritePosition < ContentLength) if (!IsCancelled && WritePosition < endPosition)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength) if (WritePosition > endPosition)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
} }
catch (TaskCanceledException) catch (TaskCanceledException)
@ -235,7 +287,6 @@ namespace AaxDecrypter
finally finally
{ {
networkStream.Close(); networkStream.Close();
_writeFile.Close();
_downloadedPiece.Set(); _downloadedPiece.Set();
OnUpdate(); OnUpdate();
} }

View File

@ -418,7 +418,6 @@ namespace AppScaffolding
public List<string> Filters { get; set; } = new(); public List<string> Filters { get; set; } = new();
} }
public static void migrate_to_v12_0_1(Configuration config) public static void migrate_to_v12_0_1(Configuration config)
{ {
#nullable enable #nullable enable

View File

@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="NPOI" Version="2.7.2" /> <PackageReference Include="NPOI" Version="2.7.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -47,6 +47,22 @@ namespace AudibleUtilities
update_no_validate(); update_no_validate();
} }
} }
private string _cdm;
[JsonProperty]
public string Cdm
{
get => _cdm;
set
{
if (value is null)
return;
_cdm = value;
update_no_validate();
}
}
[JsonIgnore] [JsonIgnore]
public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly(); public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly();
#endregion #endregion

View File

@ -5,7 +5,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AudibleApi" Version="9.3.2.1" /> <PackageReference Include="AudibleApi" Version="9.4.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.30.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -20,4 +21,9 @@
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Update="Widevine\Cdm.*.cs">
<DependentUpon>Cdm.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,189 @@
using AudibleApi;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using AudibleApi.Cryptography;
using Newtonsoft.Json.Linq;
using Dinah.Core.Net.Http;
using System.Text.Json.Nodes;
#nullable enable
namespace AudibleUtilities.Widevine;
public partial class Cdm
{
/// <summary>
/// Get a <see cref="Cdm"/> from <see cref="AccountsSettings"/> or from the API.
/// </summary>
/// <returns>A <see cref="Cdm"/> if successful, otherwise <see cref="null"/></returns>
public static async Task<Cdm?> GetCdmAsync()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
//Check if there are any Android accounts. If not, we can't use Widevine.
if (!persister.Target.Accounts.Any(a => a.IdentityTokens.DeviceType == Resources.DeviceType))
return null;
if (!string.IsNullOrEmpty(persister.Target.Cdm))
{
try
{
var cdm = Convert.FromBase64String(persister.Target.Cdm);
return new Cdm(new Device(cdm));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error loading CDM from account settings.");
persister.Target.Cdm = string.Empty;
//Clear the stored Cdm and try getting a fresh one from the server.
}
}
if (string.IsNullOrEmpty(persister.Target.Cdm))
{
using var client = new HttpClient();
if (await GetCdmUris(client) is not Uri[] uris)
return null;
//try to get a CDM file for any account that's registered as an android device.
//CDMs are not account-specific, so it doesn't matter which account we're successful with.
foreach (var account in persister.Target.Accounts.Where(a => a.IdentityTokens.DeviceType == Resources.DeviceType))
{
try
{
var requestMessage = CreateApiRequest(account);
await TestApiRequest(client, new JsonObject { { "body", requestMessage.ToString() } });
//Try all CDM URIs until a CDM has been retrieved successfully
foreach (var uri in uris)
{
try
{
var resp = await client.PostAsync(uri, ((HttpBody)requestMessage).Content);
if (!resp.IsSuccessStatusCode)
{
var message = await resp.Content.ReadAsStringAsync();
throw new ApiErrorException(uri, null, message);
}
var cdmBts = await resp.Content.ReadAsByteArrayAsync();
var device = new Device(cdmBts);
persister.Target.Cdm = Convert.ToBase64String(cdmBts);
return new Cdm(device);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error getting a CDM from URI: " + uri);
//try the next URI
}
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error getting a CDM for account: " + account.MaskedLogEntry);
//try the next Account
}
}
}
return null;
}
/// <summary>
/// Get a list of CDM API URIs from the main Gitgub repository's .cdmurls.json file.
/// </summary>
/// <returns>If successful, an array of URIs to try. Otherwise null</returns>
private static async Task<Uri[]?> GetCdmUris(HttpClient httpClient)
{
const string CdmUrlListFile = "https://raw.githubusercontent.com/rmcrackan/Libation/refs/heads/master/.cdmurls.json";
try
{
var fileContents = await httpClient.GetStringAsync(CdmUrlListFile);
var releaseIndex = JObject.Parse(fileContents);
var urlArray = releaseIndex["CdmUrls"] as JArray;
if (urlArray is null)
throw new System.IO.InvalidDataException("CDM url list not found in JSON: " + fileContents);
var uris = urlArray.Select(u => u.Value<string>()).OfType<string>().Select(u => new Uri(u)).ToArray();
if (uris.Length == 0)
throw new System.IO.InvalidDataException("No CDM url found in JSON: " + fileContents);
return uris;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error getting CDM URLs");
return null;
}
}
static readonly string[] TLDs = ["com", "co.uk", "com.au", "com.br", "ca", "fr", "de", "in", "it", "co.jp", "es"];
//Ensure that the request can be made successfully before sending it to the API
//The API uses System.Text.Json, so perform test with same.
private static async Task TestApiRequest(HttpClient client, JsonObject input)
{
if (input["body"]?.GetValue<string>() is not string body
|| JsonNode.Parse(body) is not JsonNode bodyJson)
throw new Exception("Api request doesn't contain a body");
if (bodyJson?["Url"]?.GetValue<string>() is not string url
|| !Uri.TryCreate(url, UriKind.Absolute, out var uri))
throw new Exception("Api request doesn't contain a url");
if (!TLDs.Select(tld => "api.audible." + tld).Contains(uri.Host.ToLower()))
throw new Exception($"Unknown Audible Api domain: {uri.Host}");
if (bodyJson?["Headers"] is not JsonObject headers)
throw new Exception($"Api request doesn't contain any headers");
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
Dictionary<string, string>? headersDict = null;
try
{
headersDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(headers);
}
catch (Exception ex)
{
throw new Exception("Failed to read Audible Api headers.", ex);
}
if (headersDict is null)
throw new Exception("Failed to read Audible Api headers.");
foreach (var kvp in headersDict)
request.Headers.Add(kvp.Key, kvp.Value);
using var resp = await client.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
/// <summary>
/// Create a request body to send to the API
/// </summary>
/// <param name="account">An authenticated account</param>
private static JObject CreateApiRequest(Account account)
{
const string ACCOUNT_INFO_PATH = "/1.0/account/information";
var message = new HttpRequestMessage(HttpMethod.Get, ACCOUNT_INFO_PATH);
message.SignRequest(
DateTime.UtcNow,
account.IdentityTokens.AdpToken,
account.IdentityTokens.PrivateKey);
return new JObject
{
{ "Url", new Uri(account.Locale.AudibleApiUri(), ACCOUNT_INFO_PATH) },
{ "Headers", JObject.FromObject(message.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Single())) }
};
}
}

View File

@ -0,0 +1,300 @@
using Google.Protobuf;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
#nullable enable
namespace AudibleUtilities.Widevine;
public enum KeyType
{
/// <summary>
/// Exactly one key of this type must appear.
/// </summary>
Signing = 1,
/// <summary>
/// Content key.
/// </summary>
Content = 2,
/// <summary>
/// Key control block for license renewals. No key.
/// </summary>
KeyControl = 3,
/// <summary>
/// wrapped keys for auxiliary crypto operations.
/// </summary>
OperatorSession = 4,
/// <summary>
/// Entitlement keys.
/// </summary>
Entitlement = 5,
/// <summary>
/// Partner-specific content key.
/// </summary>
OemContent = 6,
}
public interface ISession : IDisposable
{
string? GetLicenseChallenge(MpegDash dash);
WidevineKey[] ParseLicense(string licenseMessage);
}
public class WidevineKey
{
public Guid Kid { get; }
public KeyType Type { get; }
public byte[] Key { get; }
internal WidevineKey(Guid kid, License.Types.KeyContainer.Types.KeyType type, byte[] key)
{
Kid = kid;
Type = (KeyType)type;
Key = key;
}
public override string ToString() => $"{Convert.ToHexString(Kid.ToByteArray()).ToLower()}:{Convert.ToHexString(Key).ToLower()}";
}
public partial class Cdm
{
public static Guid WidevineContentProtection { get; } = new("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
private const int MAX_NUM_OF_SESSIONS = 16;
internal Device Device { get; }
private ConcurrentDictionary<Guid, Session> Sessions { get; } = new(-1, MAX_NUM_OF_SESSIONS);
internal Cdm(Device device)
{
Device = device;
}
public ISession OpenSession()
{
if (Sessions.Count == MAX_NUM_OF_SESSIONS)
throw new Exception("Too Many Sessions");
var session = new Session(Sessions.Count + 1, this);
var ddd = Sessions.TryAdd(session.Id, session);
return session;
}
#region Session
internal class Session : ISession
{
public Guid Id { get; } = Guid.NewGuid();
private int SessionNumber { get; }
private Cdm Cdm { get; }
private byte[]? EncryptionContext { get; set; }
private byte[]? AuthenticationContext { get; set; }
public Session(int number, Cdm cdm)
{
SessionNumber = number;
Cdm = cdm;
}
private string GetRequestId()
=> $"{RandomUint():x8}00000000{Convert.ToHexString(BitConverter.GetBytes((long)SessionNumber)).ToLowerInvariant()}";
public void Dispose()
{
if (Cdm.Sessions.ContainsKey(Id))
Cdm.Sessions.TryRemove(Id, out var session);
}
public string? GetLicenseChallenge(MpegDash dash)
{
if (!dash.TryGetPssh(Cdm.WidevineContentProtection, out var pssh))
return null;
var licRequest = new LicenseRequest
{
ClientId = Cdm.Device.ClientId,
ContentId = new()
{
WidevinePsshData = new()
{
LicenseType = LicenseType.Offline,
RequestId = ByteString.CopyFrom(GetRequestId(), Encoding.ASCII)
}
},
Type = LicenseRequest.Types.RequestType.New,
RequestTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ProtocolVersion = ProtocolVersion.Version21,
KeyControlNonce = RandomUint()
};
licRequest.ContentId.WidevinePsshData.PsshData.Add(ByteString.CopyFrom(pssh.InitData));
var licRequestBts = licRequest.ToByteArray();
EncryptionContext = CreateContext("ENCRYPTION", 128, licRequestBts);
AuthenticationContext = CreateContext("AUTHENTICATION", 512, licRequestBts);
var signedMessage = new SignedMessage
{
Type = SignedMessage.Types.MessageType.LicenseRequest,
Msg = ByteString.CopyFrom(licRequestBts),
Signature = ByteString.CopyFrom(Cdm.Device.SignMessage(licRequestBts))
};
return Convert.ToBase64String(signedMessage.ToByteArray());
}
public WidevineKey[] ParseLicense(string licenseMessage)
{
if (EncryptionContext is null || AuthenticationContext is null)
throw new InvalidOperationException($"{nameof(GetLicenseChallenge)}() must be called before calling {nameof(ParseLicense)}()");
var signedMessage = SignedMessage.Parser.ParseFrom(Convert.FromBase64String(licenseMessage));
if (signedMessage.Type != SignedMessage.Types.MessageType.License)
throw new InvalidDataException("Invalid license");
var sessionKey = Cdm.Device.DecryptSessionKey(signedMessage.SessionKey.ToByteArray());
if (!VerifySignature(signedMessage, AuthenticationContext, sessionKey))
throw new InvalidDataException("Message signature is invalid");
var license = License.Parser.ParseFrom(signedMessage.Msg);
var keyToTheKeys = DeriveKey(sessionKey, EncryptionContext, 1);
return DecryptKeys(keyToTheKeys, license.Key);
}
private static WidevineKey[] DecryptKeys(byte[] keyToTheKeys, IList<License.Types.KeyContainer> licenseKeys)
{
using var aes = Aes.Create();
aes.Key = keyToTheKeys;
var keys = new WidevineKey[licenseKeys.Count];
for (int i = 0; i < licenseKeys.Count; i++)
{
var keyContainer = licenseKeys[i];
var keyBytes = aes.DecryptCbc(keyContainer.Key.ToByteArray(), keyContainer.Iv.ToByteArray(), PaddingMode.PKCS7);
var id = keyContainer.Id.ToByteArray();
if (id.Length > 16)
{
var tryB64 = new byte[id.Length * 3 / 4];
if (Convert.TryFromBase64String(Encoding.ASCII.GetString(id), tryB64, out int bytesWritten))
{
id = tryB64;
}
Array.Resize(ref id, 16);
}
else if (id.Length < 16)
{
id = id.Append(new byte[16 - id.Length]);
}
keys[i] = new WidevineKey(new Guid(id), keyContainer.Type, keyBytes);
}
return keys;
}
private static bool VerifySignature(SignedMessage signedMessage, byte[] authContext, byte[] sessionKey)
{
var mac_key_server = DeriveKey(sessionKey, authContext, 1).Append(DeriveKey(sessionKey, authContext, 2));
var hmacData = (signedMessage.OemcryptoCoreMessage?.ToByteArray() ?? []).Append(signedMessage.Msg?.ToByteArray() ?? []);
var computed_signature = HMACSHA256.HashData(mac_key_server, hmacData);
return computed_signature.SequenceEqual(signedMessage.Signature);
}
private static byte[] DeriveKey(byte[] session_key, byte[] context, int counter)
{
var data = new byte[context.Length + 1];
Array.Copy(context, 0, data, 1, context.Length);
data[0] = (byte)counter;
return AESCMAC(session_key, data);
}
private static byte[] AESCMAC(byte[] key, byte[] data)
{
using var aes = Aes.Create();
aes.Key = key;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] subKey = aes.EncryptCbc(new byte[16], new byte[16], PaddingMode.None);
nextSubKey();
// MAC computing
if ((data.Length == 0) || (data.Length % 16 != 0))
{
// If the size of the input message block is not equal to a positive
// multiple of the block size (namely, 128 bits), the last block shall
// be padded with 10^i
nextSubKey();
var padLen = 16 - data.Length % 16;
Array.Resize(ref data, data.Length + padLen);
data[^padLen] = 0x80;
}
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < subKey.Length; j++)
data[data.Length - 16 + j] ^= subKey[j];
// The result of the previous process will be the input of the last encryption.
byte[] encResult = aes.EncryptCbc(data, new byte[16], PaddingMode.None);
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
return HashValue;
void nextSubKey()
{
const byte const_Rb = 0x87;
if (Rol(subKey) != 0)
subKey[15] ^= const_Rb;
static int Rol(byte[] b)
{
int carry = 0;
for (int i = b.Length - 1; i >= 0; i--)
{
ushort u = (ushort)(b[i] << 1);
b[i] = (byte)((u & 0xff) + carry);
carry = (u & 0xff00) >> 8;
}
return carry;
}
}
}
private static byte[] CreateContext(string label, int keySize, byte[] licRequestBts)
{
var contextSize = label.Length + 1 + licRequestBts.Length + sizeof(int);
var context = new byte[contextSize];
var numChars = Encoding.ASCII.GetBytes(label.AsSpan(), context);
Array.Copy(licRequestBts, 0, context, numChars + 1, licRequestBts.Length);
var numBts = BitConverter.GetBytes(keySize);
if (BitConverter.IsLittleEndian)
Array.Reverse(numBts);
Array.Copy(numBts, 0, context, context.Length - sizeof(int), sizeof(int));
return context;
}
private static uint RandomUint()
{
var bts = new byte[4];
new Random().NextBytes(bts);
return BitConverter.ToUInt32(bts, 0);
}
}
#endregion
}

View File

@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Security.Cryptography;
#nullable enable
namespace AudibleUtilities.Widevine;
internal enum DeviceTypes : byte
{
Unknown = 0,
Chrome = 1,
Android = 2
}
internal class Device
{
public DeviceTypes Type { get; }
public int FileVersion { get; }
public int SecurityLevel { get; }
public int Flags { get; }
public RSA CdmKey { get; }
internal ClientIdentification ClientId { get; }
public Device(Span<byte> fileData)
{
if (fileData.Length < 7 || fileData[0] != 'W' || fileData[1] != 'V' || fileData[2] != 'D')
throw new InvalidDataException();
FileVersion = fileData[3];
Type = (DeviceTypes)fileData[4];
SecurityLevel = fileData[5];
Flags = fileData[6];
if (FileVersion != 2)
throw new InvalidDataException($"Unknown CDM File Version: '{FileVersion}'");
if (Type != DeviceTypes.Android)
throw new InvalidDataException($"Unknown CDM Type: '{Type}'");
if (SecurityLevel != 3)
throw new InvalidDataException($"Unknown CDM Security Level: '{SecurityLevel}'");
var privateKeyLength = (fileData[7] << 8) | fileData[8];
if (privateKeyLength <= 0 || fileData.Length < 9 + privateKeyLength + 2)
throw new InvalidDataException($"Invalid private key length: '{privateKeyLength}'");
var clientIdLength = (fileData[9 + privateKeyLength] << 8) | fileData[10 + privateKeyLength];
if (clientIdLength <= 0 || fileData.Length < 11 + privateKeyLength + clientIdLength)
throw new InvalidDataException($"Invalid client id length: '{clientIdLength}'");
ClientId = ClientIdentification.Parser.ParseFrom(fileData.Slice(11 + privateKeyLength));
CdmKey = RSA.Create();
CdmKey.ImportRSAPrivateKey(fileData.Slice(9, privateKeyLength), out _);
}
public byte[] SignMessage(byte[] message)
{
using var sha1 = SHA1.Create();
var digestion = sha1.ComputeHash(message);
return CdmKey.SignHash(digestion, HashAlgorithmName.SHA1, RSASignaturePadding.Pss);
}
public bool VerifyMessage(byte[] message, byte[] signature)
{
using var sha1 = SHA1.Create();
var digestion = sha1.ComputeHash(message);
return CdmKey.VerifyHash(digestion, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pss);
}
public byte[] DecryptSessionKey(byte[] sessionKey)
=> CdmKey.Decrypt(sessionKey, RSAEncryptionPadding.OaepSHA1);
}

View File

@ -0,0 +1,15 @@
using System;
#nullable enable
namespace AudibleUtilities.Widevine;
internal static class Extensions
{
public static T[] Append<T>(this T[] message, T[] appendData)
{
var origLength = message.Length;
Array.Resize(ref message, origLength + appendData.Length);
Array.Copy(appendData, 0, message, origLength, appendData.Length);
return message;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
using Mpeg4Lib.Boxes;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
#nullable enable
namespace AudibleUtilities.Widevine;
public class MpegDash
{
private const string MpegDashNamespace = "urn:mpeg:dash:schema:mpd:2011";
private const string CencNamespace = "urn:mpeg:cenc:2013";
private const string UuidPreamble = "urn:uuid:";
private XElement DashMpd { get; }
private static XmlNamespaceManager NamespaceManager { get; } = new(new NameTable());
static MpegDash()
{
NamespaceManager.AddNamespace("dash", MpegDashNamespace);
NamespaceManager.AddNamespace("cenc", CencNamespace);
}
public MpegDash(Stream contents)
{
DashMpd = XElement.Load(contents);
}
public bool TryGetUri(Uri baseUri, [NotNullWhen(true)] out Uri? fileUri)
{
foreach (var baseUrl in DashMpd.XPathSelectElements("/dash:Period/dash:AdaptationSet/dash:Representation/dash:BaseURL", NamespaceManager))
{
try
{
fileUri = new Uri(baseUri, baseUrl.Value);
return true;
}
catch
{
fileUri = null;
return false;
}
}
fileUri = null;
return false;
}
public bool TryGetPssh(Guid protectionSystemId, [NotNullWhen(true)] out PsshBox? pssh)
{
foreach (var psshEle in DashMpd.XPathSelectElements("/dash:Period/dash:AdaptationSet/dash:ContentProtection/cenc:pssh", NamespaceManager))
{
if (psshEle?.Value?.Trim() is string psshStr
&& psshEle.Parent?.Attribute(XName.Get("schemeIdUri")) is XAttribute scheme
&& scheme.Value is string uuid
&& uuid.Equals(UuidPreamble + protectionSystemId.ToString(), StringComparison.OrdinalIgnoreCase))
{
Span<byte> buffer = new byte[psshStr.Length * 3 / 4];
if (Convert.TryFromBase64String(psshStr, buffer, out var written))
{
using var ms = new MemoryStream(buffer.Slice(0, written).ToArray());
pssh = BoxFactory.CreateBox(ms, null) as PsshBox;
return pssh is not null;
}
}
}
pssh = null;
return false;
}
}

View File

@ -12,12 +12,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.1.1" /> <PackageReference Include="Dinah.Core" Version="9.0.1.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" /> <PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -43,5 +43,7 @@ namespace DataLayer
} }
public override string ToString() => Name; public override string ToString() => Name;
public void SetAudibleContributorId(string audibleContributorId)
=> AudibleContributorId = audibleContributorId;
} }
} }

View File

@ -61,19 +61,19 @@ namespace DtoImporterService
private int upsertPeople(List<Person> people) private int upsertPeople(List<Person> people)
{ {
var hash = people var qtyNew = 0;
// new people only foreach (var person in people)
.Where(p => !Cache.ContainsKey(p.Name))
// remove duplicates by Name. first in wins
.ToDictionarySafe(p => p.Name);
foreach (var kvp in hash)
{ {
var person = kvp.Value; if (!Cache.TryGetValue(person.Name, out var contributor))
addContributor(person.Name, person.Asin); {
contributor = createContributor(person.Name, person.Asin);
qtyNew++;
}
updateContributor(person, contributor);
} }
return hash.Count; return qtyNew;
} }
// only use after loading contributors => local // only use after loading contributors => local
@ -86,16 +86,22 @@ namespace DtoImporterService
.ToHashSet(); .ToHashSet();
foreach (var pub in hash) foreach (var pub in hash)
addContributor(pub); createContributor(pub);
return hash.Count; return hash.Count;
} }
private Contributor addContributor(string name, string id = null) private void updateContributor(Person person, Contributor contributor)
{
if (person.Asin != contributor.AudibleContributorId)
contributor.SetAudibleContributorId(person.Asin);
}
private Contributor createContributor(string name, string id = null)
{ {
try try
{ {
var newContrib = new Contributor(name); var newContrib = new Contributor(name, id);
var entityEntry = DbContext.Contributors.Add(newContrib); var entityEntry = DbContext.Contributors.Add(newContrib);
var entity = entityEntry.Entity; var entity = entityEntry.Entity;

View File

@ -62,7 +62,7 @@ namespace DtoImporterService
existing.SetAccount(item.AccountId); existing.SetAccount(item.AccountId);
} }
existing.AbsentFromLastScan = isPlusTitleUnavailable(item); existing.AbsentFromLastScan = isUnavailable(item);
} }
else else
{ {
@ -71,7 +71,7 @@ namespace DtoImporterService
item.DtoItem.DateAdded, item.DtoItem.DateAdded,
item.AccountId) item.AccountId)
{ {
AbsentFromLastScan = isPlusTitleUnavailable(item) AbsentFromLastScan = isUnavailable(item)
}; };
try try
@ -113,7 +113,13 @@ namespace DtoImporterService
} }
private static ImportItem tieBreak(ImportItem item1, ImportItem item2) private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
=> isPlusTitleUnavailable(item1) && !isPlusTitleUnavailable(item2) ? item2 : item1; => isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1;
private static bool isUnavailable(ImportItem item)
=> isFutureRelease(item) || isPlusTitleUnavailable(item);
private static bool isFutureRelease(ImportItem item)
=> item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow;
private static bool isPlusTitleUnavailable(ImportItem item) private static bool isPlusTitleUnavailable(ImportItem item)
=> item.DtoItem.ContentType is null => item.DtoItem.ContentType is null

View File

@ -15,34 +15,6 @@ namespace FileLiberator
public event EventHandler<byte[]> CoverImageDiscovered; public event EventHandler<byte[]> CoverImageDiscovered;
public abstract Task CancelAsync(); public abstract Task CancelAsync();
protected LameConfig GetLameOptions(Configuration config)
{
LameConfig lameConfig = new()
{
Mode = MPEGMode.Mono,
Quality = config.LameEncoderQuality,
OutputSampleRate = (int)config.MaxSampleRate
};
if (config.LameTargetBitrate)
{
if (config.LameConstantBitrate)
lameConfig.BitRate = config.LameBitrate;
else
{
lameConfig.ABRRateKbps = config.LameBitrate;
lameConfig.VBR = VBRMode.ABR;
lameConfig.WriteVBRTag = true;
}
}
else
{
lameConfig.VBR = VBRMode.Default;
lameConfig.VBRQuality = config.LameVBRQuality;
lameConfig.WriteVBRTag = true;
}
return lameConfig;
}
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title); protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
protected void OnTitleDiscovered(object _, string title) protected void OnTitleDiscovered(object _, string title)
{ {

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace FileLiberator namespace FileLiberator
{ {

View File

@ -44,13 +44,17 @@ namespace FileLiberator
var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read)); var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
//AAXClean.Codecs only supports decoding AAC and E-AC-3 audio.
if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null)
continue;
OnTitleDiscovered(m4bBook.AppleTags.Title); OnTitleDiscovered(m4bBook.AppleTags.Title);
OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor); OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor);
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator); OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
OnCoverImageDiscovered(m4bBook.AppleTags.Cover); OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
var config = Configuration.Instance; var config = Configuration.Instance;
var lameConfig = GetLameOptions(config); var lameConfig = DownloadOptions.GetLameOptions(config);
var chapters = m4bBook.GetChaptersFromMetadata(); var chapters = m4bBook.GetChaptersFromMetadata();
//Finishing configuring lame encoder. //Finishing configuring lame encoder.
AaxDecrypter.MpegUtil.ConfigureLameOptions( AaxDecrypter.MpegUtil.ConfigureLameOptions(

View File

@ -122,15 +122,13 @@ namespace FileLiberator
downloadValidation(libraryBook); downloadValidation(libraryBook);
var quality = (AudibleApi.DownloadQuality)config.FileDownloadQuality;
var api = await libraryBook.GetApiAsync(); var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, quality);
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
using var dlOptions = await DownloadOptions.InitiateDownloadAsync(api, libraryBook, config);
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower()); var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
if (contentLic.DrmType != DrmType.Adrm) if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions); abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
else else
{ {
@ -140,7 +138,7 @@ namespace FileLiberator
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions); new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
if (config.AllowLibationFixup) if (config.AllowLibationFixup)
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.LowestCategoryNames()); converter.RetrievedMetadata += Converter_RetrievedMetadata;
abDownloader = converter; abDownloader = converter;
} }
@ -158,194 +156,40 @@ namespace FileLiberator
if (success && config.SaveMetadataToFile) if (success && config.SaveMetadataToFile)
{ {
var metadataFile = Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json"); var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ChapterInfo)); item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo));
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ContentReference)); item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference));
File.WriteAllText(metadataFile, item.SourceJson.ToString()); File.WriteAllText(metadataFile, item.SourceJson.ToString());
OnFileCreated(libraryBook, metadataFile); OnFileCreated(libraryBook, metadataFile);
} }
return success; return success;
} }
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic) private void Converter_RetrievedMetadata(object sender, AAXClean.AppleTags tags)
{ {
//If DrmType != Adrm the delivered file is an unencrypted mp3. if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options)
return;
var outputFormat tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
= contentLic.DrmType != DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy) tags.Album ??= tags.Title;
? OutputFormat.Mp3 tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
: OutputFormat.M4b; tags.AlbumArtists ??= tags.Artist;
tags.Generes = string.Join(", ", options.LibraryBook.Book.LowestCategoryNames());
long chapterStartMs tags.ProductID ??= options.ContentMetadata.ContentReference.Sku;
= config.StripAudibleBrandAudio tags.Comment ??= options.LibraryBook.Book.Description;
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs tags.LongDescription ??= tags.Comment;
: 0; tags.Publisher ??= options.LibraryBook.Book.Publisher;
tags.Narrator ??= string.Join("; ", options.LibraryBook.Book.Narrators.Select(n => n.Name));
//Set the requested AudioFormat for use in file naming templates tags.Asin = options.LibraryBook.Book.AudibleProductId;
libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat); tags.Acr = options.ContentMetadata.ContentReference.Acr;
tags.Version = options.ContentMetadata.ContentReference.Version;
var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl) if (options.LibraryBook.Book.DatePublished is DateTime pubDate)
{ {
AudibleKey = contentLic?.Voucher?.Key, tags.Year ??= pubDate.Year.ToString();
AudibleIV = contentLic?.Voucher?.Iv, tags.ReleaseDate ??= pubDate.ToString("dd-MMM-yyyy");
OutputFormat = outputFormat,
LameConfig = GetLameOptions(config),
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
};
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
var chapters
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
.OrderBy(c => c.StartOffsetMs)
.ToList();
if (config.MergeOpeningAndEndCredits)
combineCredits(chapters);
for (int i = 0; i < chapters.Count; i++)
{
var chapter = chapters[i];
long chapLenMs = chapter.LengthMs;
if (i == 0)
chapLenMs -= chapterStartMs;
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
}
return dlOptions;
}
/*
Flatten Audible's new hierarchical chapters, combining children into parents.
Audible may deliver chapters like this:
00:00 - 00:10 Opening Credits
00:10 - 00:12 Book 1
00:12 - 00:14 | Part 1
00:14 - 01:40 | | Chapter 1
01:40 - 03:20 | | Chapter 2
03:20 - 03:22 | Part 2
03:22 - 05:00 | | Chapter 3
05:00 - 06:40 | | Chapter 4
06:40 - 06:42 Book 2
06:42 - 06:44 | Part 3
06:44 - 08:20 | | Chapter 5
08:20 - 10:00 | | Chapter 6
10:00 - 10:02 | Part 4
10:02 - 11:40 | | Chapter 7
11:40 - 13:20 | | Chapter 8
13:20 - 13:30 End Credits
And flattenChapters will combine them into this:
00:00 - 00:10 Opening Credits
00:10 - 01:40 Book 1: Part 1: Chapter 1
01:40 - 03:20 Book 1: Part 1: Chapter 2
03:20 - 05:00 Book 1: Part 2: Chapter 3
05:00 - 06:40 Book 1: Part 2: Chapter 4
06:40 - 08:20 Book 2: Part 3: Chapter 5
08:20 - 10:00 Book 2: Part 3: Chapter 6
10:00 - 11:40 Book 2: Part 4: Chapter 7
11:40 - 13:20 Book 2: Part 4: Chapter 8
13:20 - 13:40 End Credits
However, if one of the parent chapters is longer than 10000 milliseconds, it's kept as its own
chapter. A duration longer than a few seconds implies that the chapter contains more than just
the narrator saying the chapter title, so it should probably be preserved as a separate chapter.
Using the example above, if "Book 1" was 15 seconds long and "Part 3" was 20 seconds long:
00:00 - 00:10 Opening Credits
00:10 - 00:25 Book 1
00:25 - 00:27 | Part 1
00:27 - 01:40 | | Chapter 1
01:40 - 03:20 | | Chapter 2
03:20 - 03:22 | Part 2
03:22 - 05:00 | | Chapter 3
05:00 - 06:40 | | Chapter 4
06:40 - 06:42 Book 2
06:42 - 07:02 | Part 3
07:02 - 08:20 | | Chapter 5
08:20 - 10:00 | | Chapter 6
10:00 - 10:02 | Part 4
10:02 - 11:40 | | Chapter 7
11:40 - 13:20 | | Chapter 8
13:20 - 13:30 End Credits
then flattenChapters will combine them into this:
00:00 - 00:10 Opening Credits
00:10 - 00:25 Book 1
00:25 - 01:40 Book 1: Part 1: Chapter 1
01:40 - 03:20 Book 1: Part 1: Chapter 2
03:20 - 05:00 Book 1: Part 2: Chapter 3
05:00 - 06:40 Book 1: Part 2: Chapter 4
06:40 - 07:02 Book 2: Part 3
07:02 - 08:20 Book 2: Part 3: Chapter 5
08:20 - 10:00 Book 2: Part 3: Chapter 6
10:00 - 11:40 Book 2: Part 4: Chapter 7
11:40 - 13:20 Book 2: Part 4: Chapter 8
13:20 - 13:40 End Credits
*/
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string titleConcat = ": ")
{
List<Chapter> chaps = new();
foreach (var c in chapters)
{
if (c.Chapters is null)
chaps.Add(c);
else if (titleConcat is null)
{
chaps.Add(c);
chaps.AddRange(flattenChapters(c.Chapters));
}
else
{
if (c.LengthMs < 10000)
{
c.Chapters[0].StartOffsetMs = c.StartOffsetMs;
c.Chapters[0].StartOffsetSec = c.StartOffsetSec;
c.Chapters[0].LengthMs += c.LengthMs;
}
else
chaps.Add(c);
var children = flattenChapters(c.Chapters);
foreach (var child in children)
child.Title = $"{c.Title}{titleConcat}{child.Title}";
chaps.AddRange(children);
}
}
return chaps;
}
public static void combineCredits(IList<Chapter> chapters)
{
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
{
chapters[1].StartOffsetMs = chapters[0].StartOffsetMs;
chapters[1].StartOffsetSec = chapters[0].StartOffsetSec;
chapters[1].LengthMs += chapters[0].LengthMs;
chapters.RemoveAt(0);
}
if (chapters.Count > 1 && chapters[^1].Title == "End Credits")
{
chapters[^2].LengthMs += chapters[^1].LengthMs;
chapters.Remove(chapters[^1]);
} }
} }

View File

@ -0,0 +1,362 @@
using AaxDecrypter;
using AudibleApi;
using AudibleApi.Common;
using AudibleUtilities.Widevine;
using DataLayer;
using LibationFileManager;
using NAudio.Lame;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
#nullable enable
namespace FileLiberator;
public partial class DownloadOptions
{
private const string Ec3Codec = "ec+3";
private const string Ac4Codec = "ac-4";
/// <summary>
/// Initiate an audiobook download from the audible api.
/// </summary>
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, LibraryBook libraryBook, Configuration config)
{
var license = await ChooseContent(api, libraryBook, config);
var options = BuildDownloadOptions(libraryBook, config, license);
return options;
}
private static async Task<ContentLicense> ChooseContent(Api api, LibraryBook libraryBook, Configuration config)
{
var cdm = await Cdm.GetCdmAsync();
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
ContentLicense? contentLic = null;
ContentLicense? fallback = null;
if (cdm is null)
{
//Doesn't matter what the user chose. We can't get a CDM so we must fall back to AAX(C)
contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
}
else
{
var spatial = config.FileDownloadQuality is Configuration.DownloadQuality.Spatial;
try
{
var codecChoice = config.SpatialAudioCodec switch
{
Configuration.SpatialCodec.EC_3 => Ec3Codec,
Configuration.SpatialCodec.AC_4 => Ac4Codec,
_ => throw new NotSupportedException($"Unknown value for {nameof(config.SpatialAudioCodec)}")
};
contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality, ChapterTitlesType.Tree, DrmType.Widevine, spatial, codecChoice);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to request a Widevine license.");
}
if (contentLic is null)
{
//We failed to get a widevine license, so fall back to AAX(C)
contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
}
else if (!contentLic.ContentMetadata.ContentReference.IsSpatial && contentLic.DrmType != DrmType.Adrm)
{
/*
We got a widevine license and we have a Cdm, but we still need to decide if we WANT the file
being delivered with widevine. This file is not "spatial", so it may be no better than the
audio in the Adrm files. All else being equal, we prefer Adrm files because they have more
build-in metadata and always AAC-LC, which is a codec playable by pretty much every device
in existence.
Unfortunately, there appears to be no way to determine which codec/quality combination we'll
get until we make the request and see what content gets delivered. For some books,
Widevine/High delivers 44.1 kHz / 128 kbps audio and Adrm/High delivers 22.05 kHz / 64 kbps.
In those cases, the Widevine content size is much larger. Other books will deliver the same
sample rate / bitrate for both Widevine and Adrm, the only difference being codec. Widevine
is usually xHE-AAC, but is sometimes AAC-LC. Adrm is always AAC-LC.
To decide which file we want, use this simple rule: if files are different codecs and
Widevine is significantly larger, use Widevine. Otherwise use ADRM.
*/
fallback = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
var wvCr = contentLic.ContentMetadata.ContentReference;
var adrmCr = fallback.ContentMetadata.ContentReference;
if (wvCr.Codec == adrmCr.Codec ||
adrmCr.ContentSizeInBytes > wvCr.ContentSizeInBytes ||
RelativePercentDifference(adrmCr.ContentSizeInBytes, wvCr.ContentSizeInBytes) < 0.05)
{
contentLic = fallback;
}
}
}
if (contentLic.DrmType == DrmType.Widevine && cdm is not null)
{
try
{
using var client = new HttpClient();
var mpdResponse = await client.GetAsync(contentLic.LicenseResponse);
var dash = new MpegDash(mpdResponse.Content.ReadAsStream());
if (!dash.TryGetUri(new Uri(contentLic.LicenseResponse), out var contentUri))
throw new InvalidDataException("Failed to get mpeg-dash content download url.");
contentLic.ContentMetadata.ContentUrl = new() { OfflineUrl = contentUri.ToString() };
using var session = cdm.OpenSession();
var challenge = session.GetLicenseChallenge(dash);
var licenseMessage = await api.WidevineDrmLicense(libraryBook.Book.AudibleProductId, challenge);
var keys = session.ParseLicense(licenseMessage);
contentLic.Voucher = new VoucherDtoV10()
{
Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()),
Iv = Convert.ToHexStringLower(keys[0].Key)
};
}
catch
{
if (fallback != null)
return fallback;
//We won't have a fallback if the requested license is for a spatial audio file.
//Throw so that the user is aware that spatial audio exists and that they were not able to download it.
throw;
}
}
return contentLic;
}
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic)
{
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
var outputFormat
= contentLic.DrmType is not DrmType.Adrm and not DrmType.Widevine ||
(config.AllowLibationFixup && config.DecryptToLossy && contentLic.ContentMetadata.ContentReference.Codec != "ac-4")
? OutputFormat.Mp3
: OutputFormat.M4b;
long chapterStartMs
= config.StripAudibleBrandAudio
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
: 0;
AAXClean.FileType? inputType
= contentLic.DrmType is DrmType.Widevine ? AAXClean.FileType.Dash
: contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 8 && contentLic.Voucher?.Iv == null ? AAXClean.FileType.Aax
: contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc
: null;
//Set the requested AudioFormat for use in file naming templates
libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat);
var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl)
{
AudibleKey = contentLic.Voucher?.Key,
AudibleIV = contentLic.Voucher?.Iv,
InputType = inputType,
OutputFormat = outputFormat,
DrmType = contentLic.DrmType,
ContentMetadata = contentLic.ContentMetadata,
LameConfig = outputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null,
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs),
};
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
var chapters
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
.OrderBy(c => c.StartOffsetMs)
.ToList();
if (config.MergeOpeningAndEndCredits)
combineCredits(chapters);
for (int i = 0; i < chapters.Count; i++)
{
var chapter = chapters[i];
long chapLenMs = chapter.LengthMs;
if (i == 0)
chapLenMs -= chapterStartMs;
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
}
return dlOptions;
}
public static LameConfig GetLameOptions(Configuration config)
{
LameConfig lameConfig = new()
{
Mode = MPEGMode.Mono,
Quality = config.LameEncoderQuality,
OutputSampleRate = (int)config.MaxSampleRate
};
if (config.LameTargetBitrate)
{
if (config.LameConstantBitrate)
lameConfig.BitRate = config.LameBitrate;
else
{
lameConfig.ABRRateKbps = config.LameBitrate;
lameConfig.VBR = VBRMode.ABR;
lameConfig.WriteVBRTag = true;
}
}
else
{
lameConfig.VBR = VBRMode.Default;
lameConfig.VBRQuality = config.LameVBRQuality;
lameConfig.WriteVBRTag = true;
}
return lameConfig;
}
/*
Flatten Audible's new hierarchical chapters, combining children into parents.
Audible may deliver chapters like this:
00:00 - 00:10 Opening Credits
00:10 - 00:12 Book 1
00:12 - 00:14 | Part 1
00:14 - 01:40 | | Chapter 1
01:40 - 03:20 | | Chapter 2
03:20 - 03:22 | Part 2
03:22 - 05:00 | | Chapter 3
05:00 - 06:40 | | Chapter 4
06:40 - 06:42 Book 2
06:42 - 06:44 | Part 3
06:44 - 08:20 | | Chapter 5
08:20 - 10:00 | | Chapter 6
10:00 - 10:02 | Part 4
10:02 - 11:40 | | Chapter 7
11:40 - 13:20 | | Chapter 8
13:20 - 13:30 End Credits
And flattenChapters will combine them into this:
00:00 - 00:10 Opening Credits
00:10 - 01:40 Book 1: Part 1: Chapter 1
01:40 - 03:20 Book 1: Part 1: Chapter 2
03:20 - 05:00 Book 1: Part 2: Chapter 3
05:00 - 06:40 Book 1: Part 2: Chapter 4
06:40 - 08:20 Book 2: Part 3: Chapter 5
08:20 - 10:00 Book 2: Part 3: Chapter 6
10:00 - 11:40 Book 2: Part 4: Chapter 7
11:40 - 13:20 Book 2: Part 4: Chapter 8
13:20 - 13:40 End Credits
However, if one of the parent chapters is longer than 10000 milliseconds, it's kept as its own
chapter. A duration longer than a few seconds implies that the chapter contains more than just
the narrator saying the chapter title, so it should probably be preserved as a separate chapter.
Using the example above, if "Book 1" was 15 seconds long and "Part 3" was 20 seconds long:
00:00 - 00:10 Opening Credits
00:10 - 00:25 Book 1
00:25 - 00:27 | Part 1
00:27 - 01:40 | | Chapter 1
01:40 - 03:20 | | Chapter 2
03:20 - 03:22 | Part 2
03:22 - 05:00 | | Chapter 3
05:00 - 06:40 | | Chapter 4
06:40 - 06:42 Book 2
06:42 - 07:02 | Part 3
07:02 - 08:20 | | Chapter 5
08:20 - 10:00 | | Chapter 6
10:00 - 10:02 | Part 4
10:02 - 11:40 | | Chapter 7
11:40 - 13:20 | | Chapter 8
13:20 - 13:30 End Credits
then flattenChapters will combine them into this:
00:00 - 00:10 Opening Credits
00:10 - 00:25 Book 1
00:25 - 01:40 Book 1: Part 1: Chapter 1
01:40 - 03:20 Book 1: Part 1: Chapter 2
03:20 - 05:00 Book 1: Part 2: Chapter 3
05:00 - 06:40 Book 1: Part 2: Chapter 4
06:40 - 07:02 Book 2: Part 3
07:02 - 08:20 Book 2: Part 3: Chapter 5
08:20 - 10:00 Book 2: Part 3: Chapter 6
10:00 - 11:40 Book 2: Part 4: Chapter 7
11:40 - 13:20 Book 2: Part 4: Chapter 8
13:20 - 13:40 End Credits
*/
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string? titleConcat = ": ")
{
List<Chapter> chaps = new();
foreach (var c in chapters)
{
if (c.Chapters is null)
chaps.Add(c);
else if (titleConcat is null)
{
chaps.Add(c);
chaps.AddRange(flattenChapters(c.Chapters));
}
else
{
if (c.LengthMs < 10000)
{
c.Chapters[0].StartOffsetMs = c.StartOffsetMs;
c.Chapters[0].StartOffsetSec = c.StartOffsetSec;
c.Chapters[0].LengthMs += c.LengthMs;
}
else
chaps.Add(c);
var children = flattenChapters(c.Chapters);
foreach (var child in children)
child.Title = $"{c.Title}{titleConcat}{child.Title}";
chaps.AddRange(children);
}
}
return chaps;
}
public static void combineCredits(IList<Chapter> chapters)
{
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
{
chapters[1].StartOffsetMs = chapters[0].StartOffsetMs;
chapters[1].StartOffsetSec = chapters[0].StartOffsetSec;
chapters[1].LengthMs += chapters[0].LengthMs;
chapters.RemoveAt(0);
}
if (chapters.Count > 1 && chapters[^1].Title == "End Credits")
{
chapters[^2].LengthMs += chapters[^1].LengthMs;
chapters.Remove(chapters[^1]);
}
}
static double RelativePercentDifference(long num1, long num2)
=> Math.Abs(num1 - num2) / (double)(num1 + num2);
}

View File

@ -7,10 +7,11 @@ using System.Threading.Tasks;
using System; using System;
using System.IO; using System.IO;
using ApplicationServices; using ApplicationServices;
using LibationFileManager.Templates;
namespace FileLiberator namespace FileLiberator
{ {
public class DownloadOptions : IDownloadOptions, IDisposable public partial class DownloadOptions : IDownloadOptions, IDisposable
{ {
public event EventHandler<long> DownloadSpeedChanged; public event EventHandler<long> DownloadSpeedChanged;
public LibraryBook LibraryBook { get; } public LibraryBook LibraryBook { get; }
@ -26,8 +27,8 @@ namespace FileLiberator
public string Publisher => LibraryBook.Book.Publisher; public string Publisher => LibraryBook.Book.Publisher;
public string Language => LibraryBook.Book.Language; public string Language => LibraryBook.Book.Language;
public string AudibleProductId => LibraryBookDto.AudibleProductId; public string AudibleProductId => LibraryBookDto.AudibleProductId;
public string SeriesName => LibraryBookDto.SeriesName; public string SeriesName => LibraryBookDto.FirstSeries?.Name;
public float? SeriesNumber => LibraryBookDto.SeriesNumber; public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent; public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio; public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
@ -40,6 +41,9 @@ namespace FileLiberator
public bool Downsample => config.AllowLibationFixup && config.LameDownsampleMono; public bool Downsample => config.AllowLibationFixup && config.LameDownsampleMono;
public bool MatchSourceBitrate => config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate; public bool MatchSourceBitrate => config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate;
public bool MoveMoovToBeginning => config.MoveMoovToBeginning; public bool MoveMoovToBeginning => config.MoveMoovToBeginning;
public AAXClean.FileType? InputType { get; init; }
public AudibleApi.Common.DrmType DrmType { get; init; }
public AudibleApi.Common.ContentMetadata ContentMetadata { get; init; }
public string GetMultipartFileName(MultiConvertFileProperties props) public string GetMultipartFileName(MultiConvertFileProperties props)
{ {
@ -82,9 +86,13 @@ namespace FileLiberator
private readonly Configuration config; private readonly Configuration config;
private readonly IDisposable cancellation; private readonly IDisposable cancellation;
public void Dispose() => cancellation?.Dispose(); public void Dispose()
{
cancellation?.Dispose();
GC.SuppressFinalize(this);
}
public DownloadOptions(Configuration config, LibraryBook libraryBook, string downloadUrl) private DownloadOptions(Configuration config, LibraryBook libraryBook, [System.Diagnostics.CodeAnalysis.NotNull] string downloadUrl)
{ {
this.config = ArgumentValidator.EnsureNotNull(config, nameof(config)); this.config = ArgumentValidator.EnsureNotNull(config, nameof(config));
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));

View File

@ -19,5 +19,10 @@
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Update="DownloadOptions.*.cs">
<DependentUpon>DownloadOptions.cs</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>

View File

@ -5,8 +5,9 @@ using System.Threading.Tasks;
using AudibleUtilities; using AudibleUtilities;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager.Templates;
#nullable enable
namespace FileLiberator namespace FileLiberator
{ {
public static class UtilityExtensions public static class UtilityExtensions
@ -47,12 +48,10 @@ namespace FileLiberator
YearPublished = libraryBook.Book.DatePublished?.Year, YearPublished = libraryBook.Book.DatePublished?.Year,
DatePublished = libraryBook.Book.DatePublished, DatePublished = libraryBook.Book.DatePublished,
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(), Authors = libraryBook.Book.Authors.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
Narrators = libraryBook.Book.Narrators.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(),
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(), Series = getSeries(libraryBook.Book.SeriesLink),
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Index,
IsPodcastParent = libraryBook.Book.IsEpisodeParent(), IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(), IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
@ -62,5 +61,21 @@ namespace FileLiberator
Language = libraryBook.Book.Language Language = libraryBook.Book.Language
}; };
} }
private static List<SeriesDto>? getSeries(IEnumerable<SeriesBook> seriesBooks)
{
if (!seriesBooks.Any())
return null;
//I don't remember why or if there was a good reason not to have series numbers for
//podcast parents, but preserving the behavior for backwards compatibility.
return seriesBooks
.Select(sb
=> new SeriesDto(
sb.Series.Name,
sb.Book.IsEpisodeParent() ? null : sb.Index,
sb.Series.AudibleSeriesId)
).ToList();
}
} }
} }

View File

@ -71,13 +71,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.5" /> <PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" /> <PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.2.8" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" /> <ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@ -43,10 +43,26 @@
<controls:WheelComboBox <controls:WheelComboBox
Margin="5,0,0,0" Margin="5,0,0,0"
Grid.Column="1" Grid.Column="1"
SelectionChanged="Quality_SelectionChanged"
ItemsSource="{CompiledBinding DownloadQualities}" ItemsSource="{CompiledBinding DownloadQualities}"
SelectedItem="{CompiledBinding FileDownloadQuality}"/> SelectedItem="{CompiledBinding FileDownloadQuality}"/>
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto" Margin="0,5,0,0"
IsEnabled="{CompiledBinding SpatialSelected}"
ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
<TextBlock
VerticalAlignment="Center"
Text="{CompiledBinding SpatialAudioCodecText}" />
<controls:WheelComboBox
Margin="5,0,0,0"
Grid.Column="1"
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
</Grid>
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}"> <CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CreateCueSheetText}" /> <TextBlock Text="{CompiledBinding CreateCueSheetText}" />
</CheckBox> </CheckBox>

View File

@ -1,7 +1,10 @@
using AudibleUtilities;
using Avalonia.Controls; using Avalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Controls.Settings namespace LibationAvalonia.Controls.Settings
@ -19,6 +22,25 @@ namespace LibationAvalonia.Controls.Settings
} }
} }
public async void Quality_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_viewModel.SpatialSelected)
{
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{
await MessageBox.Show(VisualRoot as Window,
"Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.",
"Spatial Audio Unavailable",
MessageBoxButtons.OK);
_viewModel.FileDownloadQuality = _viewModel.DownloadQualities[1];
}
}
}
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (_viewModel is null) return; if (_viewModel is null) return;

View File

@ -2,6 +2,7 @@ using Avalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Controls.Settings namespace LibationAvalonia.Controls.Settings

View File

@ -2,9 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="520" mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="540"
MinWidth="450" MinHeight="520" MinWidth="450" MinHeight="540"
Width="450" Height="520" Width="450" Height="540"
x:Class="LibationAvalonia.Dialogs.AboutDialog" x:Class="LibationAvalonia.Dialogs.AboutDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls" xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="About Libation"> Title="About Libation">
@ -41,39 +41,38 @@
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2"> <controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
<StackPanel> <StackPanel>
<StackPanel.Styles>
<Style Selector="controls|LinkLabel">
<Setter Property="Margin" Value="5,0" />
<Setter Property="FontSize" Value="13" />
</Style>
</StackPanel.Styles>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto"> <ItemsControl ItemsSource="{Binding PrimaryContributors}">
<controls:LinkLabel FontWeight="Bold" Text="rmcrackan" Tapped="Link_GithubUser" /> <ItemsControl.ItemTemplate>
<TextBlock Grid.Column="1" Margin="10,0" Text="Creator" /> <DataTemplate>
<controls:LinkLabel Grid.Row="1" FontWeight="Bold" Text="Mbucari" Tapped="Link_GithubUser" /> <StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Grid.Column="1" Margin="10,0" Text="Developer" /> <controls:LinkLabel FontWeight="Bold" Text="{Binding Name}" Tapped="ContributorLink_Tapped" />
</Grid> <TextBlock Grid.Column="1" Margin="10,0" Text="{Binding Type}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/> <TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
<WrapPanel> <ItemsControl ItemsSource="{Binding AdditionalContributors}">
<WrapPanel.Styles> <ItemsControl.ItemsPanel>
<Style Selector="controls|LinkLabel"> <ItemsPanelTemplate>
<Setter Property="Margin" Value="5,0" /> <WrapPanel />
<Setter Property="FontSize" Value="13" /> </ItemsPanelTemplate>
</Style> </ItemsControl.ItemsPanel>
</WrapPanel.Styles> <ItemsControl.ItemTemplate>
<controls:LinkLabel Text="pixil98" Tapped="Link_GithubUser" /> <DataTemplate>
<controls:LinkLabel Text="hutattedonmyarm" Tapped="Link_GithubUser" /> <controls:LinkLabel Text="{Binding Name}" Tapped="ContributorLink_Tapped" />
<controls:LinkLabel Text="seanke" Tapped="Link_GithubUser" /> </DataTemplate>
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" /> </ItemsControl.ItemTemplate>
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" /> </ItemsControl>
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="cbordeman" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="jwillikers" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="Shuvashish76" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="RokeJulianLockhart" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="maaximal" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="muchtall" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="ScubyG" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="patienttruth" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="stickystyle" Tapped="Link_GithubUser" />
</WrapPanel>
</StackPanel> </StackPanel>
</controls:GroupBox> </controls:GroupBox>

View File

@ -5,6 +5,7 @@ using LibationFileManager;
using LibationUiBase; using LibationUiBase;
using ReactiveUI; using ReactiveUI;
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
@ -48,11 +49,11 @@ namespace LibationAvalonia.Dialogs
} }
} }
private void Link_GithubUser(object sender, Avalonia.Input.TappedEventArgs e) private void ContributorLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{ {
if (sender is LinkLabel lbl) if (sender is LinkLabel lbl && lbl.DataContext is LibationContributor contributor)
{ {
Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.','-')}"); Dinah.Core.Go.To.Url(contributor.Link.AbsoluteUri);
} }
} }
@ -72,6 +73,9 @@ namespace LibationAvalonia.Dialogs
private bool canCheckForUpgrade = true; private bool canCheckForUpgrade = true;
private string upgradeButtonText = "Check for Upgrade"; private string upgradeButtonText = "Check for Upgrade";
public IEnumerable<LibationContributor> PrimaryContributors => LibationContributor.PrimaryContributors;
public IEnumerable<LibationContributor> AdditionalContributors => LibationContributor.AdditionalContributors;
public AboutVM() public AboutVM()
{ {
Version = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}"; Version = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";

View File

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using ReactiveUI; using ReactiveUI;
using System; using System;
using System.IO; using System.IO;

View File

@ -73,14 +73,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.5" /> <PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.8" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.2.8" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.2.5" /> <PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" /> <PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.2.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -22,13 +22,13 @@ namespace LibationAvalonia.ViewModels.Settings
private int _lameBitrate; private int _lameBitrate;
private int _lameVBRQuality; private int _lameVBRQuality;
private string _chapterTitleTemplate; private string _chapterTitleTemplate;
public EnumDiaplay<SampleRate> SelectedSampleRate { get; set; } public EnumDisplay<SampleRate> SelectedSampleRate { get; set; }
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; } public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; } public AvaloniaList<EnumDisplay<SampleRate>> SampleRates { get; }
= new(Enum.GetValues<SampleRate>() = new(Enum.GetValues<SampleRate>()
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000) .Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
.Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz"))); .Select(v => new EnumDisplay<SampleRate>(v, $"{(int)v} Hz")));
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; } public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
= new( = new(
@ -48,7 +48,6 @@ namespace LibationAvalonia.ViewModels.Settings
DownloadCoverArt = config.DownloadCoverArt; DownloadCoverArt = config.DownloadCoverArt;
RetainAaxFile = config.RetainAaxFile; RetainAaxFile = config.RetainAaxFile;
DownloadClipsBookmarks = config.DownloadClipsBookmarks; DownloadClipsBookmarks = config.DownloadClipsBookmarks;
FileDownloadQuality = config.FileDownloadQuality;
ClipBookmarkFormat = config.ClipsBookmarksFileFormat; ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
SplitFilesByChapter = config.SplitFilesByChapter; SplitFilesByChapter = config.SplitFilesByChapter;
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits; MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
@ -64,6 +63,8 @@ namespace LibationAvalonia.ViewModels.Settings
LameBitrate = config.LameBitrate; LameBitrate = config.LameBitrate;
LameVBRQuality = config.LameVBRQuality; LameVBRQuality = config.LameVBRQuality;
SpatialAudioCodec = SpatialAudioCodecs.SingleOrDefault(s => s.Value == config.SpatialAudioCodec) ?? SpatialAudioCodecs[0];
FileDownloadQuality = DownloadQualities.SingleOrDefault(s => s.Value == config.FileDownloadQuality) ?? DownloadQualities[0];
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0]; SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
SelectedEncoderQuality = config.LameEncoderQuality; SelectedEncoderQuality = config.LameEncoderQuality;
} }
@ -76,7 +77,6 @@ namespace LibationAvalonia.ViewModels.Settings
config.DownloadCoverArt = DownloadCoverArt; config.DownloadCoverArt = DownloadCoverArt;
config.RetainAaxFile = RetainAaxFile; config.RetainAaxFile = RetainAaxFile;
config.DownloadClipsBookmarks = DownloadClipsBookmarks; config.DownloadClipsBookmarks = DownloadClipsBookmarks;
config.FileDownloadQuality = FileDownloadQuality;
config.ClipsBookmarksFileFormat = ClipBookmarkFormat; config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
config.SplitFilesByChapter = SplitFilesByChapter; config.SplitFilesByChapter = SplitFilesByChapter;
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits; config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
@ -94,11 +94,23 @@ namespace LibationAvalonia.ViewModels.Settings
config.LameEncoderQuality = SelectedEncoderQuality; config.LameEncoderQuality = SelectedEncoderQuality;
config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate; config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate;
config.FileDownloadQuality = FileDownloadQuality?.Value ?? config.FileDownloadQuality;
config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec;
} }
public AvaloniaList<Configuration.DownloadQuality> DownloadQualities { get; } = new(Enum<Configuration.DownloadQuality>.GetValues()); public AvaloniaList<EnumDisplay<Configuration.DownloadQuality>> DownloadQualities { get; } = new([
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"),
]);
public AvaloniaList<EnumDisplay<Configuration.SpatialCodec>> SpatialAudioCodecs { get; } = new([
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"),
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.AC_4, "Dolby AC-4")
]);
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues()); public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality)); public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
public string SpatialAudioCodecText { get; } = Configuration.GetDescription(nameof(Configuration.SpatialAudioCodec));
public string SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles)); public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles));
public string CombineNestedChapterTitlesTip => Configuration.GetHelpText(nameof(CombineNestedChapterTitles)); public string CombineNestedChapterTitlesTip => Configuration.GetHelpText(nameof(CombineNestedChapterTitles));
@ -120,7 +132,21 @@ namespace LibationAvalonia.ViewModels.Settings
public bool RetainAaxFile { get; set; } public bool RetainAaxFile { get; set; }
public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile)); public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); } public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
public bool SpatialSelected { get; private set; }
private EnumDisplay<Configuration.DownloadQuality>? _fileDownloadQuality;
public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality
{
get => _fileDownloadQuality ?? DownloadQualities[0];
set
{
SpatialSelected = value?.Value == Configuration.DownloadQuality.Spatial;
this.RaiseAndSetIfChanged(ref _fileDownloadQuality, value);
this.RaisePropertyChanged(nameof(SpatialSelected));
}
}
public EnumDisplay<Configuration.SpatialCodec> SpatialAudioCodec { get; set; }
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; } public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
public bool MergeOpeningAndEndCredits { get; set; } public bool MergeOpeningAndEndCredits { get; set; }
public string MergeOpeningAndEndCreditsTip => Configuration.GetHelpText(nameof(MergeOpeningAndEndCredits)); public string MergeOpeningAndEndCreditsTip => Configuration.GetHelpText(nameof(MergeOpeningAndEndCredits));

View File

@ -72,9 +72,9 @@ namespace LibationAvalonia.ViewModels.Settings
public string OverwriteExistingText { get; } = Configuration.GetDescription(nameof(Configuration.OverwriteExisting)); public string OverwriteExistingText { get; } = Configuration.GetDescription(nameof(Configuration.OverwriteExisting));
public string CreationTimeText { get; } = Configuration.GetDescription(nameof(Configuration.CreationTime)); public string CreationTimeText { get; } = Configuration.GetDescription(nameof(Configuration.CreationTime));
public string LastWriteTimeText { get; } = Configuration.GetDescription(nameof(Configuration.LastWriteTime)); public string LastWriteTimeText { get; } = Configuration.GetDescription(nameof(Configuration.LastWriteTime));
public EnumDiaplay<Configuration.DateTimeSource>[] DateTimeSources { get; } public EnumDisplay<Configuration.DateTimeSource>[] DateTimeSources { get; }
= Enum.GetValues<Configuration.DateTimeSource>() = Enum.GetValues<Configuration.DateTimeSource>()
.Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v)) .Select(v => new EnumDisplay<Configuration.DateTimeSource>(v))
.ToArray(); .ToArray();
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>(); public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor)); public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
@ -87,8 +87,8 @@ namespace LibationAvalonia.ViewModels.Settings
public bool OverwriteExisting { get; set; } public bool OverwriteExisting { get; set; }
public float GridScaleFactor { get; set; } public float GridScaleFactor { get; set; }
public float GridFontScaleFactor { get; set; } public float GridFontScaleFactor { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> CreationTime { get; set; } public EnumDisplay<Configuration.DateTimeSource> CreationTime { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> LastWriteTime { get; set; } public EnumDisplay<Configuration.DateTimeSource> LastWriteTime { get; set; }
public Serilog.Events.LogEventLevel LoggingLevel { get; set; } public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
public string ThemeVariant public string ThemeVariant

View File

@ -12,6 +12,7 @@ using LibationAvalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels; using LibationAvalonia.ViewModels;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using ReactiveUI; using ReactiveUI;
using System; using System;
@ -350,7 +351,7 @@ namespace LibationAvalonia.Views
#region Edit Templates (Single book only) #region Edit Templates (Single book only)
async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new() where T : Templates, LibationFileManager.Templates.ITemplate, new()
{ {
var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate); var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template); var form = new EditTemplateDialog(template);

View File

@ -82,6 +82,13 @@ namespace LibationFileManager
from the decrypted audiobook. This does not require from the decrypted audiobook. This does not require
re-encoding. re-encoding.
""" }, """ },
{nameof(SpatialAudioCodec), """
The Dolby Digital Plus (E-AC-3) codec is more widely
supported than the AC-4 codec, but E-AC-3 files are
much larger than AC-4 files.
AC-4 cannot be converted to MP3.
""" },
} }
.AsReadOnly(); .AsReadOnly();

View File

@ -246,9 +246,20 @@ namespace LibationFileManager
public enum DownloadQuality public enum DownloadQuality
{ {
High, High,
Normal Normal,
Spatial
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum SpatialCodec
{
EC_3,
AC_4
}
[Description("Spatial audio codec:")]
public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); }
[Description("Audio quality to request from Audible:")] [Description("Audio quality to request from Audible:")]
public DownloadQuality FileDownloadQuality { get => GetNonString(defaultValue: DownloadQuality.High); set => SetNonString(value); } public DownloadQuality FileDownloadQuality { get => GetNonString(defaultValue: DownloadQuality.High); set => SetNonString(value); }
@ -306,41 +317,41 @@ namespace LibationFileManager
[Description("How to format the folders in which files will be saved")] [Description("How to format the folders in which files will be saved")]
public string FolderTemplate public string FolderTemplate
{ {
get => getTemplate<Templates.FolderTemplate>(); get => getTemplate<Templates.Templates.FolderTemplate>();
set => setTemplate<Templates.FolderTemplate>(value); set => setTemplate<Templates.Templates.FolderTemplate>(value);
} }
[Description("How to format the saved pdf and audio files")] [Description("How to format the saved pdf and audio files")]
public string FileTemplate public string FileTemplate
{ {
get => getTemplate<Templates.FileTemplate>(); get => getTemplate<Templates.Templates.FileTemplate>();
set => setTemplate<Templates.FileTemplate>(value); set => setTemplate<Templates.Templates.FileTemplate>(value);
} }
[Description("How to format the saved audio files when split by chapters")] [Description("How to format the saved audio files when split by chapters")]
public string ChapterFileTemplate public string ChapterFileTemplate
{ {
get => getTemplate<Templates.ChapterFileTemplate>(); get => getTemplate<Templates.Templates.ChapterFileTemplate>();
set => setTemplate<Templates.ChapterFileTemplate>(value); set => setTemplate<Templates.Templates.ChapterFileTemplate>(value);
} }
[Description("How to format the file's Title stored in metadata")] [Description("How to format the file's Title stored in metadata")]
public string ChapterTitleTemplate public string ChapterTitleTemplate
{ {
get => getTemplate<Templates.ChapterTitleTemplate>(); get => getTemplate<Templates.Templates.ChapterTitleTemplate>();
set => setTemplate<Templates.ChapterTitleTemplate>(value); set => setTemplate<Templates.Templates.ChapterTitleTemplate>(value);
} }
private string getTemplate<T>([CallerMemberName] string propertyName = "") private string getTemplate<T>([CallerMemberName] string propertyName = "")
where T : Templates, ITemplate, new() where T : Templates.Templates, Templates.ITemplate, new()
{ {
return Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText; return Templates.Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText;
} }
private void setTemplate<T>(string newValue, [CallerMemberName] string propertyName = "") private void setTemplate<T>(string newValue, [CallerMemberName] string propertyName = "")
where T : Templates, ITemplate, new() where T : Templates.Templates, Templates.ITemplate, new()
{ {
SetString(Templates.GetTemplate<T>(newValue).TemplateText, propertyName); SetString(Templates.Templates.GetTemplate<T>(newValue).TemplateText, propertyName);
} }
#endregion #endregion
} }

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageReference Include="NameParserSharp" Version="1.5.0" /> <PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationFileManager
{
public class BookDto
{
public string? AudibleProductId { get; set; }
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? TitleWithSubtitle { get; set; }
public string? Locale { get; set; }
public int? YearPublished { get; set; }
public IEnumerable<string>? Authors { get; set; }
public string? AuthorNames => Authors is null ? null : string.Join(", ", Authors);
public string? FirstAuthor => Authors?.FirstOrDefault();
public IEnumerable<string>? Narrators { get; set; }
public string? NarratorNames => Narrators is null? null: string.Join(", ", Narrators);
public string? FirstNarrator => Narrators?.FirstOrDefault();
public string? SeriesName { get; set; }
public float? SeriesNumber { get; set; }
public bool IsSeries => !string.IsNullOrEmpty(SeriesName);
public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; }
public int BitRate { get; set; }
public int SampleRate { get; set; }
public int Channels { get; set; }
public DateTime FileDate { get; set; } = DateTime.Now;
public DateTime? DatePublished { get; set; }
public string? Language { get; set; }
}
public class LibraryBookDto : BookDto
{
public DateTime? DateAdded { get; set; }
public string? Account { get; set; }
public string? AccountNickname { get; set; }
}
}

View File

@ -1,98 +0,0 @@
using FileManager.NamingTemplate;
using NameParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager
{
internal partial class NameListFormat
{
public static string Formatter(ITemplateTag _, IEnumerable<string>? names, string formatString)
{
if (names is null) return "";
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
var sortedNames = Sort(humanNames, formatString);
var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
var separatorString = Separator(formatString, defaultValue: ", ");
var maxNames = Max(formatString, defaultValue: humanNames.Count());
var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => FormatName(n, nameFormatString)));
while (formattedNames.Contains(" "))
formattedNames = formattedNames.Replace(" ", " ");
return formattedNames;
}
private static string RemoveSuffix(string namesString)
{
namesString = namesString.Replace('', '\'').Replace(" - Ret.", ", Ret.");
int dashIndex = namesString.IndexOf(" - ");
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
}
private static IEnumerable<HumanName> Sort(IEnumerable<HumanName> humanNames, string formatString)
{
var sortMatch = SortRegex().Match(formatString);
return
sortMatch.Success
? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First)
: sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle)
: sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last)
: humanNames
: humanNames;
}
private static string Format(string formatString, string defaultValue)
{
var formatMatch = FormatRegex().Match(formatString);
return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue;
}
private static string Separator(string formatString, string defaultValue)
{
var separatorMatch = SeparatorRegex().Match(formatString);
return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue;
}
private static int Max(string formatString, int defaultValue)
{
var maxMatch = MaxRegex().Match(formatString);
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue;
}
private static string FormatName(HumanName humanName, string nameFormatString)
{
//Single-word names parse as first names. Use it as last name.
var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last;
nameFormatString
= nameFormatString
.Replace("{T}", "{0}")
.Replace("{F}", "{1}")
.Replace("{M}", "{2}")
.Replace("{L}", "{3}")
.Replace("{S}", "{4}");
return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim();
}
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
private static partial Regex SortRegex();
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")]
private static partial Regex FormatRegex();
/// <summary> Separator can be anything </summary>
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
private static partial Regex SeparatorRegex();
/// <summary> Max must have a 1 or 2-digit number </summary>
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
private static partial Regex MaxRegex();
}
}

View File

@ -0,0 +1,44 @@
using NameParser;
using System;
#nullable enable
namespace LibationFileManager.Templates;
public class ContributorDto : IFormattable
{
public HumanName HumanName { get; }
public string? AudibleContributorId { get; }
public ContributorDto(string name, string? audibleContributorId)
{
HumanName = new HumanName(RemoveSuffix(name), Prefer.FirstOverPrefix);
AudibleContributorId = audibleContributorId;
}
public override string ToString()
=> ToString("{T} {F} {M} {L} {S}", null);
public string ToString(string? format, IFormatProvider? _)
{
if (string.IsNullOrWhiteSpace(format))
return ToString();
//Single-word names parse as first names. Use it as last name.
var lastName = string.IsNullOrWhiteSpace(HumanName.Last) ? HumanName.First : HumanName.Last;
return format
.Replace("{T}", HumanName.Title)
.Replace("{F}", HumanName.First)
.Replace("{M}", HumanName.Middle)
.Replace("{L}", lastName)
.Replace("{S}", HumanName.Suffix)
.Replace("{ID}", AudibleContributorId)
.Trim();
}
private static string RemoveSuffix(string namesString)
{
namesString = namesString.Replace('', '\'').Replace(" - Ret.", ", Ret.");
int dashIndex = namesString.IndexOf(" - ");
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager.Templates;
internal partial interface IListFormat<TList> where TList : IListFormat<TList>
{
static string Join<T>(string formatString, IEnumerable<T> items)
where T : IFormattable
{
var itemFormatter = Formatter(formatString);
var separatorString = Separator(formatString) ?? ", ";
var maxValues = Max(formatString) ?? items.Count();
var formattedValues = string.Join(separatorString, items.Take(maxValues).Select(n => n.ToString(itemFormatter, null)));
while (formattedValues.Contains(" "))
formattedValues = formattedValues.Replace(" ", " ");
return formattedValues;
static string? Formatter(string formatString)
{
var formatMatch = TList.FormatRegex().Match(formatString);
return formatMatch.Success ? formatMatch.Groups[1].Value : null;
}
static int? Max(string formatString)
{
var maxMatch = MaxRegex().Match(formatString);
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : null;
}
static string? Separator(string formatString)
{
var separatorMatch = SeparatorRegex().Match(formatString);
return separatorMatch.Success ? separatorMatch.Groups[1].Value : ", ";
}
}
static abstract Regex FormatRegex();
/// <summary> Separator can be anything </summary>
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
private static partial Regex SeparatorRegex();
/// <summary> Max must have a 1 or 2-digit number </summary>
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
private static partial Regex MaxRegex();
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationFileManager.Templates;
public class BookDto
{
public string? AudibleProductId { get; set; }
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? TitleWithSubtitle { get; set; }
public string? Locale { get; set; }
public int? YearPublished { get; set; }
public IEnumerable<ContributorDto>? Authors { get; set; }
public ContributorDto? FirstAuthor => Authors?.FirstOrDefault();
public IEnumerable<ContributorDto>? Narrators { get; set; }
public ContributorDto? FirstNarrator => Narrators?.FirstOrDefault();
public IEnumerable<SeriesDto>? Series { get; set; }
public SeriesDto? FirstSeries => Series?.FirstOrDefault();
public bool IsSeries => Series is not null;
public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; }
public int BitRate { get; set; }
public int SampleRate { get; set; }
public int Channels { get; set; }
public DateTime FileDate { get; set; } = DateTime.Now;
public DateTime? DatePublished { get; set; }
public string? Language { get; set; }
}
public class LibraryBookDto : BookDto
{
public DateTime? DateAdded { get; set; }
public string? Account { get; set; }
public string? AccountNickname { get; set; }
}

View File

@ -0,0 +1,33 @@
using FileManager.NamingTemplate;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager.Templates;
internal partial class NameListFormat : IListFormat<NameListFormat>
{
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString)
=> names is null ? string.Empty
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString));
private static IEnumerable<ContributorDto> Sort(IEnumerable<ContributorDto> names, string formatString)
{
var sortMatch = SortRegex().Match(formatString);
return
sortMatch.Success
? sortMatch.Groups[1].Value == "F" ? names.OrderBy(n => n.HumanName.First)
: sortMatch.Groups[1].Value == "M" ? names.OrderBy(n => n.HumanName.Middle)
: sortMatch.Groups[1].Value == "L" ? names.OrderBy(n => n.HumanName.Last)
: names
: names;
}
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
private static partial Regex SortRegex();
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]}|{ID})+.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@ -0,0 +1,27 @@
using System;
#nullable enable
namespace LibationFileManager.Templates;
public record SeriesDto : IFormattable
{
public string Name { get; }
public float? Number { get; }
public string AudibleSeriesId { get; }
public SeriesDto(string name, float? number, string audibleSeriesId)
{
Name = name;
Number = number;
AudibleSeriesId = audibleSeriesId;
}
public override string ToString() => Name.Trim();
public string ToString(string? format, IFormatProvider? _)
=> string.IsNullOrWhiteSpace(format) ? ToString()
: format
.Replace("{N}", Name)
.Replace("{#}", Number?.ToString())
.Replace("{ID}", AudibleSeriesId)
.Trim();
}

View File

@ -0,0 +1,17 @@
using FileManager.NamingTemplate;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager.Templates;
internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
{
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString)
=> series is null ? string.Empty
: IListFormat<SeriesListFormat>.Join(formatString, series);
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[N#]}|{ID})+.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@ -1,11 +1,10 @@
using AaxDecrypter; using AaxDecrypter;
using FileManager; using FileManager;
using System.Collections.Generic;
using System; using System;
using System.IO; using System.IO;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public interface ITemplateEditor public interface ITemplateEditor
{ {
@ -61,16 +60,15 @@ namespace LibationFileManager
AccountNickname = "my account", AccountNickname = "my account",
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
AudibleProductId = "123456789", AudibleProductId = "B06WLMWF2S",
Title = "A Study in Scarlet", Title = "A Study in Scarlet",
TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel", TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel",
Subtitle = "A Sherlock Holmes Novel", Subtitle = "A Sherlock Holmes Novel",
Locale = "us", Locale = "us",
YearPublished = 2017, YearPublished = 2017,
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
Narrators = new List<string> { "Stephen Fry" }, Narrators = [new("Stephen Fry", null)],
SeriesName = "Sherlock Holmes", Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")],
SeriesNumber = 1,
BitRate = 128, BitRate = 128,
SampleRate = 44100, SampleRate = 44100,
Channels = 2, Channels = 2,
@ -131,7 +129,7 @@ namespace LibationFileManager
if (!templateEditor.IsFolder && !templateEditor.IsFilePath) if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates"); throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
if (templateEditor.IsFolder) if (templateEditor.IsFolder)
templateEditor.File = Templates.File; templateEditor.File = Templates.File;
else else

View File

@ -1,7 +1,7 @@
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public sealed class TemplateTags : ITemplateTag public sealed class TemplateTags : ITemplateTag
{ {
@ -33,15 +33,15 @@ namespace LibationFileManager
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author"); public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)"); public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator"); public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series"); public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)");
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4" public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series");
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series"); public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>");
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate"); public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate");
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate"); public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels"); public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book"); public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book"); public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
public static TemplateTags Locale { get; } = new ("locale", "Region/country"); public static TemplateTags Locale { get; } = new("locale", "Region/country");
public static TemplateTags YearPublished { get; } = new("year", "Year published"); public static TemplateTags YearPublished { get; } = new("year", "Year published");
public static TemplateTags Language { get; } = new("language", "Book's language"); public static TemplateTags Language { get; } = new("language", "Book's language");
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG"); public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");

View File

@ -10,7 +10,7 @@ using FileManager.NamingTemplate;
using NameParser; using NameParser;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public interface ITemplate public interface ITemplate
{ {
@ -58,19 +58,19 @@ namespace LibationFileManager
{ {
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))] [PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
(_,e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string); (_, e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FileTemplate))] [PropertyChangeFilter(nameof(Configuration.FileTemplate))]
(_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string); (_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))] [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string); (_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))] [PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string); (_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string);
HumanName.Suffixes.Add("ret"); HumanName.Suffixes.Add("ret");
HumanName.Titles.Add("professor"); HumanName.Titles.Add("professor");
@ -121,7 +121,7 @@ namespace LibationFileManager
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
replacements ??= Configuration.Instance.ReplacementCharacters; replacements ??= Configuration.Instance.ReplacementCharacters;
return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto); return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto);
} }
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
@ -154,7 +154,7 @@ namespace LibationFileManager
//If file already exists, GetValidFilename will append " (n)" to the filename. //If file already exists, GetValidFilename will append " (n)" to the filename.
//This could cause the filename length to exceed MaxFilenameLength, so reduce //This could cause the filename length to exceed MaxFilenameLength, so reduce
//allowable filename length by 5 chars, allowing for up to 99 duplicates. //allowable filename length by 5 chars, allowing for up to 99 duplicates.
var maxFilenameLength = LongPath.MaxFilenameLength - var maxFilenameLength = LongPath.MaxFilenameLength -
(i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5); (i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5);
while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength) while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength)
@ -170,7 +170,7 @@ namespace LibationFileManager
var fullPath = Path.Combine(pathParts.Select(fileParts => string.Concat(fileParts)).Prepend(baseDir).ToArray()); var fullPath = Path.Combine(pathParts.Select(fileParts => string.Concat(fileParts)).Prepend(baseDir).ToArray());
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
} }
/// <summary> /// <summary>
@ -186,7 +186,7 @@ namespace LibationFileManager
foreach (var part in templateParts) foreach (var part in templateParts)
{ {
int slashIndex, lastIndex = 0; int slashIndex, lastIndex = 0;
while((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1) while ((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1)
{ {
dir.Add(part[lastIndex..slashIndex]); dir.Add(part[lastIndex..slashIndex]);
RemoveSpaces(dir); RemoveSpaces(dir);
@ -229,7 +229,7 @@ namespace LibationFileManager
{ {
original = parts[i]; original = parts[i];
parts[i] = original.Replace(" ", " "); parts[i] = original.Replace(" ", " ");
}while(original.Length != parts[i].Length); } while (original.Length != parts[i].Length);
} }
//Remove instances of double spaces at part boundaries //Remove instances of double spaces at part boundaries
@ -262,11 +262,12 @@ namespace LibationFileManager
{ TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleTitle, lb => lb.Title },
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter }, { TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter },
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor }, { TemplateTags.FirstAuthor, lb => lb.FirstAuthor, FormattableFormatter },
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter }, { TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator }, { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter },
{ TemplateTags.Series, lb => lb.SeriesName }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
{ TemplateTags.SeriesNumber, lb => lb.IsPodcastParent ? null : lb.SeriesNumber }, { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter },
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Number },
{ TemplateTags.Language, lb => lb.Language }, { TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort //Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
@ -280,7 +281,7 @@ namespace LibationFileManager
{ TemplateTags.DatePublished, lb => lb.DatePublished }, { TemplateTags.DatePublished, lb => lb.DatePublished },
{ TemplateTags.DateAdded, lb => lb.DateAdded }, { TemplateTags.DateAdded, lb => lb.DateAdded },
{ TemplateTags.FileDate, lb => lb.FileDate }, { TemplateTags.FileDate, lb => lb.FileDate },
}; };
private static readonly List<TagCollection> chapterPropertyTags = new() private static readonly List<TagCollection> chapterPropertyTags = new()
{ {
@ -290,7 +291,8 @@ namespace LibationFileManager
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
{ TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleTitle, lb => lb.Title },
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
{ TemplateTags.Series, lb => lb.SeriesName }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter },
}, },
new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter) new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
{ {
@ -332,6 +334,9 @@ namespace LibationFileManager
return language[..3].ToUpper(); return language[..3].ToUpper();
} }
private static string FormattableFormatter(ITemplateTag templateTag, IFormattable? value, string formatString)
=> value?.ToString(formatString, null) ?? "";
private static string StringFormatter(ITemplateTag templateTag, string value, string formatString) private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
{ {
if (value is null) return ""; if (value is null) return "";
@ -368,7 +373,7 @@ namespace LibationFileManager
public class FolderTemplate : Templates, ITemplate public class FolderTemplate : Templates, ITemplate
{ {
public static string Name { get; }= "Folder Template"; public static string Name { get; } = "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections public static IEnumerable<TagCollection> TagCollections

View File

@ -1,17 +0,0 @@
using Dinah.Core;
using System;
namespace LibationUiBase
{
public record EnumDiaplay<T> where T : Enum
{
public T Value { get; }
public string Description { get; }
public EnumDiaplay(T value, string description = null)
{
Value = value;
Description = description ?? value.GetDescription() ?? value.ToString();
}
public override string ToString() => Description;
}
}

View File

@ -0,0 +1,21 @@
using Dinah.Core;
using System;
namespace LibationUiBase
{
public class EnumDisplay<T> where T : Enum
{
public T Value { get; }
public string Description { get; }
public EnumDisplay(T value, string description = null)
{
Value = value;
Description = description ?? value.GetDescription() ?? value.ToString();
}
public override string ToString() => Description;
public override bool Equals(object obj)
=> (obj is EnumDisplay<T> other && other.Value.Equals(Value)) || (obj is T value && value.Equals(Value));
public override int GetHashCode() => Value.GetHashCode();
}
}

View File

@ -2,6 +2,7 @@
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -25,7 +26,7 @@ public class GridContextMenu
public string FolderTemplateText => "Folder Template"; public string FolderTemplateText => "Folder Template";
public string FileTemplateText => "File Template"; public string FileTemplateText => "File Template";
public string MultipartTemplateText => "Multipart File Template"; public string MultipartTemplateText => "Multipart File Template";
public string ViewBookmarksText => "View _Bookmarks/Clips"; public string ViewBookmarksText => $"View {Accelerator}Bookmarks/Clips";
public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series"; public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)); public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace LibationUiBase;
public enum LibationContributorType
{
Contributor,
Collaborator,
Creator
}
public class LibationContributor
{
public string Name { get; }
public LibationContributorType Type { get; }
public Uri Link { get; }
public static IEnumerable<LibationContributor> PrimaryContributors
=> Contributors.Where(c => c.Type is LibationContributorType.Creator or LibationContributorType.Collaborator);
public static IEnumerable<LibationContributor> AdditionalContributors
=> Contributors.Where(c => c.Type is LibationContributorType.Contributor);
public static IReadOnlyList<LibationContributor> Contributors { get; }
= new ReadOnlyCollection<LibationContributor>([
GitHubUser("rmcrackan", LibationContributorType.Creator),
GitHubUser("Mbucari", LibationContributorType.Collaborator),
GitHubUser("pixil98"),
GitHubUser("hutattedonmyarm"),
GitHubUser("seanke"),
GitHubUser("wtanksleyjr"),
GitHubUser("Dr.Blank"),
GitHubUser("CharlieRussel"),
GitHubUser("cbordeman"),
GitHubUser("jwillikers"),
GitHubUser("Shuvashish76"),
GitHubUser("RokeJulianLockhart"),
GitHubUser("maaximal"),
GitHubUser("muchtall"),
GitHubUser("ScubyG"),
GitHubUser("patienttruth"),
GitHubUser("stickystyle")
]);
private LibationContributor(string name, LibationContributorType type,Uri link)
{
Name = name;
Type = type;
Link = link;
}
private static LibationContributor GitHubUser(string name, LibationContributorType type = LibationContributorType.Contributor)
=> new LibationContributor(name, type, new Uri($"ht" + $"tps://github.com/{name.Replace('.', '-')}"));
}

View File

@ -40,15 +40,8 @@
label2 = new System.Windows.Forms.Label(); label2 = new System.Windows.Forms.Label();
label1 = new System.Windows.Forms.Label(); label1 = new System.Windows.Forms.Label();
flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
linkLabel4 = new System.Windows.Forms.LinkLabel();
linkLabel2 = new System.Windows.Forms.LinkLabel();
linkLabel3 = new System.Windows.Forms.LinkLabel();
linkLabel1 = new System.Windows.Forms.LinkLabel();
linkLabel5 = new System.Windows.Forms.LinkLabel();
linkLabel6 = new System.Windows.Forms.LinkLabel();
((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
groupBox1.SuspendLayout(); groupBox1.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
// pictureBox1 // pictureBox1
@ -65,7 +58,7 @@
// releaseNotesLbl // releaseNotesLbl
// //
releaseNotesLbl.AutoSize = true; releaseNotesLbl.AutoSize = true;
releaseNotesLbl.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); releaseNotesLbl.Font = new System.Drawing.Font("Segoe UI", 11F);
releaseNotesLbl.Location = new System.Drawing.Point(12, 12); releaseNotesLbl.Location = new System.Drawing.Point(12, 12);
releaseNotesLbl.Name = "releaseNotesLbl"; releaseNotesLbl.Name = "releaseNotesLbl";
releaseNotesLbl.Size = new System.Drawing.Size(171, 20); releaseNotesLbl.Size = new System.Drawing.Size(171, 20);
@ -77,7 +70,7 @@
// checkForUpgradeBtn // checkForUpgradeBtn
// //
checkForUpgradeBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; checkForUpgradeBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
checkForUpgradeBtn.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); checkForUpgradeBtn.Font = new System.Drawing.Font("Segoe UI", 10F);
checkForUpgradeBtn.Location = new System.Drawing.Point(12, 54); checkForUpgradeBtn.Location = new System.Drawing.Point(12, 54);
checkForUpgradeBtn.Name = "checkForUpgradeBtn"; checkForUpgradeBtn.Name = "checkForUpgradeBtn";
checkForUpgradeBtn.Size = new System.Drawing.Size(410, 31); checkForUpgradeBtn.Size = new System.Drawing.Size(410, 31);
@ -90,7 +83,7 @@
// //
getLibationLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; getLibationLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
getLibationLbl.AutoSize = true; getLibationLbl.AutoSize = true;
getLibationLbl.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); getLibationLbl.Font = new System.Drawing.Font("Segoe UI", 11F);
getLibationLbl.Location = new System.Drawing.Point(245, 12); getLibationLbl.Location = new System.Drawing.Point(245, 12);
getLibationLbl.Name = "getLibationLbl"; getLibationLbl.Name = "getLibationLbl";
getLibationLbl.Size = new System.Drawing.Size(162, 20); getLibationLbl.Size = new System.Drawing.Size(162, 20);
@ -103,7 +96,7 @@
// //
rmcrackanLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; rmcrackanLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
rmcrackanLbl.AutoSize = true; rmcrackanLbl.AutoSize = true;
rmcrackanLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); rmcrackanLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
rmcrackanLbl.Location = new System.Drawing.Point(6, 19); rmcrackanLbl.Location = new System.Drawing.Point(6, 19);
rmcrackanLbl.Name = "rmcrackanLbl"; rmcrackanLbl.Name = "rmcrackanLbl";
rmcrackanLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); rmcrackanLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
@ -111,13 +104,13 @@
rmcrackanLbl.TabIndex = 8; rmcrackanLbl.TabIndex = 8;
rmcrackanLbl.TabStop = true; rmcrackanLbl.TabStop = true;
rmcrackanLbl.Text = "rmcrackan"; rmcrackanLbl.Text = "rmcrackan";
rmcrackanLbl.LinkClicked += Link_GithubUser; rmcrackanLbl.LinkClicked += ContributorLabel_LinkClicked;
// //
// MBucariLbl // MBucariLbl
// //
MBucariLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; MBucariLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
MBucariLbl.AutoSize = true; MBucariLbl.AutoSize = true;
MBucariLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); MBucariLbl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
MBucariLbl.Location = new System.Drawing.Point(6, 40); MBucariLbl.Location = new System.Drawing.Point(6, 40);
MBucariLbl.Name = "MBucariLbl"; MBucariLbl.Name = "MBucariLbl";
MBucariLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3); MBucariLbl.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
@ -125,7 +118,7 @@
MBucariLbl.TabIndex = 9; MBucariLbl.TabIndex = 9;
MBucariLbl.TabStop = true; MBucariLbl.TabStop = true;
MBucariLbl.Text = "Mbucari"; MBucariLbl.Text = "Mbucari";
MBucariLbl.LinkClicked += Link_GithubUser; MBucariLbl.LinkClicked += ContributorLabel_LinkClicked;
// //
// groupBox1 // groupBox1
// //
@ -147,7 +140,7 @@
// label3 // label3
// //
label3.AutoSize = true; label3.AutoSize = true;
label3.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); label3.Font = new System.Drawing.Font("Segoe UI", 10F);
label3.Location = new System.Drawing.Point(92, 43); label3.Location = new System.Drawing.Point(92, 43);
label3.Name = "label3"; label3.Name = "label3";
label3.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); label3.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3);
@ -158,7 +151,7 @@
// label4 // label4
// //
label4.AutoSize = true; label4.AutoSize = true;
label4.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); label4.Font = new System.Drawing.Font("Segoe UI", 10F);
label4.Location = new System.Drawing.Point(92, 22); label4.Location = new System.Drawing.Point(92, 22);
label4.Name = "label4"; label4.Name = "label4";
label4.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); label4.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3);
@ -169,7 +162,7 @@
// label2 // label2
// //
label2.AutoSize = true; label2.AutoSize = true;
label2.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); label2.Font = new System.Drawing.Font("Segoe UI", 10F);
label2.Location = new System.Drawing.Point(92, 22); label2.Location = new System.Drawing.Point(92, 22);
label2.Name = "label2"; label2.Name = "label2";
label2.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3); label2.Padding = new System.Windows.Forms.Padding(0, 0, 0, 3);
@ -189,101 +182,11 @@
// flowLayoutPanel1 // flowLayoutPanel1
// //
flowLayoutPanel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; flowLayoutPanel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
flowLayoutPanel1.Controls.Add(linkLabel4);
flowLayoutPanel1.Controls.Add(linkLabel2);
flowLayoutPanel1.Controls.Add(linkLabel3);
flowLayoutPanel1.Controls.Add(linkLabel1);
flowLayoutPanel1.Controls.Add(linkLabel5);
flowLayoutPanel1.Controls.Add(linkLabel6);
flowLayoutPanel1.Location = new System.Drawing.Point(6, 100); flowLayoutPanel1.Location = new System.Drawing.Point(6, 100);
flowLayoutPanel1.Name = "flowLayoutPanel1"; flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new System.Drawing.Size(398, 66); flowLayoutPanel1.Size = new System.Drawing.Size(398, 66);
flowLayoutPanel1.TabIndex = 10; flowLayoutPanel1.TabIndex = 10;
// //
// linkLabel4
//
linkLabel4.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel4.AutoSize = true;
linkLabel4.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel4.Location = new System.Drawing.Point(3, 0);
linkLabel4.Name = "linkLabel4";
linkLabel4.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel4.Size = new System.Drawing.Size(41, 21);
linkLabel4.TabIndex = 9;
linkLabel4.TabStop = true;
linkLabel4.Text = "pixil98";
linkLabel4.LinkClicked += Link_GithubUser;
//
// linkLabel2
//
linkLabel2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel2.AutoSize = true;
linkLabel2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel2.Location = new System.Drawing.Point(50, 0);
linkLabel2.Name = "linkLabel2";
linkLabel2.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel2.Size = new System.Drawing.Size(104, 21);
linkLabel2.TabIndex = 9;
linkLabel2.TabStop = true;
linkLabel2.Text = "hutattedonmyarm";
linkLabel2.LinkClicked += Link_GithubUser;
//
// linkLabel3
//
linkLabel3.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel3.AutoSize = true;
linkLabel3.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel3.Location = new System.Drawing.Point(160, 0);
linkLabel3.Name = "linkLabel3";
linkLabel3.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel3.Size = new System.Drawing.Size(43, 21);
linkLabel3.TabIndex = 9;
linkLabel3.TabStop = true;
linkLabel3.Text = "seanke";
linkLabel3.LinkClicked += Link_GithubUser;
//
// linkLabel1
//
linkLabel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel1.AutoSize = true;
linkLabel1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel1.Location = new System.Drawing.Point(209, 0);
linkLabel1.Name = "linkLabel1";
linkLabel1.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel1.Size = new System.Drawing.Size(66, 21);
linkLabel1.TabIndex = 9;
linkLabel1.TabStop = true;
linkLabel1.Text = "wtanksleyjr";
linkLabel1.LinkClicked += Link_GithubUser;
//
// linkLabel5
//
linkLabel5.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel5.AutoSize = true;
linkLabel5.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel5.Location = new System.Drawing.Point(281, 0);
linkLabel5.Name = "linkLabel5";
linkLabel5.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel5.Size = new System.Drawing.Size(51, 21);
linkLabel5.TabIndex = 9;
linkLabel5.TabStop = true;
linkLabel5.Text = "Dr.Blank";
linkLabel5.LinkClicked += Link_GithubUser;
//
// linkLabel6
//
linkLabel6.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
linkLabel6.AutoSize = true;
linkLabel6.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
linkLabel6.Location = new System.Drawing.Point(3, 21);
linkLabel6.Name = "linkLabel6";
linkLabel6.Padding = new System.Windows.Forms.Padding(0, 3, 0, 3);
linkLabel6.Size = new System.Drawing.Size(77, 21);
linkLabel6.TabIndex = 9;
linkLabel6.TabStop = true;
linkLabel6.Text = "CharlieRussel";
linkLabel6.LinkClicked += Link_GithubUser;
//
// AboutDialog // AboutDialog
// //
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
@ -301,8 +204,6 @@
((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
groupBox1.ResumeLayout(false); groupBox1.ResumeLayout(false);
groupBox1.PerformLayout(); groupBox1.PerformLayout();
flowLayoutPanel1.ResumeLayout(false);
flowLayoutPanel1.PerformLayout();
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
@ -317,15 +218,9 @@
private System.Windows.Forms.LinkLabel MBucariLbl; private System.Windows.Forms.LinkLabel MBucariLbl;
private System.Windows.Forms.GroupBox groupBox1; private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label1;
private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
private System.Windows.Forms.LinkLabel linkLabel1;
private System.Windows.Forms.LinkLabel linkLabel4;
private System.Windows.Forms.LinkLabel linkLabel2;
private System.Windows.Forms.LinkLabel linkLabel3;
private System.Windows.Forms.LinkLabel linkLabel5;
private System.Windows.Forms.LinkLabel linkLabel6;
private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label4; private System.Windows.Forms.Label label4;
private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1;
private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label2;
} }
} }

View File

@ -1,5 +1,6 @@
using LibationUiBase; using LibationUiBase;
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
@ -13,10 +14,29 @@ namespace LibationWinForms.Dialogs
this.SetLibationIcon(); this.SetLibationIcon();
releaseNotesLbl.Text = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}"; releaseNotesLbl.Text = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";
rmcrackanLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == rmcrackanLbl.Text);
MBucariLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == MBucariLbl.Text);
foreach (var contributor in LibationContributor.AdditionalContributors)
{
var label = new LinkLabel { Tag = contributor, Text = contributor.Name, AutoSize = true };
label.LinkClicked += ContributorLabel_LinkClicked;
flowLayoutPanel1.Controls.Add(label);
}
var toolTip = new ToolTip(); var toolTip = new ToolTip();
toolTip.SetToolTip(releaseNotesLbl, "View Release Notes"); toolTip.SetToolTip(releaseNotesLbl, "View Release Notes");
} }
private void ContributorLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
if (sender is LinkLabel lbl && lbl.Tag is LibationContributor contributor)
{
Dinah.Core.Go.To.Url(contributor.Link.AbsoluteUri);
e.Link.Visited = true;
}
}
private void releaseNotesLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) private void releaseNotesLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
=> Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}"); => Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}");
@ -50,13 +70,5 @@ namespace LibationWinForms.Dialogs
private void getLibationLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) private void getLibationLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
=> Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl); => Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl);
private void Link_GithubUser(object sender, LinkLabelLinkClickedEventArgs e)
{
if (sender is LinkLabel lbl)
{
Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.', '-')}");
}
}
} }
} }

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -2,6 +2,9 @@
using LibationFileManager; using LibationFileManager;
using System.Linq; using System.Linq;
using LibationUiBase; using LibationUiBase;
using LibationFileManager.Templates;
using AudibleUtilities;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
@ -20,6 +23,7 @@ namespace LibationWinForms.Dialogs
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning)); this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
this.spatialCodecLbl.Text = desc(nameof(config.SpatialAudioCodec));
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles))); toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup))); toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
@ -30,41 +34,49 @@ namespace LibationWinForms.Dialogs
toolTip.SetToolTip(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits))); toolTip.SetToolTip(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits)));
toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile))); toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile)));
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio))); toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio)));
toolTip.SetToolTip(spatialCodecLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
fileDownloadQualityCb.Items.AddRange( fileDownloadQualityCb.Items.AddRange(
new object[] [
{ new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Normal),
Configuration.DownloadQuality.Normal, new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.High),
Configuration.DownloadQuality.High new EnumDisplay<Configuration.DownloadQuality>(Configuration.DownloadQuality.Spatial, "Spatial (if available)"),
}); ]);
spatialAudioCodecCb.Items.AddRange(
[
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.EC_3, "Dolby Digital Plus (E-AC-3)"),
new EnumDisplay<Configuration.SpatialCodec>(Configuration.SpatialCodec.AC_4, "Dolby AC-4")
]);
clipsBookmarksFormatCb.Items.AddRange( clipsBookmarksFormatCb.Items.AddRange(
new object[] [
{
Configuration.ClipBookmarkFormat.CSV, Configuration.ClipBookmarkFormat.CSV,
Configuration.ClipBookmarkFormat.Xlsx, Configuration.ClipBookmarkFormat.Xlsx,
Configuration.ClipBookmarkFormat.Json Configuration.ClipBookmarkFormat.Json
}); ]);
maxSampleRateCb.Items.AddRange( maxSampleRateCb.Items.AddRange(
Enum.GetValues<AAXClean.SampleRate>() Enum.GetValues<AAXClean.SampleRate>()
.Where(r => r >= AAXClean.SampleRate.Hz_8000 && r <= AAXClean.SampleRate.Hz_48000) .Where(r => r >= AAXClean.SampleRate.Hz_8000 && r <= AAXClean.SampleRate.Hz_48000)
.Select(v => new EnumDiaplay<AAXClean.SampleRate>(v, $"{(int)v} Hz")) .Select(v => new EnumDisplay<AAXClean.SampleRate>(v, $"{(int)v} Hz"))
.ToArray()); .ToArray());
encoderQualityCb.Items.AddRange( encoderQualityCb.Items.AddRange(
new object[] [
{
NAudio.Lame.EncoderQuality.High, NAudio.Lame.EncoderQuality.High,
NAudio.Lame.EncoderQuality.Standard, NAudio.Lame.EncoderQuality.Standard,
NAudio.Lame.EncoderQuality.Fast, NAudio.Lame.EncoderQuality.Fast,
}); ]);
allowLibationFixupCbox.Checked = config.AllowLibationFixup; allowLibationFixupCbox.Checked = config.AllowLibationFixup;
createCueSheetCbox.Checked = config.CreateCueSheet; createCueSheetCbox.Checked = config.CreateCueSheet;
downloadCoverArtCbox.Checked = config.DownloadCoverArt; downloadCoverArtCbox.Checked = config.DownloadCoverArt;
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks; downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality; fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat; clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile; retainAaxFileCbox.Checked = config.RetainAaxFile;
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles; combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
@ -79,11 +91,7 @@ namespace LibationWinForms.Dialogs
lameTargetBitrateRb.Checked = config.LameTargetBitrate; lameTargetBitrateRb.Checked = config.LameTargetBitrate;
lameTargetQualityRb.Checked = !config.LameTargetBitrate; lameTargetQualityRb.Checked = !config.LameTargetBitrate;
maxSampleRateCb.SelectedItem maxSampleRateCb.SelectedItem = config.MaxSampleRate;
= maxSampleRateCb.Items
.Cast<EnumDiaplay<AAXClean.SampleRate>>()
.SingleOrDefault(v => v.Value == config.MaxSampleRate)
?? maxSampleRateCb.Items[0];
encoderQualityCb.SelectedItem = config.LameEncoderQuality; encoderQualityCb.SelectedItem = config.LameEncoderQuality;
lameDownsampleMonoCbox.Checked = config.LameDownsampleMono; lameDownsampleMonoCbox.Checked = config.LameDownsampleMono;
@ -109,7 +117,8 @@ namespace LibationWinForms.Dialogs
config.CreateCueSheet = createCueSheetCbox.Checked; config.CreateCueSheet = createCueSheetCbox.Checked;
config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.DownloadCoverArt = downloadCoverArtCbox.Checked;
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked; config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem; config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value;
config.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem; config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked; config.RetainAaxFile = retainAaxFileCbox.Checked;
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked; config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
@ -120,7 +129,7 @@ namespace LibationWinForms.Dialogs
config.DecryptToLossy = convertLossyRb.Checked; config.DecryptToLossy = convertLossyRb.Checked;
config.MoveMoovToBeginning = moveMoovAtomCbox.Checked; config.MoveMoovToBeginning = moveMoovAtomCbox.Checked;
config.LameTargetBitrate = lameTargetBitrateRb.Checked; config.LameTargetBitrate = lameTargetBitrateRb.Checked;
config.MaxSampleRate = ((EnumDiaplay<AAXClean.SampleRate>)maxSampleRateCb.SelectedItem).Value; config.MaxSampleRate = ((EnumDisplay<AAXClean.SampleRate>)maxSampleRateCb.SelectedItem).Value;
config.LameEncoderQuality = (NAudio.Lame.EncoderQuality)encoderQualityCb.SelectedItem; config.LameEncoderQuality = (NAudio.Lame.EncoderQuality)encoderQualityCb.SelectedItem;
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked; config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
config.LameBitrate = lameBitrateTb.Value; config.LameBitrate = lameBitrateTb.Value;
@ -180,5 +189,28 @@ namespace LibationWinForms.Dialogs
stripAudibleBrandingCbox.Checked = false; stripAudibleBrandingCbox.Checked = false;
} }
} }
private void fileDownloadQualityCb_SelectedIndexChanged(object sender, EventArgs e)
{
var selectedSpatial = fileDownloadQualityCb.SelectedItem.Equals(Configuration.DownloadQuality.Spatial);
if (selectedSpatial)
{
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{
MessageBox.Show(this,
"Your must remove account(s) from Libation and then re-add them to enable spatial audiobook downloads.",
"Spatial Audio Unavailable",
MessageBoxButtons.OK);
fileDownloadQualityCb.SelectedItem = Configuration.DownloadQuality.High;
return;
}
}
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = selectedSpatial;
}
} }
} }

View File

@ -84,6 +84,8 @@
folderTemplateTb = new System.Windows.Forms.TextBox(); folderTemplateTb = new System.Windows.Forms.TextBox();
folderTemplateLbl = new System.Windows.Forms.Label(); folderTemplateLbl = new System.Windows.Forms.Label();
tab4AudioFileOptions = new System.Windows.Forms.TabPage(); tab4AudioFileOptions = new System.Windows.Forms.TabPage();
spatialAudioCodecCb = new System.Windows.Forms.ComboBox();
spatialCodecLbl = new System.Windows.Forms.Label();
moveMoovAtomCbox = new System.Windows.Forms.CheckBox(); moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
fileDownloadQualityCb = new System.Windows.Forms.ComboBox(); fileDownloadQualityCb = new System.Windows.Forms.ComboBox();
fileDownloadQualityLbl = new System.Windows.Forms.Label(); fileDownloadQualityLbl = new System.Windows.Forms.Label();
@ -281,10 +283,10 @@
// stripAudibleBrandingCbox // stripAudibleBrandingCbox
// //
stripAudibleBrandingCbox.AutoSize = true; stripAudibleBrandingCbox.AutoSize = true;
stripAudibleBrandingCbox.Location = new System.Drawing.Point(13, 72); stripAudibleBrandingCbox.Location = new System.Drawing.Point(13, 70);
stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox"; stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox";
stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34); stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34);
stripAudibleBrandingCbox.TabIndex = 13; stripAudibleBrandingCbox.TabIndex = 14;
stripAudibleBrandingCbox.Text = "[StripAudibleBranding\r\ndesc]"; stripAudibleBrandingCbox.Text = "[StripAudibleBranding\r\ndesc]";
stripAudibleBrandingCbox.UseVisualStyleBackColor = true; stripAudibleBrandingCbox.UseVisualStyleBackColor = true;
// //
@ -294,7 +296,7 @@
splitFilesByChapterCbox.Location = new System.Drawing.Point(13, 22); splitFilesByChapterCbox.Location = new System.Drawing.Point(13, 22);
splitFilesByChapterCbox.Name = "splitFilesByChapterCbox"; splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19); splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
splitFilesByChapterCbox.TabIndex = 13; splitFilesByChapterCbox.TabIndex = 12;
splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]"; splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
splitFilesByChapterCbox.UseVisualStyleBackColor = true; splitFilesByChapterCbox.UseVisualStyleBackColor = true;
splitFilesByChapterCbox.CheckedChanged += splitFilesByChapterCbox_CheckedChanged; splitFilesByChapterCbox.CheckedChanged += splitFilesByChapterCbox_CheckedChanged;
@ -304,10 +306,10 @@
allowLibationFixupCbox.AutoSize = true; allowLibationFixupCbox.AutoSize = true;
allowLibationFixupCbox.Checked = true; allowLibationFixupCbox.Checked = true;
allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked; allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
allowLibationFixupCbox.Location = new System.Drawing.Point(19, 181); allowLibationFixupCbox.Location = new System.Drawing.Point(19, 205);
allowLibationFixupCbox.Name = "allowLibationFixupCbox"; allowLibationFixupCbox.Name = "allowLibationFixupCbox";
allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19); allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19);
allowLibationFixupCbox.TabIndex = 10; allowLibationFixupCbox.TabIndex = 11;
allowLibationFixupCbox.Text = "[AllowLibationFixup desc]"; allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
allowLibationFixupCbox.UseVisualStyleBackColor = true; allowLibationFixupCbox.UseVisualStyleBackColor = true;
allowLibationFixupCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged; allowLibationFixupCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -318,7 +320,7 @@
convertLossyRb.Location = new System.Drawing.Point(438, 53); convertLossyRb.Location = new System.Drawing.Point(438, 53);
convertLossyRb.Name = "convertLossyRb"; convertLossyRb.Name = "convertLossyRb";
convertLossyRb.Size = new System.Drawing.Size(329, 19); convertLossyRb.Size = new System.Drawing.Size(329, 19);
convertLossyRb.TabIndex = 12; convertLossyRb.TabIndex = 27;
convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)"; convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
convertLossyRb.UseVisualStyleBackColor = true; convertLossyRb.UseVisualStyleBackColor = true;
convertLossyRb.CheckedChanged += convertFormatRb_CheckedChanged; convertLossyRb.CheckedChanged += convertFormatRb_CheckedChanged;
@ -330,7 +332,7 @@
convertLosslessRb.Location = new System.Drawing.Point(438, 6); convertLosslessRb.Location = new System.Drawing.Point(438, 6);
convertLosslessRb.Name = "convertLosslessRb"; convertLosslessRb.Name = "convertLosslessRb";
convertLosslessRb.Size = new System.Drawing.Size(335, 19); convertLosslessRb.Size = new System.Drawing.Size(335, 19);
convertLosslessRb.TabIndex = 11; convertLosslessRb.TabIndex = 25;
convertLosslessRb.TabStop = true; convertLosslessRb.TabStop = true;
convertLosslessRb.Text = "Download my books in the original audio format (Lossless)"; convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
convertLosslessRb.UseVisualStyleBackColor = true; convertLosslessRb.UseVisualStyleBackColor = true;
@ -770,6 +772,8 @@
// tab4AudioFileOptions // tab4AudioFileOptions
// //
tab4AudioFileOptions.AutoScroll = true; tab4AudioFileOptions.AutoScroll = true;
tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb);
tab4AudioFileOptions.Controls.Add(spatialCodecLbl);
tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox); tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityCb); tab4AudioFileOptions.Controls.Add(fileDownloadQualityCb);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityLbl); tab4AudioFileOptions.Controls.Add(fileDownloadQualityLbl);
@ -794,13 +798,32 @@
tab4AudioFileOptions.Text = "Audio File Options"; tab4AudioFileOptions.Text = "Audio File Options";
tab4AudioFileOptions.UseVisualStyleBackColor = true; tab4AudioFileOptions.UseVisualStyleBackColor = true;
// //
// spatialAudioCodecCb
//
spatialAudioCodecCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
spatialAudioCodecCb.FormattingEnabled = true;
spatialAudioCodecCb.Location = new System.Drawing.Point(249, 35);
spatialAudioCodecCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3);
spatialAudioCodecCb.Name = "spatialAudioCodecCb";
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
spatialAudioCodecCb.TabIndex = 2;
//
// spatialCodecLbl
//
spatialCodecLbl.AutoSize = true;
spatialCodecLbl.Location = new System.Drawing.Point(19, 37);
spatialCodecLbl.Name = "spatialCodecLbl";
spatialCodecLbl.Size = new System.Drawing.Size(143, 15);
spatialCodecLbl.TabIndex = 24;
spatialCodecLbl.Text = "[SpatialAudioCodec desc]";
//
// moveMoovAtomCbox // moveMoovAtomCbox
// //
moveMoovAtomCbox.AutoSize = true; moveMoovAtomCbox.AutoSize = true;
moveMoovAtomCbox.Location = new System.Drawing.Point(448, 28); moveMoovAtomCbox.Location = new System.Drawing.Point(448, 28);
moveMoovAtomCbox.Name = "moveMoovAtomCbox"; moveMoovAtomCbox.Name = "moveMoovAtomCbox";
moveMoovAtomCbox.Size = new System.Drawing.Size(189, 19); moveMoovAtomCbox.Size = new System.Drawing.Size(189, 19);
moveMoovAtomCbox.TabIndex = 14; moveMoovAtomCbox.TabIndex = 26;
moveMoovAtomCbox.Text = "[MoveMoovToBeginning desc]"; moveMoovAtomCbox.Text = "[MoveMoovToBeginning desc]";
moveMoovAtomCbox.UseVisualStyleBackColor = true; moveMoovAtomCbox.UseVisualStyleBackColor = true;
// //
@ -808,11 +831,12 @@
// //
fileDownloadQualityCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; fileDownloadQualityCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
fileDownloadQualityCb.FormattingEnabled = true; fileDownloadQualityCb.FormattingEnabled = true;
fileDownloadQualityCb.Location = new System.Drawing.Point(264, 8); fileDownloadQualityCb.Location = new System.Drawing.Point(292, 6);
fileDownloadQualityCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3); fileDownloadQualityCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3);
fileDownloadQualityCb.Name = "fileDownloadQualityCb"; fileDownloadQualityCb.Name = "fileDownloadQualityCb";
fileDownloadQualityCb.Size = new System.Drawing.Size(88, 23); fileDownloadQualityCb.Size = new System.Drawing.Size(130, 23);
fileDownloadQualityCb.TabIndex = 23; fileDownloadQualityCb.TabIndex = 1;
fileDownloadQualityCb.SelectedIndexChanged += fileDownloadQualityCb_SelectedIndexChanged;
// //
// fileDownloadQualityLbl // fileDownloadQualityLbl
// //
@ -827,10 +851,10 @@
// combineNestedChapterTitlesCbox // combineNestedChapterTitlesCbox
// //
combineNestedChapterTitlesCbox.AutoSize = true; combineNestedChapterTitlesCbox.AutoSize = true;
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 157); combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 181);
combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox"; combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox";
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19); combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19);
combineNestedChapterTitlesCbox.TabIndex = 13; combineNestedChapterTitlesCbox.TabIndex = 10;
combineNestedChapterTitlesCbox.Text = "[CombineNestedChapterTitles desc]"; combineNestedChapterTitlesCbox.Text = "[CombineNestedChapterTitles desc]";
combineNestedChapterTitlesCbox.UseVisualStyleBackColor = true; combineNestedChapterTitlesCbox.UseVisualStyleBackColor = true;
// //
@ -838,18 +862,18 @@
// //
clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
clipsBookmarksFormatCb.FormattingEnabled = true; clipsBookmarksFormatCb.FormattingEnabled = true;
clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 81); clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 107);
clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb"; clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb";
clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23); clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23);
clipsBookmarksFormatCb.TabIndex = 21; clipsBookmarksFormatCb.TabIndex = 6;
// //
// downloadClipsBookmarksCbox // downloadClipsBookmarksCbox
// //
downloadClipsBookmarksCbox.AutoSize = true; downloadClipsBookmarksCbox.AutoSize = true;
downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 82); downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 109);
downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox"; downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox";
downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19); downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19);
downloadClipsBookmarksCbox.TabIndex = 20; downloadClipsBookmarksCbox.TabIndex = 5;
downloadClipsBookmarksCbox.Text = "Download Clips, Notes, and Bookmarks as"; downloadClipsBookmarksCbox.Text = "Download Clips, Notes, and Bookmarks as";
downloadClipsBookmarksCbox.UseVisualStyleBackColor = true; downloadClipsBookmarksCbox.UseVisualStyleBackColor = true;
downloadClipsBookmarksCbox.CheckedChanged += downloadClipsBookmarksCbox_CheckedChanged; downloadClipsBookmarksCbox.CheckedChanged += downloadClipsBookmarksCbox_CheckedChanged;
@ -859,9 +883,9 @@
audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox); audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox);
audiobookFixupsGb.Controls.Add(stripUnabridgedCbox); audiobookFixupsGb.Controls.Add(stripUnabridgedCbox);
audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox); audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox);
audiobookFixupsGb.Location = new System.Drawing.Point(6, 200); audiobookFixupsGb.Location = new System.Drawing.Point(6, 229);
audiobookFixupsGb.Name = "audiobookFixupsGb"; audiobookFixupsGb.Name = "audiobookFixupsGb";
audiobookFixupsGb.Size = new System.Drawing.Size(416, 116); audiobookFixupsGb.Size = new System.Drawing.Size(416, 114);
audiobookFixupsGb.TabIndex = 19; audiobookFixupsGb.TabIndex = 19;
audiobookFixupsGb.TabStop = false; audiobookFixupsGb.TabStop = false;
audiobookFixupsGb.Text = "Audiobook Fix-ups"; audiobookFixupsGb.Text = "Audiobook Fix-ups";
@ -869,7 +893,7 @@
// stripUnabridgedCbox // stripUnabridgedCbox
// //
stripUnabridgedCbox.AutoSize = true; stripUnabridgedCbox.AutoSize = true;
stripUnabridgedCbox.Location = new System.Drawing.Point(13, 47); stripUnabridgedCbox.Location = new System.Drawing.Point(13, 46);
stripUnabridgedCbox.Name = "stripUnabridgedCbox"; stripUnabridgedCbox.Name = "stripUnabridgedCbox";
stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19); stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
stripUnabridgedCbox.TabIndex = 13; stripUnabridgedCbox.TabIndex = 13;
@ -894,7 +918,7 @@
chapterTitleTemplateBtn.Location = new System.Drawing.Point(769, 22); chapterTitleTemplateBtn.Location = new System.Drawing.Point(769, 22);
chapterTitleTemplateBtn.Name = "chapterTitleTemplateBtn"; chapterTitleTemplateBtn.Name = "chapterTitleTemplateBtn";
chapterTitleTemplateBtn.Size = new System.Drawing.Size(75, 23); chapterTitleTemplateBtn.Size = new System.Drawing.Size(75, 23);
chapterTitleTemplateBtn.TabIndex = 17; chapterTitleTemplateBtn.TabIndex = 15;
chapterTitleTemplateBtn.Text = "Edit..."; chapterTitleTemplateBtn.Text = "Edit...";
chapterTitleTemplateBtn.UseVisualStyleBackColor = true; chapterTitleTemplateBtn.UseVisualStyleBackColor = true;
chapterTitleTemplateBtn.Click += chapterTitleTemplateBtn_Click; chapterTitleTemplateBtn.Click += chapterTitleTemplateBtn_Click;
@ -954,7 +978,7 @@
encoderQualityCb.Location = new System.Drawing.Point(327, 72); encoderQualityCb.Location = new System.Drawing.Point(327, 72);
encoderQualityCb.Name = "encoderQualityCb"; encoderQualityCb.Name = "encoderQualityCb";
encoderQualityCb.Size = new System.Drawing.Size(79, 23); encoderQualityCb.Size = new System.Drawing.Size(79, 23);
encoderQualityCb.TabIndex = 2; encoderQualityCb.TabIndex = 32;
// //
// maxSampleRateCb // maxSampleRateCb
// //
@ -964,7 +988,7 @@
maxSampleRateCb.Location = new System.Drawing.Point(113, 72); maxSampleRateCb.Location = new System.Drawing.Point(113, 72);
maxSampleRateCb.Name = "maxSampleRateCb"; maxSampleRateCb.Name = "maxSampleRateCb";
maxSampleRateCb.Size = new System.Drawing.Size(75, 23); maxSampleRateCb.Size = new System.Drawing.Size(75, 23);
maxSampleRateCb.TabIndex = 2; maxSampleRateCb.TabIndex = 31;
// //
// lameDownsampleMonoCbox // lameDownsampleMonoCbox
// //
@ -972,7 +996,7 @@
lameDownsampleMonoCbox.Location = new System.Drawing.Point(209, 29); lameDownsampleMonoCbox.Location = new System.Drawing.Point(209, 29);
lameDownsampleMonoCbox.Name = "lameDownsampleMonoCbox"; lameDownsampleMonoCbox.Name = "lameDownsampleMonoCbox";
lameDownsampleMonoCbox.Size = new System.Drawing.Size(197, 34); lameDownsampleMonoCbox.Size = new System.Drawing.Size(197, 34);
lameDownsampleMonoCbox.TabIndex = 1; lameDownsampleMonoCbox.TabIndex = 30;
lameDownsampleMonoCbox.Text = "Downsample stereo to mono?\r\n(Recommended)\r\n"; lameDownsampleMonoCbox.Text = "Downsample stereo to mono?\r\n(Recommended)\r\n";
lameDownsampleMonoCbox.UseVisualStyleBackColor = true; lameDownsampleMonoCbox.UseVisualStyleBackColor = true;
// //
@ -1002,7 +1026,7 @@
LameMatchSourceBRCbox.Location = new System.Drawing.Point(254, 65); LameMatchSourceBRCbox.Location = new System.Drawing.Point(254, 65);
LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox"; LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox";
LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19); LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19);
LameMatchSourceBRCbox.TabIndex = 3; LameMatchSourceBRCbox.TabIndex = 35;
LameMatchSourceBRCbox.Text = "Match source bitrate?"; LameMatchSourceBRCbox.Text = "Match source bitrate?";
LameMatchSourceBRCbox.UseVisualStyleBackColor = true; LameMatchSourceBRCbox.UseVisualStyleBackColor = true;
LameMatchSourceBRCbox.CheckedChanged += LameMatchSourceBRCbox_CheckedChanged; LameMatchSourceBRCbox.CheckedChanged += LameMatchSourceBRCbox_CheckedChanged;
@ -1013,7 +1037,7 @@
lameConstantBitrateCbox.Location = new System.Drawing.Point(10, 65); lameConstantBitrateCbox.Location = new System.Drawing.Point(10, 65);
lameConstantBitrateCbox.Name = "lameConstantBitrateCbox"; lameConstantBitrateCbox.Name = "lameConstantBitrateCbox";
lameConstantBitrateCbox.Size = new System.Drawing.Size(216, 19); lameConstantBitrateCbox.Size = new System.Drawing.Size(216, 19);
lameConstantBitrateCbox.TabIndex = 2; lameConstantBitrateCbox.TabIndex = 34;
lameConstantBitrateCbox.Text = "Restrict encoder to constant bitrate?"; lameConstantBitrateCbox.Text = "Restrict encoder to constant bitrate?";
lameConstantBitrateCbox.UseVisualStyleBackColor = true; lameConstantBitrateCbox.UseVisualStyleBackColor = true;
// //
@ -1093,7 +1117,7 @@
lameBitrateTb.Name = "lameBitrateTb"; lameBitrateTb.Name = "lameBitrateTb";
lameBitrateTb.Size = new System.Drawing.Size(388, 45); lameBitrateTb.Size = new System.Drawing.Size(388, 45);
lameBitrateTb.SmallChange = 8; lameBitrateTb.SmallChange = 8;
lameBitrateTb.TabIndex = 0; lameBitrateTb.TabIndex = 33;
lameBitrateTb.TickFrequency = 16; lameBitrateTb.TickFrequency = 16;
lameBitrateTb.Value = 64; lameBitrateTb.Value = 64;
// //
@ -1246,7 +1270,7 @@
lameVBRQualityTb.Maximum = 9; lameVBRQualityTb.Maximum = 9;
lameVBRQualityTb.Name = "lameVBRQualityTb"; lameVBRQualityTb.Name = "lameVBRQualityTb";
lameVBRQualityTb.Size = new System.Drawing.Size(388, 45); lameVBRQualityTb.Size = new System.Drawing.Size(388, 45);
lameVBRQualityTb.TabIndex = 0; lameVBRQualityTb.TabIndex = 36;
lameVBRQualityTb.Value = 9; lameVBRQualityTb.Value = 9;
// //
// groupBox2 // groupBox2
@ -1268,7 +1292,7 @@
lameTargetQualityRb.Location = new System.Drawing.Point(104, 18); lameTargetQualityRb.Location = new System.Drawing.Point(104, 18);
lameTargetQualityRb.Name = "lameTargetQualityRb"; lameTargetQualityRb.Name = "lameTargetQualityRb";
lameTargetQualityRb.Size = new System.Drawing.Size(63, 19); lameTargetQualityRb.Size = new System.Drawing.Size(63, 19);
lameTargetQualityRb.TabIndex = 0; lameTargetQualityRb.TabIndex = 29;
lameTargetQualityRb.TabStop = true; lameTargetQualityRb.TabStop = true;
lameTargetQualityRb.Text = "Quality"; lameTargetQualityRb.Text = "Quality";
lameTargetQualityRb.UseVisualStyleBackColor = true; lameTargetQualityRb.UseVisualStyleBackColor = true;
@ -1280,7 +1304,7 @@
lameTargetBitrateRb.Location = new System.Drawing.Point(14, 18); lameTargetBitrateRb.Location = new System.Drawing.Point(14, 18);
lameTargetBitrateRb.Name = "lameTargetBitrateRb"; lameTargetBitrateRb.Name = "lameTargetBitrateRb";
lameTargetBitrateRb.Size = new System.Drawing.Size(59, 19); lameTargetBitrateRb.Size = new System.Drawing.Size(59, 19);
lameTargetBitrateRb.TabIndex = 0; lameTargetBitrateRb.TabIndex = 28;
lameTargetBitrateRb.TabStop = true; lameTargetBitrateRb.TabStop = true;
lameTargetBitrateRb.Text = "Bitrate"; lameTargetBitrateRb.Text = "Bitrate";
lameTargetBitrateRb.UseVisualStyleBackColor = true; lameTargetBitrateRb.UseVisualStyleBackColor = true;
@ -1300,20 +1324,20 @@
// mergeOpeningEndCreditsCbox // mergeOpeningEndCreditsCbox
// //
mergeOpeningEndCreditsCbox.AutoSize = true; mergeOpeningEndCreditsCbox.AutoSize = true;
mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 133); mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 157);
mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox";
mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19); mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19);
mergeOpeningEndCreditsCbox.TabIndex = 13; mergeOpeningEndCreditsCbox.TabIndex = 9;
mergeOpeningEndCreditsCbox.Text = "[MergeOpeningEndCredits desc]"; mergeOpeningEndCreditsCbox.Text = "[MergeOpeningEndCredits desc]";
mergeOpeningEndCreditsCbox.UseVisualStyleBackColor = true; mergeOpeningEndCreditsCbox.UseVisualStyleBackColor = true;
// //
// retainAaxFileCbox // retainAaxFileCbox
// //
retainAaxFileCbox.AutoSize = true; retainAaxFileCbox.AutoSize = true;
retainAaxFileCbox.Location = new System.Drawing.Point(19, 107); retainAaxFileCbox.Location = new System.Drawing.Point(19, 133);
retainAaxFileCbox.Name = "retainAaxFileCbox"; retainAaxFileCbox.Name = "retainAaxFileCbox";
retainAaxFileCbox.Size = new System.Drawing.Size(131, 19); retainAaxFileCbox.Size = new System.Drawing.Size(131, 19);
retainAaxFileCbox.TabIndex = 10; retainAaxFileCbox.TabIndex = 8;
retainAaxFileCbox.Text = "[RetainAaxFile desc]"; retainAaxFileCbox.Text = "[RetainAaxFile desc]";
retainAaxFileCbox.UseVisualStyleBackColor = true; retainAaxFileCbox.UseVisualStyleBackColor = true;
retainAaxFileCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged; retainAaxFileCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1323,10 +1347,10 @@
downloadCoverArtCbox.AutoSize = true; downloadCoverArtCbox.AutoSize = true;
downloadCoverArtCbox.Checked = true; downloadCoverArtCbox.Checked = true;
downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked; downloadCoverArtCbox.CheckState = System.Windows.Forms.CheckState.Checked;
downloadCoverArtCbox.Location = new System.Drawing.Point(19, 58); downloadCoverArtCbox.Location = new System.Drawing.Point(19, 85);
downloadCoverArtCbox.Name = "downloadCoverArtCbox"; downloadCoverArtCbox.Name = "downloadCoverArtCbox";
downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19); downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19);
downloadCoverArtCbox.TabIndex = 10; downloadCoverArtCbox.TabIndex = 4;
downloadCoverArtCbox.Text = "[DownloadCoverArt desc]"; downloadCoverArtCbox.Text = "[DownloadCoverArt desc]";
downloadCoverArtCbox.UseVisualStyleBackColor = true; downloadCoverArtCbox.UseVisualStyleBackColor = true;
downloadCoverArtCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged; downloadCoverArtCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1336,10 +1360,10 @@
createCueSheetCbox.AutoSize = true; createCueSheetCbox.AutoSize = true;
createCueSheetCbox.Checked = true; createCueSheetCbox.Checked = true;
createCueSheetCbox.CheckState = System.Windows.Forms.CheckState.Checked; createCueSheetCbox.CheckState = System.Windows.Forms.CheckState.Checked;
createCueSheetCbox.Location = new System.Drawing.Point(19, 32); createCueSheetCbox.Location = new System.Drawing.Point(19, 61);
createCueSheetCbox.Name = "createCueSheetCbox"; createCueSheetCbox.Name = "createCueSheetCbox";
createCueSheetCbox.Size = new System.Drawing.Size(145, 19); createCueSheetCbox.Size = new System.Drawing.Size(145, 19);
createCueSheetCbox.TabIndex = 10; createCueSheetCbox.TabIndex = 3;
createCueSheetCbox.Text = "[CreateCueSheet desc]"; createCueSheetCbox.Text = "[CreateCueSheet desc]";
createCueSheetCbox.UseVisualStyleBackColor = true; createCueSheetCbox.UseVisualStyleBackColor = true;
createCueSheetCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged; createCueSheetCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1505,5 +1529,7 @@
private System.Windows.Forms.Label gridFontScaleFactorLbl; private System.Windows.Forms.Label gridFontScaleFactorLbl;
private System.Windows.Forms.GroupBox groupBox1; private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.Button applyDisplaySettingsBtn; private System.Windows.Forms.Button applyDisplaySettingsBtn;
private System.Windows.Forms.ComboBox spatialAudioCodecCb;
private System.Windows.Forms.Label spatialCodecLbl;
} }
} }

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs
gridScaleFactorLbl.Text = desc(nameof(config.GridScaleFactor)); gridScaleFactorLbl.Text = desc(nameof(config.GridScaleFactor));
gridFontScaleFactorLbl.Text = desc(nameof(config.GridFontScaleFactor)); gridFontScaleFactorLbl.Text = desc(nameof(config.GridFontScaleFactor));
var dateTimeSources = Enum.GetValues<Configuration.DateTimeSource>().Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v)).ToArray(); var dateTimeSources = Enum.GetValues<Configuration.DateTimeSource>().Select(v => new EnumDisplay<Configuration.DateTimeSource>(v)).ToArray();
creationTimeCb.Items.AddRange(dateTimeSources); creationTimeCb.Items.AddRange(dateTimeSources);
lastWriteTimeCb.Items.AddRange(dateTimeSources); lastWriteTimeCb.Items.AddRange(dateTimeSources);
@ -92,8 +92,8 @@ namespace LibationWinForms.Dialogs
config.OverwriteExisting = overwriteExistingCbox.Checked; config.OverwriteExisting = overwriteExistingCbox.Checked;
config.CreationTime = ((EnumDiaplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value; config.CreationTime = ((EnumDisplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value;
config.LastWriteTime = ((EnumDiaplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value; config.LastWriteTime = ((EnumDisplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value;
} }
private static int scaleFactorToLinearRange(float scaleFactor) private static int scaleFactorToLinearRange(float scaleFactor)

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -3,6 +3,7 @@ using AudibleUtilities;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using LibationWinForms.SeriesView; using LibationWinForms.SeriesView;
@ -258,7 +259,7 @@ namespace LibationWinForms.GridView
#region Edit Templates (Single book only) #region Edit Templates (Single book only)
void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new() where T : Templates, ITemplate, new()
{ {
var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate); var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template); var form = new EditTemplateDialog(template);
@ -280,8 +281,8 @@ namespace LibationWinForms.GridView
var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText }; var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText };
editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem }); editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem });
ctxMenu.Items.Add(new ToolStripSeparator());
ctxMenu.Items.Add(editTemplatesMenuItem); ctxMenu.Items.Add(editTemplatesMenuItem);
ctxMenu.Items.Add(new ToolStripSeparator());
} }
#endregion #endregion

View File

@ -26,7 +26,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3124.44" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -22,7 +22,7 @@ namespace AccountsTests
#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
public class AccountsTestBase public class AccountsTestBase
{ {
protected string EMPTY_FILE { get; } = "{\r\n \"Accounts\": []\r\n}".Replace("\r\n", Environment.NewLine); protected string EMPTY_FILE { get; } = "{\r\n \"Accounts\": [],\r\n \"Cdm\": null\r\n}".Replace("\r\n", Environment.NewLine);
protected string TestFile; protected string TestFile;
protected Locale usLocale => Localization.Get("us"); protected Locale usLocale => Localization.Get("us");

View File

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="3.8.3" />

View File

@ -346,8 +346,8 @@ namespace FileLiberator.Tests
} }
}; };
var flatChapters = DownloadDecryptBook.flattenChapters(HierarchicalChapters); var flatChapters = DownloadOptions.flattenChapters(HierarchicalChapters);
DownloadDecryptBook.combineCredits(flatChapters); DownloadOptions.combineCredits(flatChapters);
checkChapters(flatChapters, expected); checkChapters(flatChapters, expected);
} }
@ -429,7 +429,7 @@ namespace FileLiberator.Tests
} }
}; };
var flatChapters = DownloadDecryptBook.flattenChapters(HierarchicalChapters); var flatChapters = DownloadOptions.flattenChapters(HierarchicalChapters);
checkChapters(flatChapters, expected); checkChapters(flatChapters, expected);
} }
@ -525,7 +525,7 @@ namespace FileLiberator.Tests
} }
}; };
var flatChapters = DownloadDecryptBook.flattenChapters(HierarchicalChapters_LongerParents); var flatChapters = DownloadOptions.flattenChapters(HierarchicalChapters_LongerParents);
checkChapters(flatChapters, expected); checkChapters(flatChapters, expected);
} }

View File

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="3.8.3" />

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="3.8.3" />

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="3.8.3" />

View File

@ -6,7 +6,7 @@ using Dinah.Core;
using FileManager; using FileManager;
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
using FluentAssertions; using FluentAssertions;
using LibationFileManager; using LibationFileManager.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using static TemplatesTests.Shared; using static TemplatesTests.Shared;
@ -23,7 +23,10 @@ namespace TemplatesTests
public static class Shared public static class Shared
{ {
public static LibraryBookDto GetLibraryBook(string seriesName = "Sherlock Holmes") public static LibraryBookDto GetLibraryBook()
=> GetLibraryBook([new SeriesDto("Sherlock Holmes", 1, "B08376S3R2")]);
public static LibraryBookDto GetLibraryBook(IEnumerable<SeriesDto> series)
=> new() => new()
{ {
Account = "myaccount@example.co", Account = "myaccount@example.co",
@ -35,10 +38,9 @@ namespace TemplatesTests
Title = "A Study in Scarlet: A Sherlock Holmes Novel", Title = "A Study in Scarlet: A Sherlock Holmes Novel",
Locale = "us", Locale = "us",
YearPublished = 2017, YearPublished = 2017,
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
Narrators = new List<string> { "Stephen Fry" }, Narrators = [new("Stephen Fry", "B000APAGVS"), new("Some Narrator", "B000000000")],
SeriesName = seriesName ?? "", Series = series,
SeriesNumber = 1,
BitRate = 128, BitRate = 128,
SampleRate = 44100, SampleRate = 44100,
Channels = 2, Channels = 2,
@ -253,7 +255,6 @@ namespace TemplatesTests
} }
} }
[TestMethod] [TestMethod]
[DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28.m4b")] [DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28.m4b")]
public void DateFormat_null(string template, string dirFullPath, string extension, string expected) public void DateFormat_null(string template, string dirFullPath, string extension, string expected)
@ -308,7 +309,7 @@ namespace TemplatesTests
public void NameFormat_unusual(string author, string expected) public void NameFormat_unusual(string author, string expected)
{ {
var bookDto = GetLibraryBook(); var bookDto = GetLibraryBook();
bookDto.Authors = new List<string> { author }; bookDto.Authors = [new(author, null)];
Templates.TryGetTemplate<Templates.FileTemplate>("<author[format(Title={T}, First={F}, Middle={M} Last={L}, Suffix={S})]>", out var fileTemplate).Should().BeTrue(); Templates.TryGetTemplate<Templates.FileTemplate>("<author[format(Title={T}, First={F}, Middle={M} Last={L}, Suffix={S})]>", out var fileTemplate).Should().BeTrue();
fileTemplate fileTemplate
.GetFilename(bookDto, "", "", Replacements) .GetFilename(bookDto, "", "", Replacements)
@ -329,6 +330,11 @@ namespace TemplatesTests
[DataRow("<author[max(2)]>", "Jill Conner Browne, Charles E. Gannon")] [DataRow("<author[max(2)]>", "Jill Conner Browne, Charles E. Gannon")]
[DataRow("<author[max(3)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf")] [DataRow("<author[max(3)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf")]
[DataRow("<author[format({L}, {F})]>", "Browne, Jill, Gannon, Charles, Fetherolf, Christopher, Montgomery, Lucy, Bon Jovi, Jon, Van Doren, Paul")] [DataRow("<author[format({L}, {F})]>", "Browne, Jill, Gannon, Charles, Fetherolf, Christopher, Montgomery, Lucy, Bon Jovi, Jon, Van Doren, Paul")]
[DataRow("<author[format({L}, {F} {ID})]>", "Browne, Jill B1, Gannon, Charles B2, Fetherolf, Christopher B3, Montgomery, Lucy B4, Bon Jovi, Jon B5, Van Doren, Paul B6")]
[DataRow("<author[format({ID})]>", "B1, B2, B3, B4, B5, B6")]
[DataRow("<author[format({Id})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[format({iD})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[format({id})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[format({f}, {l})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] [DataRow("<author[format({f}, {l})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[format(First={F}, Last={L})]>", "First=Jill, Last=Browne, First=Charles, Last=Gannon, First=Christopher, Last=Fetherolf, First=Lucy, Last=Montgomery, First=Jon, Last=Bon Jovi, First=Paul, Last=Van Doren")] [DataRow("<author[format(First={F}, Last={L})]>", "First=Jill, Last=Browne, First=Charles, Last=Gannon, First=Christopher, Last=Fetherolf, First=Lucy, Last=Montgomery, First=Jon, Last=Bon Jovi, First=Paul, Last=Van Doren")]
[DataRow("<author[format({L}, {F}) separator( - ) max(3)]>", "Browne, Jill - Gannon, Charles - Fetherolf, Christopher")] [DataRow("<author[format({L}, {F}) separator( - ) max(3)]>", "Browne, Jill - Gannon, Charles - Fetherolf, Christopher")]
@ -337,18 +343,21 @@ namespace TemplatesTests
//Jon Bon Jovi and Paul Van Doren don't have middle names, so they are sorted to the top. //Jon Bon Jovi and Paul Van Doren don't have middle names, so they are sorted to the top.
//Since only the middle names of the first 2 names are to be displayed, the name string is empty. //Since only the middle names of the first 2 names are to be displayed, the name string is empty.
[DataRow("<author[sort(M) max(2) separator(; ) format({M})]>", ";")] [DataRow("<author[sort(M) max(2) separator(; ) format({M})]>", ";")]
[DataRow("<first author>", "Jill Conner Browne")]
[DataRow("<first author[]>", "Jill Conner Browne")]
[DataRow("<first author[{L}, {F}]>", "Browne, Jill")]
public void NameFormat_formatters(string template, string expected) public void NameFormat_formatters(string template, string expected)
{ {
var bookDto = GetLibraryBook(); var bookDto = GetLibraryBook();
bookDto.Authors = new List<string> bookDto.Authors =
{ [
"Jill Conner Browne", new("Jill Conner Browne", "B1"),
"Charles E. Gannon", new("Charles E. Gannon", "B2"),
"Christopher John Fetherolf", new("Christopher John Fetherolf", "B3"),
"Lucy Maud Montgomery", new("Lucy Maud Montgomery", "B4"),
"Jon Bon Jovi", new("Jon Bon Jovi", "B5"),
"Paul Van Doren" new("Paul Van Doren", "B6")
}; ];
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue(); Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
fileTemplate fileTemplate
@ -358,6 +367,35 @@ namespace TemplatesTests
} }
[TestMethod]
[DataRow("<series>", "Series A, Series B, Series C")]
[DataRow("<series[]>", "Series A, Series B, Series C")]
[DataRow("<series[max(1)]>", "Series A")]
[DataRow("<series[max(2)]>", "Series A, Series B")]
[DataRow("<series[max(3)]>", "Series A, Series B, Series C")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(3)]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(2)]>", "Series A, 1, B1; Series B, 6, B2")]
[DataRow("<first series>", "Series A")]
[DataRow("<first series[]>", "Series A")]
[DataRow("<first series[{N}, {#}, {ID}]>", "Series A, 1, B1")]
public void SeriesFormat_formatters(string template, string expected)
{
var bookDto = GetLibraryBook();
bookDto.Series =
[
new("Series A", 1, "B1"),
new("Series B", 6, "B2"),
new("Series C", 2, "B3")
];
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
fileTemplate
.GetFilename(bookDto, "", "", Replacements)
.PathWithoutPrefix
.Should().Be(expected);
}
[TestMethod] [TestMethod]
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)] [DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)] [DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" /> <PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" /> <PackageReference Include="MSTest.TestFramework" Version="3.8.3" />