Merge pull request #1223 from Mbucari/master
New features, including spatial audio support
This commit is contained in:
commit
8232b2b5e5
5
.cdmurls.json
Normal file
5
.cdmurls.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"CdmUrls": [
|
||||||
|
"https://ollj0gz40d.execute-api.us-west-2.amazonaws.com/default/AudibleCdm"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
.github/workflows/build-windows.yml
vendored
8
.github/workflows/build-windows.yml
vendored
@ -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"
|
||||||
|
|||||||
@ -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|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow > NextUpdateTime)
|
||||||
{
|
{
|
||||||
Updated?.Invoke(this, EventArgs.Empty);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
189
Source/AudibleUtilities/Widevine/Cdm.Api.cs
Normal file
189
Source/AudibleUtilities/Widevine/Cdm.Api.cs
Normal 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())) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
300
Source/AudibleUtilities/Widevine/Cdm.cs
Normal file
300
Source/AudibleUtilities/Widevine/Cdm.cs
Normal 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
|
||||||
|
}
|
||||||
73
Source/AudibleUtilities/Widevine/Device.cs
Normal file
73
Source/AudibleUtilities/Widevine/Device.cs
Normal 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);
|
||||||
|
}
|
||||||
15
Source/AudibleUtilities/Widevine/Extensions.cs
Normal file
15
Source/AudibleUtilities/Widevine/Extensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14552
Source/AudibleUtilities/Widevine/LicenseProtocol.cs
Normal file
14552
Source/AudibleUtilities/Widevine/LicenseProtocol.cs
Normal file
File diff suppressed because it is too large
Load Diff
70
Source/AudibleUtilities/Widevine/MpegDash.cs
Normal file
70
Source/AudibleUtilities/Widevine/MpegDash.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -43,5 +43,7 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
public void SetAudibleContributorId(string audibleContributorId)
|
||||||
|
=> AudibleContributorId = audibleContributorId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash.Count;
|
updateContributor(person, contributor);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,11 +156,11 @@ 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);
|
||||||
@ -170,182 +168,28 @@ namespace FileLiberator
|
|||||||
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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
362
Source/FileLiberator/DownloadOptions.Factory.cs
Normal file
362
Source/FileLiberator/DownloadOptions.Factory.cs
Normal 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);
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
|
||||||
<controls:LinkLabel FontWeight="Bold" Text="rmcrackan" Tapped="Link_GithubUser" />
|
|
||||||
<TextBlock Grid.Column="1" Margin="10,0" Text="Creator" />
|
|
||||||
<controls:LinkLabel Grid.Row="1" FontWeight="Bold" Text="Mbucari" Tapped="Link_GithubUser" />
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Margin="10,0" Text="Developer" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
|
|
||||||
|
|
||||||
<WrapPanel>
|
|
||||||
<WrapPanel.Styles>
|
|
||||||
<Style Selector="controls|LinkLabel">
|
<Style Selector="controls|LinkLabel">
|
||||||
<Setter Property="Margin" Value="5,0" />
|
<Setter Property="Margin" Value="5,0" />
|
||||||
<Setter Property="FontSize" Value="13" />
|
<Setter Property="FontSize" Value="13" />
|
||||||
</Style>
|
</Style>
|
||||||
</WrapPanel.Styles>
|
</StackPanel.Styles>
|
||||||
<controls:LinkLabel Text="pixil98" Tapped="Link_GithubUser" />
|
|
||||||
<controls:LinkLabel Text="hutattedonmyarm" Tapped="Link_GithubUser" />
|
<ItemsControl ItemsSource="{Binding PrimaryContributors}">
|
||||||
<controls:LinkLabel Text="seanke" Tapped="Link_GithubUser" />
|
<ItemsControl.ItemTemplate>
|
||||||
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
|
<DataTemplate>
|
||||||
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
|
<StackPanel Orientation="Horizontal">
|
||||||
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
|
<controls:LinkLabel FontWeight="Bold" Text="{Binding Name}" Tapped="ContributorLink_Tapped" />
|
||||||
<controls:LinkLabel Text="cbordeman" Tapped="Link_GithubUser" />
|
<TextBlock Grid.Column="1" Margin="10,0" Text="{Binding Type}" />
|
||||||
<controls:LinkLabel Text="jwillikers" Tapped="Link_GithubUser" />
|
</StackPanel>
|
||||||
<controls:LinkLabel Text="Shuvashish76" Tapped="Link_GithubUser" />
|
</DataTemplate>
|
||||||
<controls:LinkLabel Text="RokeJulianLockhart" Tapped="Link_GithubUser" />
|
</ItemsControl.ItemTemplate>
|
||||||
<controls:LinkLabel Text="maaximal" Tapped="Link_GithubUser" />
|
</ItemsControl>
|
||||||
<controls:LinkLabel Text="muchtall" Tapped="Link_GithubUser" />
|
|
||||||
<controls:LinkLabel Text="ScubyG" Tapped="Link_GithubUser" />
|
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
|
||||||
<controls:LinkLabel Text="patienttruth" Tapped="Link_GithubUser" />
|
|
||||||
<controls:LinkLabel Text="stickystyle" Tapped="Link_GithubUser" />
|
<ItemsControl ItemsSource="{Binding AdditionalContributors}">
|
||||||
</WrapPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<controls:LinkLabel Text="{Binding Name}" Tapped="ContributorLink_Tapped" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:GroupBox>
|
</controls:GroupBox>
|
||||||
|
|
||||||
|
|||||||
@ -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}";
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
Source/LibationFileManager/Templates/ContributorDto.cs
Normal file
44
Source/LibationFileManager/Templates/ContributorDto.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Source/LibationFileManager/Templates/IListFormat[TList].cs
Normal file
53
Source/LibationFileManager/Templates/IListFormat[TList].cs
Normal 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();
|
||||||
|
}
|
||||||
43
Source/LibationFileManager/Templates/LibraryBookDto.cs
Normal file
43
Source/LibationFileManager/Templates/LibraryBookDto.cs
Normal 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; }
|
||||||
|
}
|
||||||
33
Source/LibationFileManager/Templates/NameListFormat.cs
Normal file
33
Source/LibationFileManager/Templates/NameListFormat.cs
Normal 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();
|
||||||
|
}
|
||||||
27
Source/LibationFileManager/Templates/SeriesDto.cs
Normal file
27
Source/LibationFileManager/Templates/SeriesDto.cs
Normal 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();
|
||||||
|
}
|
||||||
17
Source/LibationFileManager/Templates/SeriesListFormat.cs
Normal file
17
Source/LibationFileManager/Templates/SeriesListFormat.cs
Normal 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();
|
||||||
|
}
|
||||||
@ -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,
|
||||||
@ -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,9 +33,9 @@ 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");
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
@ -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 },
|
||||||
@ -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 "";
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
Source/LibationUiBase/EnumDisplay[T].cs
Normal file
21
Source/LibationUiBase/EnumDisplay[T].cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
|||||||
56
Source/LibationUiBase/LibationContributor.cs
Normal file
56
Source/LibationUiBase/LibationContributor.cs
Normal 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('.', '-')}"));
|
||||||
|
}
|
||||||
127
Source/LibationWinForms/Dialogs/AboutDialog.Designer.cs
generated
127
Source/LibationWinForms/Dialogs/AboutDialog.Designer.cs
generated
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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('.', '-')}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user