mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d705c23472 | ||
|
|
de45d008c7 | ||
|
|
c267332027 | ||
|
|
4829e85faf | ||
|
|
2acb9ca7e5 | ||
|
|
b260554a2a | ||
|
|
41a4055cd9 | ||
|
|
c6e9ba9bf9 | ||
|
|
5059333b38 | ||
|
|
b4015030cf | ||
|
|
7f5cf8f018 | ||
|
|
2c9ccd9c78 | ||
|
|
cebf218db4 | ||
|
|
530b44a0e6 | ||
|
|
b3dc5a7054 | ||
|
|
2567ccb44c | ||
|
|
e67eac92fd | ||
|
|
6e84fd97f1 | ||
|
|
9a458bf3dc | ||
|
|
283a46e1e2 | ||
|
|
6ff2859c39 | ||
|
|
e8df4952fc | ||
|
|
b19e1e8a30 | ||
|
|
a3cf6ac40d | ||
|
|
ab450c37c4 | ||
|
|
c837fefbdd | ||
|
|
46b120ee41 | ||
|
|
cae8ca7ef3 | ||
|
|
904665da7f | ||
|
|
2478c61df6 | ||
|
|
288ed75b5d | ||
|
|
ad5efbd9a9 | ||
|
|
7eb7b2a0f9 | ||
|
|
d0051c0f02 | ||
|
|
d20517063e | ||
|
|
bcca69a102 | ||
|
|
35f8c05106 | ||
|
|
a3d38e082d | ||
|
|
b2e956e70b | ||
|
|
e5119357b2 | ||
|
|
b42ff827d5 | ||
|
|
68da9779da | ||
|
|
8e358d8f04 | ||
|
|
0a986238bc | ||
|
|
d636ceed8e | ||
|
|
e4fc104afe | ||
|
|
87e3075fb3 | ||
|
|
ab44823c05 | ||
|
|
2767f04621 | ||
|
|
0f1ff0aa10 | ||
|
|
c1af253300 | ||
|
|
d08962cffa | ||
|
|
7720110460 | ||
|
|
dfa5829cbd | ||
|
|
648b84ee55 | ||
|
|
6a81b9b02d | ||
|
|
c43e03b228 | ||
|
|
1de7edd9df | ||
|
|
df90094884 | ||
|
|
c9a6c8fd35 | ||
|
|
d0b78cc501 | ||
|
|
0b7bc4d938 |
@@ -1,12 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean" Version="0.1.9" />
|
||||
<PackageReference Include="Dinah.Core" Version="1.1.1.2" />
|
||||
<PackageReference Include="AAXClean" Version="0.1.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
91
AaxDecrypter/AaxcDownloadConvertBase.cs
Normal file
91
AaxDecrypter/AaxcDownloadConvertBase.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using AAXClean;
|
||||
using Dinah.Core.Net.Http;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
|
||||
{
|
||||
protected OutputFormat OutputFormat { get; }
|
||||
|
||||
protected AaxFile AaxFile;
|
||||
|
||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
||||
: base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
OutputFormat = outputFormat;
|
||||
}
|
||||
|
||||
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
|
||||
public override void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
base.SetCoverArt(coverArt);
|
||||
if (coverArt is not null)
|
||||
AaxFile?.AppleTags.SetCoverArt(coverArt);
|
||||
}
|
||||
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
AaxFile = new AaxFile(InputFileStream);
|
||||
|
||||
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
|
||||
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
|
||||
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
|
||||
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
|
||||
|
||||
return !IsCanceled;
|
||||
}
|
||||
|
||||
protected DownloadProgress Step_DownloadAudiobook_Start()
|
||||
{
|
||||
var zeroProgress = new DownloadProgress
|
||||
{
|
||||
BytesReceived = 0,
|
||||
ProgressPercentage = 0,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
};
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
|
||||
AaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
|
||||
return zeroProgress;
|
||||
}
|
||||
|
||||
protected void Step_DownloadAudiobook_End(DownloadProgress zeroProgress)
|
||||
{
|
||||
AaxFile.Close();
|
||||
|
||||
CloseInputFileStream();
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
}
|
||||
|
||||
protected void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var duration = AaxFile.Duration;
|
||||
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = 100 * progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
IsCanceled = true;
|
||||
AaxFile?.Cancel();
|
||||
AaxFile?.Dispose();
|
||||
CloseInputFileStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
using AAXClean;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadConverter : AudiobookDownloadBase
|
||||
{
|
||||
const int MAX_FILENAME_LENGTH = 255;
|
||||
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
|
||||
protected override StepSequence steps { get; }
|
||||
|
||||
private AaxFile aaxFile;
|
||||
private OutputFormat OutputFormat { get; }
|
||||
|
||||
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat, bool splitFileByChapters)
|
||||
:base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
OutputFormat = outputFormat;
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + OutputFormat,
|
||||
|
||||
["Step 1: Get Aaxc Metadata"] = Step1_GetMetadata,
|
||||
["Step 2: Download Decrypted Audiobook"] = splitFileByChapters
|
||||
? Step2_DownloadAudiobookAsMultipleFilesPerChapter
|
||||
: Step2_DownloadAudiobookAsSingleFile,
|
||||
["Step 3: Create Cue"] = splitFileByChapters
|
||||
? () => true
|
||||
: Step3_CreateCue,
|
||||
["Step 4: Cleanup"] = Step4_Cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting cover art by this method will insert the art into the audiobook metadata
|
||||
/// </summary>
|
||||
public override void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
base.SetCoverArt(coverArt);
|
||||
|
||||
aaxFile?.AppleTags.SetCoverArt(coverArt);
|
||||
}
|
||||
|
||||
protected override bool Step1_GetMetadata()
|
||||
{
|
||||
aaxFile = new AaxFile(InputFileStream);
|
||||
|
||||
OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged);
|
||||
OnRetrievedAuthors(aaxFile.AppleTags.FirstAuthor ?? "[unknown]");
|
||||
OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]");
|
||||
OnRetrievedCoverArt(aaxFile.AppleTags.Cover);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
protected override bool Step2_DownloadAudiobookAsSingleFile()
|
||||
{
|
||||
var zeroProgress = Step2_Start();
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
|
||||
var outputFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
|
||||
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
var decryptionResult = OutputFormat == OutputFormat.M4b ? aaxFile.ConvertToMp4a(outputFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outputFile);
|
||||
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
downloadLicense.ChapterInfo = aaxFile.Chapters;
|
||||
|
||||
Step2_End(zeroProgress);
|
||||
|
||||
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
|
||||
}
|
||||
|
||||
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
|
||||
{
|
||||
var zeroProgress = Step2_Start();
|
||||
|
||||
var chapters = downloadLicense.ChapterInfo.Chapters.ToList();
|
||||
|
||||
//Ensure split files are at least minChapterLength in duration.
|
||||
var splitChapters = new ChapterInfo();
|
||||
|
||||
var runningTotal = TimeSpan.Zero;
|
||||
string title = "";
|
||||
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
if (runningTotal == TimeSpan.Zero)
|
||||
title = chapters[i].Title;
|
||||
|
||||
runningTotal += chapters[i].Duration;
|
||||
|
||||
if (runningTotal >= minChapterLength)
|
||||
{
|
||||
splitChapters.AddChapter(title, runningTotal);
|
||||
runningTotal = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
if(OutputFormat == OutputFormat.M4b)
|
||||
ConvertToMultiMp4b(splitChapters);
|
||||
else
|
||||
ConvertToMultiMp3(splitChapters);
|
||||
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
Step2_End(zeroProgress);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
private DownloadProgress Step2_Start()
|
||||
{
|
||||
var zeroProgress = new DownloadProgress
|
||||
{
|
||||
BytesReceived = 0,
|
||||
ProgressPercentage = 0,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
};
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
|
||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
||||
return zeroProgress;
|
||||
}
|
||||
|
||||
private void Step2_End(DownloadProgress zeroProgress)
|
||||
{
|
||||
aaxFile.Close();
|
||||
|
||||
CloseInputFileStream();
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
}
|
||||
|
||||
private void ConvertToMultiMp4b(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
aaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
|
||||
{
|
||||
var fileName = GetMultipartFileName(outputFileName, ++chapterCount, newSplitCallback.Chapter.Title);
|
||||
if (File.Exists(fileName))
|
||||
FileExt.SafeDelete(fileName);
|
||||
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
|
||||
});
|
||||
}
|
||||
|
||||
private void ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
aaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
|
||||
{
|
||||
var fileName = GetMultipartFileName(outputFileName, ++chapterCount, newSplitCallback.Chapter.Title);
|
||||
if (File.Exists(fileName))
|
||||
FileExt.SafeDelete(fileName);
|
||||
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
|
||||
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetMultipartFileName(string baseFileName, int chapterCount, string chapterTitle)
|
||||
{
|
||||
string extension = Path.GetExtension(baseFileName);
|
||||
|
||||
var fileNameChars = $"{Path.GetFileNameWithoutExtension(baseFileName)} - {chapterCount:D2} - {chapterTitle}".ToCharArray();
|
||||
|
||||
//Replace illegal path characters with spaces.
|
||||
for (int i = 0; i <fileNameChars.Length; i++)
|
||||
{
|
||||
foreach (var illegal in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
if (fileNameChars[i] == illegal)
|
||||
{
|
||||
fileNameChars[i] = ' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileName = new string(fileNameChars).Truncate(MAX_FILENAME_LENGTH - extension.Length);
|
||||
|
||||
return Path.Combine(Path.GetDirectoryName(baseFileName), fileName + extension);
|
||||
}
|
||||
|
||||
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var duration = aaxFile.Duration;
|
||||
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = 100 * progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
aaxFile?.Cancel();
|
||||
aaxFile?.Dispose();
|
||||
CloseInputFileStream();
|
||||
}
|
||||
|
||||
protected override int GetSpeedup(TimeSpan elapsed)
|
||||
=> (int)(aaxFile.Duration.TotalSeconds / (long)elapsed.TotalSeconds);
|
||||
}
|
||||
}
|
||||
142
AaxDecrypter/AaxcDownloadMultiConverter.cs
Normal file
142
AaxDecrypter/AaxcDownloadMultiConverter.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AAXClean;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
|
||||
|
||||
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
||||
private List<string> multiPartFilePaths { get; } = new List<string>();
|
||||
|
||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat,
|
||||
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
|
||||
: base(outFileName, cacheDirectory, dlLic, outputFormat)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + OutputFormat,
|
||||
|
||||
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
|
||||
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
|
||||
["Step 3: Cleanup"] = Step_Cleanup,
|
||||
};
|
||||
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
|
||||
}
|
||||
|
||||
/*
|
||||
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
|
||||
|
||||
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
|
||||
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
|
||||
|
||||
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
|
||||
|
||||
00:00:00 - 00:00:02 | Part 1
|
||||
00:00:02 - 00:35:00 | Chapter 1
|
||||
00:35:02 - 01:02:00 | Chapter 2
|
||||
01:02:00 - 01:02:02 | Part 2
|
||||
01:02:02 - 01:41:00 | Chapter 3
|
||||
01:41:00 - 02:05:00 | Chapter 4
|
||||
|
||||
The book will be split into the following files:
|
||||
|
||||
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
|
||||
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
|
||||
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
|
||||
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
|
||||
|
||||
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
|
||||
*/
|
||||
private bool Step_DownloadAudiobookAsMultipleFilesPerChapter()
|
||||
{
|
||||
var zeroProgress = Step_DownloadAudiobook_Start();
|
||||
|
||||
var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
|
||||
|
||||
// Ensure split files are at least minChapterLength in duration.
|
||||
var splitChapters = new ChapterInfo();
|
||||
|
||||
var runningTotal = TimeSpan.Zero;
|
||||
string title = "";
|
||||
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
if (runningTotal == TimeSpan.Zero)
|
||||
title = chapters[i].Title;
|
||||
|
||||
runningTotal += chapters[i].Duration;
|
||||
|
||||
if (runningTotal >= minChapterLength)
|
||||
{
|
||||
splitChapters.AddChapter(title, runningTotal);
|
||||
runningTotal = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
// reset, just in case
|
||||
multiPartFilePaths.Clear();
|
||||
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
if (OutputFormat == OutputFormat.M4b)
|
||||
ConvertToMultiMp4a(splitChapters);
|
||||
else
|
||||
ConvertToMultiMp3(splitChapters);
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
Step_DownloadAudiobook_End(zeroProgress);
|
||||
|
||||
var success = !IsCanceled;
|
||||
|
||||
if (success)
|
||||
foreach (var path in multiPartFilePaths)
|
||||
OnFileCreated(path);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
|
||||
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback)
|
||||
);
|
||||
}
|
||||
|
||||
private void ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
|
||||
{
|
||||
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
|
||||
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
||||
{
|
||||
var fileName = multipartFileNameCallback(new()
|
||||
{
|
||||
OutputFileName = OutputFileName,
|
||||
PartsPosition = currentChapter,
|
||||
PartsTotal = splitChapters.Count,
|
||||
Title = newSplitCallback?.Chapter?.Title
|
||||
});
|
||||
fileName = FileUtility.GetValidFilename(fileName);
|
||||
|
||||
multiPartFilePaths.Add(fileName);
|
||||
|
||||
FileUtility.SaferDelete(fileName);
|
||||
|
||||
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
AaxDecrypter/AaxcDownloadSingleConverter.cs
Normal file
53
AaxDecrypter/AaxcDownloadSingleConverter.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using AAXClean;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
||||
: base(outFileName, cacheDirectory, dlLic, outputFormat)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + OutputFormat,
|
||||
|
||||
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
|
||||
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile,
|
||||
["Step 3: Create Cue"] = Step_CreateCue,
|
||||
["Step 4: Cleanup"] = Step_Cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
private bool Step_DownloadAudiobookAsSingleFile()
|
||||
{
|
||||
var zeroProgress = Step_DownloadAudiobook_Start();
|
||||
|
||||
FileUtility.SaferDelete(OutputFileName);
|
||||
|
||||
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
var decryptionResult
|
||||
= OutputFormat == OutputFormat.M4b
|
||||
? AaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo)
|
||||
: AaxFile.ConvertToMp3(outputFile);
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
DownloadLicense.ChapterInfo = AaxFile.Chapters;
|
||||
|
||||
Step_DownloadAudiobook_End(zeroProgress);
|
||||
|
||||
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
|
||||
if (success)
|
||||
base.OnFileCreated(OutputFileName);
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
@@ -21,67 +17,56 @@ namespace AaxDecrypter
|
||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
||||
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
|
||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
public event EventHandler<string> FileCreated;
|
||||
|
||||
public string AppName { get; set; }
|
||||
|
||||
protected bool isCanceled { get; set; }
|
||||
protected string outputFileName { get; }
|
||||
protected string cacheDir { get; }
|
||||
protected DownloadLicense downloadLicense { get; }
|
||||
protected bool IsCanceled { get; set; }
|
||||
protected string OutputFileName { get; private set; }
|
||||
protected DownloadLicense DownloadLicense { get; }
|
||||
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
|
||||
|
||||
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
|
||||
protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName;
|
||||
|
||||
protected abstract StepSequence steps { get; }
|
||||
protected abstract StepSequence Steps { get; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp");
|
||||
private string jsonDownloadState { get; }
|
||||
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
|
||||
|
||||
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
{
|
||||
AppName = GetType().Name;
|
||||
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||
outputFileName = outFileName;
|
||||
|
||||
var outDir = Path.GetDirectoryName(outputFileName);
|
||||
var outDir = Path.GetDirectoryName(OutputFileName);
|
||||
if (!Directory.Exists(outDir))
|
||||
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(outDir)}");
|
||||
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
|
||||
cacheDir = cacheDirectory;
|
||||
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
|
||||
jsonDownloadState = Path.Combine(cacheDirectory, Path.ChangeExtension(OutputFileName, ".json"));
|
||||
|
||||
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
|
||||
// delete file after validation is complete
|
||||
FileUtility.SaferDelete(OutputFileName);
|
||||
}
|
||||
|
||||
public abstract void Cancel();
|
||||
protected abstract int GetSpeedup(TimeSpan elapsed);
|
||||
protected abstract bool Step2_DownloadAudiobookAsSingleFile();
|
||||
protected abstract bool Step1_GetMetadata();
|
||||
|
||||
public virtual void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
if (coverArt is null) return;
|
||||
|
||||
OnRetrievedCoverArt(coverArt);
|
||||
if (coverArt is not null)
|
||||
OnRetrievedCoverArt(coverArt);
|
||||
}
|
||||
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, Elapsed) = steps.Run();
|
||||
var (IsSuccess, Elapsed) = Steps.Run();
|
||||
|
||||
if (!IsSuccess)
|
||||
{
|
||||
Console.WriteLine("WARNING-Conversion failed");
|
||||
return false;
|
||||
}
|
||||
Serilog.Log.Logger.Error("Conversion failed");
|
||||
|
||||
//Serilog.Log.Logger.Information($"Speedup is {GetSpeedup(Elapsed)}x realtime.");
|
||||
return true;
|
||||
return IsSuccess;
|
||||
}
|
||||
|
||||
protected void OnRetrievedTitle(string title)
|
||||
@@ -94,8 +79,10 @@ namespace AaxDecrypter
|
||||
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
||||
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
||||
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
|
||||
protected void OnFileCreated(string path)
|
||||
=> FileCreated?.Invoke(this, path);
|
||||
|
||||
protected void CloseInputFileStream()
|
||||
{
|
||||
@@ -103,62 +90,59 @@ namespace AaxDecrypter
|
||||
nfsPersister?.Dispose();
|
||||
}
|
||||
|
||||
protected bool Step3_CreateCue()
|
||||
protected bool Step_CreateCue()
|
||||
{
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
|
||||
var path = Path.ChangeExtension(OutputFileName, ".cue");
|
||||
path = FileUtility.GetValidFilename(path);
|
||||
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
|
||||
OnFileCreated(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step_CreateCue)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
return !IsCanceled;
|
||||
}
|
||||
|
||||
protected bool Step4_Cleanup()
|
||||
protected bool Step_Cleanup()
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
return !isCanceled;
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
FileUtility.SaferDelete(tempFile);
|
||||
return !IsCanceled;
|
||||
}
|
||||
|
||||
private NetworkFileStreamPersister OpenNetworkFileStream()
|
||||
{
|
||||
NetworkFileStreamPersister nfsp;
|
||||
if (!File.Exists(jsonDownloadState))
|
||||
return NewNetworkFilePersister();
|
||||
|
||||
if (File.Exists(jsonDownloadState))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
nfsp = new NetworkFileStreamPersister(jsonDownloadState);
|
||||
//If More than ~1 hour has elapsed since getting the download url, it will expire.
|
||||
//The new url will be to the same file.
|
||||
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
|
||||
}
|
||||
catch
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
nfsp = NewNetworkFilePersister();
|
||||
}
|
||||
var nfsp = new NetworkFileStreamPersister(jsonDownloadState);
|
||||
// If More than ~1 hour has elapsed since getting the download url, it will expire.
|
||||
// The new url will be to the same file.
|
||||
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadLicense.DownloadUrl));
|
||||
return nfsp;
|
||||
}
|
||||
else
|
||||
catch
|
||||
{
|
||||
nfsp = NewNetworkFilePersister();
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
FileUtility.SaferDelete(tempFile);
|
||||
return NewNetworkFilePersister();
|
||||
}
|
||||
return nfsp;
|
||||
}
|
||||
|
||||
private NetworkFileStreamPersister NewNetworkFilePersister()
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection
|
||||
{
|
||||
{ "User-Agent", downloadLicense.UserAgent }
|
||||
{ "User-Agent", DownloadLicense.UserAgent }
|
||||
};
|
||||
|
||||
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
var networkFileStream = new NetworkFileStream(tempFile, new Uri(DownloadLicense.DownloadUrl), 0, headers);
|
||||
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
}
|
||||
|
||||
25
AaxDecrypter/MultiConvertFileProperties.cs
Normal file
25
AaxDecrypter/MultiConvertFileProperties.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class MultiConvertFileProperties
|
||||
{
|
||||
public string OutputFileName { get; set; }
|
||||
public int PartsPosition { get; set; }
|
||||
public int PartsTotal { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
||||
public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties)
|
||||
{
|
||||
var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " };
|
||||
fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,9 +193,6 @@ namespace AaxDecrypter
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
|
||||
if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null)
|
||||
throw new WebException($"Server at {Uri.Host} does not support Http ranges");
|
||||
|
||||
//Content length is the length of the range request, and it is only equal
|
||||
//to the complete file length if requesting Range: bytes=0-
|
||||
if (WritePosition == 0)
|
||||
|
||||
@@ -1,56 +1,47 @@
|
||||
using Dinah.Core.IO;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
{
|
||||
protected override StepSequence steps { get; }
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
: base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
|
||||
steps = new StepSequence
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download Mp3 Audiobook",
|
||||
|
||||
["Step 1: Get Mp3 Metadata"] = Step1_GetMetadata,
|
||||
["Step 2: Download Audiobook"] = Step2_DownloadAudiobookAsSingleFile,
|
||||
["Step 3: Create Cue"] = Step3_CreateCue,
|
||||
["Step 4: Cleanup"] = Step4_Cleanup,
|
||||
["Step 1: Get Mp3 Metadata"] = Step_GetMetadata,
|
||||
["Step 2: Download Audiobook"] = Step_DownloadAudiobookAsSingleFile,
|
||||
["Step 3: Create Cue"] = Step_CreateCue,
|
||||
["Step 4: Cleanup"] = Step_Cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
IsCanceled = true;
|
||||
CloseInputFileStream();
|
||||
}
|
||||
|
||||
protected override int GetSpeedup(TimeSpan elapsed)
|
||||
{
|
||||
//Not implemented
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override bool Step1_GetMetadata()
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
OnRetrievedCoverArt(null);
|
||||
|
||||
return !isCanceled;
|
||||
return !IsCanceled;
|
||||
}
|
||||
|
||||
protected override bool Step2_DownloadAudiobookAsSingleFile()
|
||||
private bool Step_DownloadAudiobookAsSingleFile()
|
||||
{
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
//MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
// MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
|
||||
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
|
||||
{
|
||||
@@ -75,12 +66,11 @@ namespace AaxDecrypter
|
||||
|
||||
CloseInputFileStream();
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName);
|
||||
SetOutputFileName(realOutputFileName);
|
||||
OnFileCreated(realOutputFileName);
|
||||
|
||||
FileExt.SafeMove(InputFileStream.SaveFilePath, outputFileName);
|
||||
|
||||
return !isCanceled;
|
||||
return !IsCanceled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>6.2.3.0</Version>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>6.6.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
@@ -50,15 +50,48 @@ namespace AppScaffolding
|
||||
public static void RunPostConfigMigrations(Configuration config)
|
||||
{
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
PopulateMissingConfigValues(config);
|
||||
|
||||
//
|
||||
// migrations go below here
|
||||
//
|
||||
|
||||
Migrations.migrate_to_v5_2_0__post_config(config);
|
||||
Migrations.migrate_to_v5_7_1(config);
|
||||
Migrations.migrate_to_v6_1_2(config);
|
||||
Migrations.migrate_to_v6_2_0(config);
|
||||
Migrations.migrate_to_v6_5_2(config);
|
||||
}
|
||||
|
||||
public static void PopulateMissingConfigValues(Configuration config)
|
||||
{
|
||||
config.InProgress ??= Configuration.WinTemp;
|
||||
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!config.Exists(nameof(config.DecryptToLossy)))
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||
config.ImportEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.SplitFilesByChapter)))
|
||||
config.SplitFilesByChapter = false;
|
||||
|
||||
if (!config.Exists(nameof(config.FolderTemplate)))
|
||||
config.FolderTemplate = Templates.Folder.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.FileTemplate)))
|
||||
config.FileTemplate = Templates.File.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.ChapterFileTemplate)))
|
||||
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.ShowImportedStats)))
|
||||
config.ShowImportedStats = true;
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Run after migration</summary>
|
||||
@@ -211,11 +244,11 @@ namespace AppScaffolding
|
||||
|
||||
config.InProgress,
|
||||
|
||||
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
|
||||
AudibleFileStorage.DownloadsInProgressDirectory,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
|
||||
|
||||
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
||||
AudibleFileStorage.DecryptInProgressDirectory,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,39 +344,11 @@ namespace AppScaffolding
|
||||
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
|
||||
_ => path
|
||||
};
|
||||
|
||||
public static void migrate_to_v5_2_0__post_config(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!config.Exists(nameof(config.DecryptToLossy)))
|
||||
config.DecryptToLossy = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
// add config.BadBook
|
||||
public static void migrate_to_v5_7_1(Configuration config)
|
||||
public static void migrate_to_v6_5_2(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
}
|
||||
|
||||
// add config.DownloadEpisodes , config.ImportEpisodes
|
||||
public static void migrate_to_v6_1_2(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||
config.ImportEpisodes = true;
|
||||
}
|
||||
|
||||
// add config.SplitFilesByChapter
|
||||
public static void migrate_to_v6_2_0(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.SplitFilesByChapter)))
|
||||
config.SplitFilesByChapter = false;
|
||||
// example
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="27.1.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.4" />
|
||||
<PackageReference Include="CsvHelper" Version="27.2.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
|
||||
@@ -3,10 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
@@ -43,7 +44,7 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(FileManager.Configuration.Instance.LibationFiles);
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
@@ -98,7 +99,7 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(FileManager.Configuration.Instance.LibationFiles);
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
@@ -155,7 +156,7 @@ namespace ApplicationServices
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, FileManager.Configuration.Instance.ImportEpisodes);
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, Configuration.Instance.ImportEpisodes);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
@@ -166,8 +167,8 @@ namespace ApplicationServices
|
||||
{
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
|
||||
var libraryBookImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
var qtyChanges = context.SaveChanges();
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
@@ -228,6 +229,7 @@ namespace ApplicationServices
|
||||
if (qtyChanges > 0)
|
||||
{
|
||||
SearchEngineCommands.UpdateLiberatedStatus(book);
|
||||
SearchEngineCommands.UpdateBookTags(book);
|
||||
BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
|
||||
}
|
||||
|
||||
@@ -244,7 +246,7 @@ namespace ApplicationServices
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists ? book.UserDefinedItem.BookStatus
|
||||
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
|
||||
|
||||
@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class Account : IUpdatable
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
|
||||
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):
|
||||
@@ -3,7 +3,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
|
||||
{
|
||||
@@ -8,7 +8,7 @@ using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||
public class ApiExtended
|
||||
@@ -166,8 +166,7 @@ namespace InternalUtilities
|
||||
|
||||
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
|
||||
|
||||
// remove episode parents. even if the following stuff fails, these will still be removed from the collection.
|
||||
// also must happen before processing children because children abuses this flag
|
||||
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
if (importEpisodes)
|
||||
@@ -192,6 +191,14 @@ namespace InternalUtilities
|
||||
{
|
||||
var children = await getEpisodeChildrenAsync(parent);
|
||||
|
||||
// actual individual episode, not the parent of a series.
|
||||
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
|
||||
if (!children.Any())
|
||||
{
|
||||
results.Add(parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public static class AudibleApiStorage
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public interface IValidator
|
||||
{
|
||||
15
AudibleUtilities/AudibleUtilities.csproj
Normal file
15
AudibleUtilities/AudibleUtilities.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.5.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
1
AudibleUtilities/_InternalsVisible.cs
Normal file
1
AudibleUtilities/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(AudibleUtilities) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -12,20 +12,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.5.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.0.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -185,6 +185,9 @@ namespace DataLayer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_seriesLink is null)
|
||||
return "";
|
||||
|
||||
// first: alphabetical by name
|
||||
var withNames = _seriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace DataLayer
|
||||
|
||||
// import previously saved tags
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
|
||||
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
Tags = LibationFileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
#region Tags
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace DataLayer
|
||||
.Select(t => (t.Book.AudibleProductId, t.Tags))
|
||||
.ToList();
|
||||
|
||||
FileManager.TagsPersistence.Save(tagsCollection);
|
||||
LibationFileManager.TagsPersistence.Save(tagsCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer.Utilities
|
||||
{
|
||||
public static class LocalDatabaseInfo
|
||||
{
|
||||
public static List<string> GetLocalDBInstances()
|
||||
{
|
||||
// Start the child process.
|
||||
using var p = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/C sqllocaldb info",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
|
||||
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
|
||||
? new List<string>()
|
||||
: output
|
||||
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -68,7 +68,7 @@ namespace DtoImporterService
|
||||
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
|
||||
var book = DbContext.Books.Local.FirstOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
|
||||
if (book is null)
|
||||
{
|
||||
book = createNewBook(item);
|
||||
@@ -120,17 +120,36 @@ namespace DtoImporterService
|
||||
|
||||
var category = DbContext.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory);
|
||||
|
||||
var book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
category,
|
||||
importItem.LocaleName)
|
||||
).Entity;
|
||||
Book book;
|
||||
try
|
||||
{
|
||||
book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
category,
|
||||
importItem.LocaleName)
|
||||
).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding book. {@DebugInfo}", new {
|
||||
item.ProductId,
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
QtyAuthors = authors?.Count,
|
||||
QtyNarrators = narrators?.Count,
|
||||
Category = category?.Name,
|
||||
importItem.LocaleName
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
@@ -164,7 +183,7 @@ namespace DtoImporterService
|
||||
{
|
||||
foreach (var seriesEntry in item.Series)
|
||||
{
|
||||
var series = DbContext.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
|
||||
var series = DbContext.Series.Local.FirstOrDefault(s => seriesEntry.SeriesId == s.AudibleSeriesId);
|
||||
book.UpsertSeries(series, seriesEntry.Sequence);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -73,7 +73,15 @@ namespace DtoImporterService
|
||||
var category = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == id);
|
||||
if (category is null)
|
||||
{
|
||||
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
|
||||
try
|
||||
{
|
||||
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
|
||||
throw;
|
||||
}
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -83,7 +83,16 @@ namespace DtoImporterService
|
||||
foreach (var name in newPeople)
|
||||
{
|
||||
var p = groupby.Single(g => g.Name == name).People.First();
|
||||
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding person. {@DebugInfo}", new { p?.Name, p?.Asin });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return newPeople.Count;
|
||||
@@ -99,7 +108,17 @@ namespace DtoImporterService
|
||||
.ToList();
|
||||
|
||||
foreach (var pub in newPublishers)
|
||||
DbContext.Contributors.Add(new Contributor(pub));
|
||||
{
|
||||
try
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(pub));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding publisher. {@DebugInfo}", new { pub });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return newPublishers.Count;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -35,18 +35,38 @@ namespace DtoImporterService
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
|
||||
var newItems = importItems
|
||||
.Where(dto => !currentLibraryProductIds
|
||||
.Contains(dto.DtoItem.ProductId))
|
||||
.ToList();
|
||||
|
||||
foreach (var newItem in newItems)
|
||||
// if 2 accounts try to import the same book in the same transaction: error since we're only tracking and pulling by asin.
|
||||
// just use the first
|
||||
var groupby = newItems.GroupBy(
|
||||
i => i.DtoItem.ProductId,
|
||||
i => i,
|
||||
(key, g) => new { ProductId = key, ImportItems = g.ToList() }
|
||||
)
|
||||
.ToList();
|
||||
foreach (var gb in groupby)
|
||||
{
|
||||
var newItem = gb.ImportItems.First();
|
||||
|
||||
var libraryBook = new LibraryBook(
|
||||
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId);
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
}
|
||||
}
|
||||
|
||||
var qtyNew = newItems.Count;
|
||||
var qtyNew = groupby.Count;
|
||||
return qtyNew;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -51,7 +51,15 @@ namespace DtoImporterService
|
||||
var series = DbContext.Series.Local.FirstOrDefault(c => c.AudibleSeriesId == s.SeriesId);
|
||||
if (series is null)
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
try
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { s?.SeriesId });
|
||||
throw;
|
||||
}
|
||||
qtyNew++;
|
||||
}
|
||||
series.UpdateName(s.SeriesName);
|
||||
|
||||
56
FileLiberator/AudioFileStorageExt.cs
Normal file
56
FileLiberator/AudioFileStorageExt.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public static class AudioFileStorageExt
|
||||
{
|
||||
private class MultipartRenamer
|
||||
{
|
||||
private LibraryBook libraryBook { get; }
|
||||
|
||||
internal MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
|
||||
internal string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(libraryBook.ToDto(), props);
|
||||
}
|
||||
|
||||
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
=> new MultipartRenamer(libraryBook).MultipartFilename;
|
||||
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// File path for where to move files into.
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
=> Templates.Folder.GetFilename(libraryBook.ToDto());
|
||||
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// Path: in progress directory.
|
||||
/// File name: final file name.
|
||||
/// </summary>
|
||||
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension);
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file does not exist
|
||||
/// </summary>
|
||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file already exists
|
||||
/// </summary>
|
||||
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using AAXClean;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AAXClean;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -17,7 +16,7 @@ namespace FileLiberator
|
||||
private Mp4File m4bBook;
|
||||
|
||||
private long fileSize;
|
||||
private static string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||
|
||||
public override void Cancel() => m4bBook?.Cancel();
|
||||
|
||||
@@ -52,9 +51,9 @@ namespace FileLiberator
|
||||
m4bBook.InputStream.Close();
|
||||
mp3File.Close();
|
||||
|
||||
var mp3Path = Mp3FileName(m4bPath);
|
||||
|
||||
FileExt.SafeMove(mp3File.Name, mp3Path);
|
||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -16,8 +17,27 @@ namespace FileLiberator
|
||||
{
|
||||
private AudiobookDownloadBase abDownloader;
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
|
||||
|
||||
public override void Cancel() => abDownloader?.Cancel();
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var entries = new List<FilePathCache.CacheEntry>();
|
||||
// these only work so minimally b/c CacheEntry is a record.
|
||||
// in case of parallel decrypts, only capture the ones for this book id.
|
||||
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
|
||||
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Add(e);
|
||||
}
|
||||
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Remove(e);
|
||||
}
|
||||
|
||||
OnBegin(libraryBook);
|
||||
|
||||
try
|
||||
@@ -25,16 +45,29 @@ namespace FileLiberator
|
||||
if (libraryBook.Book.Audio_Exists)
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await downloadAudiobookAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
FilePathCache.Inserted += FilePathCache_Inserted;
|
||||
FilePathCache.Removed += FilePathCache_Removed;
|
||||
|
||||
success = await downloadAudiobookAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FilePathCache.Inserted -= FilePathCache_Inserted;
|
||||
FilePathCache.Removed -= FilePathCache_Removed;
|
||||
}
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename is null)
|
||||
if (!success)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
// moves files and returns dest dir
|
||||
var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
// moves new files from temp dir to final dest
|
||||
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
|
||||
|
||||
if (!moveResults.movedAudioFile)
|
||||
// decrypt failed
|
||||
if (!movedAudioFile)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
@@ -47,13 +80,13 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> downloadAudiobookAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
|
||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||
{
|
||||
OnStreamingBegin($"Begin decrypting {libraryBook}");
|
||||
|
||||
try
|
||||
{
|
||||
validate(libraryBook);
|
||||
downloadValidation(libraryBook);
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
@@ -81,28 +114,29 @@ namespace FileLiberator
|
||||
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
||||
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
||||
}
|
||||
|
||||
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
|
||||
|
||||
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm
|
||||
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
|
||||
: new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
|
||||
abDownloader.AppName = "Libation";
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, outputFormat.ToString().ToLower());
|
||||
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
|
||||
abDownloader
|
||||
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
|
||||
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
|
||||
outFileName, cacheDir, audiobookDlLic, outputFormat,
|
||||
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)
|
||||
)
|
||||
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
|
||||
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
|
||||
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
|
||||
abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title);
|
||||
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
|
||||
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(abDownloader.Run);
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
return outFileName;
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -110,76 +144,7 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
{
|
||||
if (e is null && Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
OnRequestCoverArt(abDownloader.SetCoverArt);
|
||||
}
|
||||
|
||||
if (e is not null)
|
||||
{
|
||||
OnCoverImageDiscovered(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
// TODO make this method handle multiple audio files or a single audio file.
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
|
||||
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
|
||||
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
|
||||
|
||||
bool movedAudioFile = false;
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest
|
||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)//f.Extension.Equals($".{musicFileExt}", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.Join(destinationDir, f.Name)
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||
Cue.UpdateFileName(f, audioFileName);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
|
||||
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
|
||||
}
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return (destinationDir, movedAudioFile);
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
var m4bDir = new FileInfo(outputAudioFilename).Directory;
|
||||
var files = m4bDir
|
||||
.EnumerateFiles()
|
||||
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
|
||||
.ToList();
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
var sortedFiles = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
|
||||
return sortedFiles;
|
||||
}
|
||||
|
||||
private static void validate(LibraryBook libraryBook)
|
||||
private static void downloadValidation(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
@@ -201,11 +166,45 @@ namespace FileLiberator
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
|
||||
|
||||
public override void Cancel()
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
{
|
||||
abDownloader?.Cancel();
|
||||
if (e is not null)
|
||||
OnCoverImageDiscovered(e);
|
||||
else if (Configuration.Instance.AllowLibationFixup)
|
||||
OnRequestCoverArt(abDownloader.SetCoverArt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
|
||||
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
if (getFirstAudio() == default)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
||||
|
||||
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace FileLiberator
|
||||
try
|
||||
{
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
OnFileCreated("Upgrade", actualDownloadedFilePath);
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -8,6 +8,7 @@ using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -39,19 +40,14 @@ namespace FileLiberator
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
var extension = Path.GetExtension(getdownloadUrl(libraryBook));
|
||||
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
if (existingPath is not null)
|
||||
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, existingPath, extension);
|
||||
|
||||
if (existingPath != null)
|
||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||
|
||||
var full = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.PdfStorageDirectory,
|
||||
libraryBook.Book.Title,
|
||||
Path.GetExtension(file),
|
||||
libraryBook.Book.AudibleProductId);
|
||||
return full;
|
||||
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
|
||||
}
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
@@ -72,6 +68,8 @@ namespace FileLiberator
|
||||
var client = new HttpClient();
|
||||
|
||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||
OnFileCreated(libraryBook, actualDownloadedFilePath);
|
||||
|
||||
OnStatusUpdate(actualDownloadedFilePath);
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace FileLiberator
|
||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook =>
|
||||
Validate(libraryBook)
|
||||
&& (libraryBook.Book.ContentType != ContentType.Episode || FileManager.Configuration.Instance.DownloadEpisodes)
|
||||
&& (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace FileLiberator
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||
public event EventHandler<(string id, string path)> FileCreated;
|
||||
|
||||
protected void OnStreamingBegin(string filePath)
|
||||
{
|
||||
@@ -30,8 +32,14 @@ namespace FileLiberator
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = filePath });
|
||||
StreamingCompleted?.Invoke(this, filePath);
|
||||
}
|
||||
|
||||
//TODO: Update file cache
|
||||
protected void OnFileCreated(DataLayer.LibraryBook libraryBook, string path) => OnFileCreated(libraryBook.Book.AudibleProductId, path);
|
||||
protected void OnFileCreated(string id, string path)
|
||||
{
|
||||
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path });
|
||||
LibationFileManager.FilePathCache.Insert(id, path);
|
||||
FileCreated?.Invoke(this, (id, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -19,8 +20,24 @@ namespace FileLiberator
|
||||
|
||||
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||
{
|
||||
var apiExtended = await InternalUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
return apiExtended.Api;
|
||||
}
|
||||
|
||||
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
|
||||
{
|
||||
Account = libraryBook.Account,
|
||||
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title ?? "",
|
||||
Locale = libraryBook.Book.Locale,
|
||||
|
||||
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
|
||||
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
|
||||
|
||||
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1
FileLiberator/_InternalsVisible.cs
Normal file
1
FileLiberator/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileLiberator) + ".Tests")]
|
||||
@@ -1,149 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public enum FileType { Unknown, Audio, AAXC, PDF }
|
||||
|
||||
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
protected abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||
|
||||
public static string PdfStorageDirectory => BooksDirectory;
|
||||
|
||||
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
||||
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books");
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
private static object bookDirectoryFilesLocker { get; } = new();
|
||||
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
protected IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
protected string GetFilePath(string productId)
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase);
|
||||
|
||||
string firstOrNull;
|
||||
|
||||
if (StorageDirectory == BooksDirectory)
|
||||
{
|
||||
//If user changed the BooksDirectory, reinitialize.
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
lock (bookDirectoryFilesLocker)
|
||||
{
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstOrNull = BookDirectoryFiles.FindFile(regex);
|
||||
}
|
||||
else
|
||||
{
|
||||
firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
if (firstOrNull is null)
|
||||
return null;
|
||||
|
||||
FilePathCache.Upsert(productId, FileType, firstOrNull);
|
||||
return firstOrNull;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
public string GetDestDir(string title, string asin)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? title
|
||||
: title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
||||
|
||||
public string GetPath(string productId) => GetFilePath(productId);
|
||||
}
|
||||
|
||||
public class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
protected override string[] Extensions { get; } = new[] { "aaxc" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => DownloadsInProgress;
|
||||
|
||||
public AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
|
||||
/// <summary>
|
||||
/// Example for full books:
|
||||
/// Search recursively in _books directory. Full book exists if either are true
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,9 @@ using System.Threading.Tasks;
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
|
||||
///
|
||||
/// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
|
||||
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
|
||||
/// this state and download again.
|
||||
/// Tracks actual locations of files.
|
||||
/// </summary>
|
||||
internal class BackgroundFileSystem
|
||||
public class BackgroundFileSystem
|
||||
{
|
||||
public string RootDirectory { get; private set; }
|
||||
public string SearchPattern { get; private set; }
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="1.1.1.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
<PackageReference Include="Dinah.Core" Version="4.0.3.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
64
FileManager/FileNamingTemplate.cs
Normal file
64
FileManager/FileNamingTemplate.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
||||
public class FileNamingTemplate
|
||||
{
|
||||
/// <summary>Proposed full file path. May contain optional html-styled template tags. Eg: <name></summary>
|
||||
public string Template { get; }
|
||||
|
||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileNamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
|
||||
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
||||
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>Convenience method</summary>
|
||||
public void AddParameterReplacement(string key, object value)
|
||||
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
|
||||
=> ParameterReplacements.Add(key, value);
|
||||
|
||||
/// <summary>If set, truncate each parameter replacement to this many characters. Default 50</summary>
|
||||
public int? ParameterMaxSize { get; set; } = 50;
|
||||
|
||||
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
|
||||
public string IllegalCharacterReplacements { get; set; }
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
public string GetFilePath()
|
||||
{
|
||||
var filename = Template;
|
||||
|
||||
foreach (var r in ParameterReplacements)
|
||||
filename = filename.Replace($"<{formatKey(r.Key)}>", formatValue(r.Value));
|
||||
|
||||
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
|
||||
}
|
||||
|
||||
private static string formatKey(string key)
|
||||
=> key
|
||||
.Replace("<", "")
|
||||
.Replace(">", "");
|
||||
|
||||
private string formatValue(object value)
|
||||
{
|
||||
if (value is null)
|
||||
return "";
|
||||
|
||||
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
||||
// Esp important for file templates.
|
||||
var val = value
|
||||
.ToString()
|
||||
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
||||
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
||||
return
|
||||
ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
|
||||
? val.Truncate(ParameterMaxSize.Value)
|
||||
: val;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileUtility
|
||||
{
|
||||
public static string GetValidFilename(string dirFullPath, string filename, string extension, params string[] metadataSuffixes)
|
||||
/// <summary>
|
||||
/// "txt" => ".txt"
|
||||
/// <br />".txt" => ".txt"
|
||||
/// <br />null or whitespace => ""
|
||||
/// </summary>
|
||||
public static string GetStandardizedExtension(string extension)
|
||||
=> string.IsNullOrWhiteSpace(extension)
|
||||
? (extension ?? "")?.Trim()
|
||||
: '.' + extension.Trim().Trim('.');
|
||||
|
||||
/// <summary>
|
||||
/// Return position with correct number of leading zeros.
|
||||
/// <br />- 2 of 9 => "2"
|
||||
/// <br />- 2 of 90 => "02"
|
||||
/// <br />- 2 of 900 => "002"
|
||||
/// </summary>
|
||||
/// <param name="position">position in sequence. The 'x' in 'x of y'</param>
|
||||
/// <param name="total">total qty in sequence. The 'y' in 'x of y'</param>
|
||||
public static string GetSequenceFormatted(int position, int total)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dirFullPath))
|
||||
throw new ArgumentException($"{nameof(dirFullPath)} may not be null or whitespace", nameof(dirFullPath));
|
||||
ArgumentValidator.EnsureGreaterThan(position, nameof(position), 0);
|
||||
ArgumentValidator.EnsureGreaterThan(total, nameof(total), 0);
|
||||
if (position > total)
|
||||
throw new ArgumentException($"{position} may not be greater than {total}");
|
||||
|
||||
// file max length = 255. dir max len = 247
|
||||
return position.ToString().PadLeft(total.ToString().Length, '0');
|
||||
}
|
||||
|
||||
// sanitize
|
||||
filename = getAsciiTag(filename);
|
||||
// manage length
|
||||
if (filename.Length > 50)
|
||||
filename = filename.Substring(0, 50) + "[...]";
|
||||
private const int MAX_FILENAME_LENGTH = 255;
|
||||
private const int MAX_DIRECTORY_LENGTH = 247;
|
||||
|
||||
// append id. it is 10 or 14 char in the common cases
|
||||
if (metadataSuffixes != null && metadataSuffixes.Length > 0)
|
||||
filename += " [" + string.Join("][", metadataSuffixes) + "]";
|
||||
/// <summary>
|
||||
/// Ensure valid file name path:
|
||||
/// <br/>- remove invalid chars
|
||||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
public static string GetValidFilename(string path, string illegalCharacterReplacements = "")
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
// this method may also be used for directory names, so no guarantee of extension
|
||||
if (!string.IsNullOrWhiteSpace(extension))
|
||||
extension = '.' + extension.Trim('.');
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, illegalCharacterReplacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
dir = dir.Truncate(MAX_DIRECTORY_LENGTH);
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
var fileStem = Path.Combine(dir, filename);
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
var fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - extension.Length) + extension;
|
||||
|
||||
fullfilename = removeInvalidWhitespace(fullfilename);
|
||||
|
||||
// ensure uniqueness
|
||||
var fullfilename = Path.Combine(dirFullPath, filename + extension);
|
||||
var i = 0;
|
||||
while (File.Exists(fullfilename))
|
||||
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
|
||||
{
|
||||
var increm = $" ({++i})";
|
||||
fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension;
|
||||
}
|
||||
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
private static string getAsciiTag(string property)
|
||||
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
|
||||
|
||||
/// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
|
||||
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
|
||||
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
||||
|
||||
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||
public static string GetSafePath(string path, string illegalCharacterReplacements = "")
|
||||
{
|
||||
if (property == null)
|
||||
return "";
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
// omit characters which are invalid. EXCEPTION: change colon to underscore
|
||||
property = property.Replace(':', '_');
|
||||
path = replaceInvalidChars(path, illegalCharacterReplacements);
|
||||
path = standardizeSlashes(path);
|
||||
path = replaceColons(path, illegalCharacterReplacements);
|
||||
path = removeDoubleSlashes(path);
|
||||
|
||||
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
|
||||
foreach (var ch in Path.GetInvalidFileNameChars())
|
||||
property = property.Replace(ch.ToString(), "");
|
||||
return property;
|
||||
return path;
|
||||
}
|
||||
|
||||
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?',
|
||||
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
|
||||
// In live code, Path.GetInvalidPathChars() does not include them
|
||||
'"', '<', '>'
|
||||
}).ToArray();
|
||||
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
||||
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
||||
|
||||
private static string standardizeSlashes(string path)
|
||||
=> path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
private static string replaceColons(string path, string illegalCharacterReplacements)
|
||||
{
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < path.Length; i++)
|
||||
{
|
||||
var c = path[i];
|
||||
if (i >= 2 && c == ':')
|
||||
builder.Append(illegalCharacterReplacements);
|
||||
else
|
||||
builder.Append(c);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string removeDoubleSlashes(string path)
|
||||
{
|
||||
if (path.Length < 2)
|
||||
return path;
|
||||
|
||||
// exception: don't try to condense the initial dbl bk slashes in a path. eg: \\192.168.0.1
|
||||
|
||||
var remainder = path[1..];
|
||||
var dblSeparator = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
||||
while (remainder.Contains(dblSeparator))
|
||||
remainder = remainder.Replace(dblSeparator, $"{Path.DirectorySeparatorChar}");
|
||||
|
||||
return path[0] + remainder;
|
||||
}
|
||||
|
||||
private static string removeInvalidWhitespace_pattern { get; } = $@"\s*\{Path.DirectorySeparatorChar}\s*";
|
||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
/// <summary>no part of the path may begin or end in whitespace</summary>
|
||||
private static string removeInvalidWhitespace(string fullfilename)
|
||||
{
|
||||
// no whitespace at beginning or end
|
||||
// replace whitespace around path slashes
|
||||
// regex (with space added for clarity)
|
||||
// \s* \\ \s* => \
|
||||
fullfilename = fullfilename.Trim();
|
||||
|
||||
fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, @"\");
|
||||
|
||||
fullfilename = removeDoubleSlashes(fullfilename);
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move file.
|
||||
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
|
||||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
public static string SaferMoveToValidPath(string source, string destination)
|
||||
{
|
||||
destination = GetValidFilename(destination);
|
||||
SaferMove(source, destination);
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static int maxRetryAttempts { get; } = 3;
|
||||
private static TimeSpan pauseBetweenFailures { get; } = TimeSpan.FromMilliseconds(100);
|
||||
private static RetryPolicy retryPolicy { get; } =
|
||||
Policy
|
||||
.Handle<Exception>()
|
||||
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
|
||||
|
||||
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||
public static void SaferDelete(string source)
|
||||
=> retryPolicy.Execute(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(source))
|
||||
{
|
||||
File.Delete(source);
|
||||
Serilog.Log.Logger.Information("File successfully deleted", new { source });
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Serilog.Log.Logger.Error(e, "Failed to delete file", new { source });
|
||||
throw;
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||
public static void SaferMove(string source, string destination)
|
||||
=> retryPolicy.Execute(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(source))
|
||||
{
|
||||
SaferDelete(destination);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
||||
File.Move(source, destination);
|
||||
Serilog.Log.Logger.Information("File successfully moved", new { source, destination });
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Serilog.Log.Logger.Error(e, "Failed to move file", new { source, destination });
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ namespace FileManager
|
||||
if (IsReadOnly)
|
||||
return;
|
||||
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
createNewFile();
|
||||
}
|
||||
|
||||
public string GetString(string propertyName)
|
||||
@@ -205,9 +204,39 @@ namespace FileManager
|
||||
|
||||
private JObject readFile()
|
||||
{
|
||||
if (!File.Exists(Filepath))
|
||||
{
|
||||
var msg = "Unrecoverable error. Settings file cannot be found";
|
||||
var ex = new FileNotFoundException(msg, Filepath);
|
||||
Serilog.Log.Logger.Error(msg, ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
var settingsJsonContents = File.ReadAllText(Filepath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settingsJsonContents))
|
||||
{
|
||||
createNewFile();
|
||||
settingsJsonContents = File.ReadAllText(Filepath);
|
||||
}
|
||||
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(settingsJsonContents);
|
||||
|
||||
if (jObject is null)
|
||||
{
|
||||
var msg = "Unrecoverable error. Unable to read settings from Settings file";
|
||||
var ex = new NullReferenceException(msg);
|
||||
Serilog.Log.Logger.Error(msg, ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return jObject;
|
||||
}
|
||||
|
||||
private void createNewFile()
|
||||
{
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
FileManager/_InternalsVisible.cs
Normal file
1
FileManager/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileManager) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
50
Libation.sln
50
Libation.sln
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28803.156
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -23,13 +23,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 Utilities (domain ignoran
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AaxDecrypter", "AaxDecrypter\AaxDecrypter.csproj", "{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager", "LibationFileManager\LibationFileManager.csproj", "{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\DataLayer.csproj", "{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities", "AudibleUtilities\AudibleUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
|
||||
EndProject
|
||||
@@ -46,8 +46,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "Appl
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
||||
@@ -56,6 +54,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities.Tests", "_Tests\AudibleUtilities.Tests\AudibleUtilities.Tests.csproj", "{788294BE-0D8E-40D4-9CEE-67896FBB52CE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tests\FileLiberator.Tests\FileLiberator.Tests.csproj", "{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests\FileManager.Tests\FileManager.Tests.csproj", "{F2E04270-4551-41C4-99FF-E7125BED708C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -98,10 +106,6 @@ Global
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -118,6 +122,26 @@ Global
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -132,11 +156,15 @@ Global
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
106
LibationFileManager/AudibleFileStorage.cs
Normal file
106
LibationFileManager/AudibleFileStorage.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public abstract class AudibleFileStorage
|
||||
{
|
||||
protected abstract string GetFilePathCustom(string productId);
|
||||
|
||||
#region static
|
||||
public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||
public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||
|
||||
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
||||
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books");
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
private FileType FileType { get; }
|
||||
private string regexTemplate { get; }
|
||||
|
||||
protected AudibleFileStorage(FileType fileType)
|
||||
{
|
||||
FileType = fileType;
|
||||
|
||||
var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}");
|
||||
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
|
||||
}
|
||||
|
||||
protected string GetFilePath(string productId)
|
||||
{
|
||||
// primary lookup
|
||||
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
||||
if (cachedFile is not null && File.Exists(cachedFile))
|
||||
return cachedFile;
|
||||
|
||||
// secondary lookup attempt
|
||||
var firstOrNull = GetFilePathCustom(productId);
|
||||
if (firstOrNull is not null)
|
||||
FilePathCache.Insert(productId, firstOrNull);
|
||||
|
||||
return firstOrNull;
|
||||
}
|
||||
|
||||
protected Regex GetBookSearchRegex(string productId)
|
||||
{
|
||||
var pattern = string.Format(regexTemplate, productId);
|
||||
return new Regex(pattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
|
||||
protected override string GetFilePathCustom(string productId)
|
||||
{
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return Directory
|
||||
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
internal AudioFileStorage() : base(FileType.Audio)
|
||||
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
private static object bookDirectoryFilesLocker { get; } = new();
|
||||
protected override string GetFilePathCustom(string productId)
|
||||
{
|
||||
// If user changed the BooksDirectory: reinitialize
|
||||
lock (bookDirectoryFilesLocker)
|
||||
if (BooksDirectory != BookDirectoryFiles.RootDirectory)
|
||||
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return BookDirectoryFiles.FindFile(regex);
|
||||
}
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
public string GetPath(string productId) => GetFilePath(productId);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
@@ -132,6 +133,13 @@ namespace FileManager
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
@@ -144,8 +152,40 @@ namespace FileManager
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
persistentDictionary.SetString(settingName, template);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region known directories
|
||||
@@ -5,17 +5,16 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Immutable;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class FilePathCache
|
||||
{
|
||||
{
|
||||
public record CacheEntry(string Id, FileType FileType, string Path);
|
||||
|
||||
private const string FILENAME = "FileLocations.json";
|
||||
internal class CacheEntry
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public FileType FileType { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
public static event EventHandler<CacheEntry> Inserted;
|
||||
public static event EventHandler<CacheEntry> Removed;
|
||||
|
||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
@@ -31,42 +30,51 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
|
||||
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) != null;
|
||||
|
||||
public static string GetPath(string id, FileType type)
|
||||
{
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
public static List<(FileType fileType, string path)> GetFiles(string id)
|
||||
=> getEntries(entry => entry.Id == id)
|
||||
.Select(entry => (entry.FileType, entry.Path))
|
||||
.ToList();
|
||||
|
||||
if (entry == null)
|
||||
return null;
|
||||
public static string GetFirstPath(string id, FileType type)
|
||||
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
||||
?.FirstOrDefault()
|
||||
?.Path;
|
||||
|
||||
if (!File.Exists(entry.Path))
|
||||
{
|
||||
remove(entry);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.Path;
|
||||
}
|
||||
|
||||
private static void remove(CacheEntry entry)
|
||||
private static List<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
|
||||
{
|
||||
cache.Remove(entry);
|
||||
save();
|
||||
var entries = cache.Where(predicate).ToList();
|
||||
if (entries is null || !entries.Any())
|
||||
return null;
|
||||
|
||||
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static void Upsert(string id, FileType type, string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException("Cannot add path to cache. File not found");
|
||||
private static void remove(List<CacheEntry> entries)
|
||||
{
|
||||
if (entries is null)
|
||||
return;
|
||||
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
|
||||
if (entry is null)
|
||||
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
|
||||
else
|
||||
entry.Path = path;
|
||||
lock (locker)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
cache.Remove(entry);
|
||||
Removed?.Invoke(null, entry);
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Insert(string id, string path)
|
||||
{
|
||||
var type = FileTypes.GetFileTypeFromPath(path);
|
||||
var entry = new CacheEntry(id, type, path);
|
||||
cache.Add(entry);
|
||||
Inserted?.Invoke(null, entry);
|
||||
save();
|
||||
}
|
||||
|
||||
39
LibationFileManager/FileTypes.cs
Normal file
39
LibationFileManager/FileTypes.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public enum FileType { Unknown, Audio, AAXC, PDF, Zip, Cue }
|
||||
|
||||
public static class FileTypes
|
||||
{
|
||||
private static Dictionary<string, FileType> dic => new()
|
||||
{
|
||||
["aaxc"] = FileType.AAXC,
|
||||
["cue"] = FileType.Cue,
|
||||
["pdf"] = FileType.PDF,
|
||||
["zip"] = FileType.Zip,
|
||||
|
||||
["aac"] = FileType.Audio,
|
||||
["flac"] = FileType.Audio,
|
||||
["m4a"] = FileType.Audio,
|
||||
["m4b"] = FileType.Audio,
|
||||
["mp3"] = FileType.Audio,
|
||||
["mp4"] = FileType.Audio,
|
||||
["ogg"] = FileType.Audio,
|
||||
};
|
||||
|
||||
public static FileType GetFileTypeFromPath(string path)
|
||||
=> dic.TryGetValue(Path.GetExtension(path).ToLower().Trim('.'), out var fileType)
|
||||
? fileType
|
||||
: FileType.Unknown;
|
||||
|
||||
public static List<string> GetExtensions(FileType fileType)
|
||||
=> dic
|
||||
.Where(kvp => kvp.Value == fileType)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.2.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
29
LibationFileManager/LibraryBookDto.cs
Normal file
29
LibationFileManager/LibraryBookDto.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class BookDto
|
||||
{
|
||||
public string AudibleProductId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Locale { get; set; }
|
||||
|
||||
public IEnumerable<string> Authors { get; set; }
|
||||
public string AuthorNames => string.Join(", ", Authors);
|
||||
public string FirstAuthor => Authors.FirstOrDefault();
|
||||
|
||||
public IEnumerable<string> Narrators { get; set; }
|
||||
public string NarratorNames => string.Join(", ", Narrators);
|
||||
public string FirstNarrator => Narrators.FirstOrDefault();
|
||||
|
||||
public string SeriesName { get; set; }
|
||||
public string SeriesNumber { get; set; }
|
||||
}
|
||||
|
||||
public class LibraryBookDto : BookDto
|
||||
{
|
||||
public string Account { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public enum PictureSize { _80x80 = 80, _300x300 = 300, _500x500 = 500 }
|
||||
public class PictureCachedEventArgs : EventArgs
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class QuickFilters
|
||||
{
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.IO;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class SqliteStorage
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;";
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using Newtonsoft.Json;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace FileManager
|
||||
namespace LibationFileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
|
||||
44
LibationFileManager/TemplateTags.cs
Normal file
44
LibationFileManager/TemplateTags.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : Enumeration<TemplateTags>
|
||||
{
|
||||
public string TagName => DisplayName;
|
||||
public string Description { get; }
|
||||
public bool IsChapterOnly { get; }
|
||||
|
||||
private static int value = 0;
|
||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName)
|
||||
{
|
||||
Description = description;
|
||||
IsChapterOnly = isChapterOnly;
|
||||
}
|
||||
|
||||
// putting these first is the incredibly lazy way to make them show up first in the EditTemplateDialog
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title", true);
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #", true);
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros", true);
|
||||
|
||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
||||
public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
|
||||
public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
|
||||
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
|
||||
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
|
||||
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
|
||||
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
|
||||
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
|
||||
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
|
||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||
public static TemplateTags Locale { get; } = new TemplateTags("locale", "Region/country");
|
||||
|
||||
// Special case. Isn't mapped to a replacement in Templates.cs
|
||||
// Included here for display by EditTemplateDialog
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series");
|
||||
}
|
||||
}
|
||||
284
LibationFileManager/Templates.cs
Normal file
284
LibationFileManager/Templates.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public abstract class Templates
|
||||
{
|
||||
protected static string[] Valid => Array.Empty<string>();
|
||||
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||
public const string ERROR_INVALID_FILE_NAME_CHAR = @"Only file name friendly characters allowed. Eg: no colons or slashes";
|
||||
|
||||
public const string WARNING_EMPTY = "Template is empty.";
|
||||
public const string WARNING_WHITE_SPACE = "Template is white space.";
|
||||
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
||||
public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>";
|
||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||
|
||||
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
||||
public static FileTemplate File { get; } = new FileTemplate();
|
||||
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
||||
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract string DefaultTemplate { get; }
|
||||
protected abstract bool IsChapterized { get; }
|
||||
|
||||
protected Templates() { }
|
||||
|
||||
#region validation
|
||||
internal string GetValid(string configValue)
|
||||
{
|
||||
var value = configValue?.Trim();
|
||||
return IsValid(value) ? value : DefaultTemplate;
|
||||
}
|
||||
|
||||
public abstract IEnumerable<string> GetErrors(string template);
|
||||
public bool IsValid(string template) => !GetErrors(template).Any();
|
||||
|
||||
public abstract IEnumerable<string> GetWarnings(string template);
|
||||
public bool HasWarnings(string template) => GetWarnings(template).Any();
|
||||
|
||||
protected static string[] GetFileErrors(string template)
|
||||
{
|
||||
// File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
if (template.Contains(':')
|
||||
|| template.Contains(Path.DirectorySeparatorChar)
|
||||
|| template.Contains(Path.AltDirectorySeparatorChar)
|
||||
)
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
protected IEnumerable<string> GetStandardWarnings(string template)
|
||||
{
|
||||
var warnings = GetErrors(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
if (string.IsNullOrEmpty(template))
|
||||
warnings.Add(WARNING_EMPTY);
|
||||
else if (string.IsNullOrWhiteSpace(template))
|
||||
warnings.Add(WARNING_WHITE_SPACE);
|
||||
|
||||
if (TagCount(template) == 0)
|
||||
warnings.Add(WARNING_NO_TAGS);
|
||||
|
||||
if (!IsChapterized && ContainsChapterOnlyTags(template))
|
||||
warnings.Add(WARNING_HAS_CHAPTER_TAGS);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
internal int TagCount(string template)
|
||||
=> GetTemplateTags()
|
||||
// for <id><id> == 1, use:
|
||||
// .Count(t => template.Contains($"<{t.TagName}>"))
|
||||
// .Sum() impl: <id><id> == 2
|
||||
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
|
||||
|
||||
internal static bool ContainsChapterOnlyTags(string template)
|
||||
=> TemplateTags.GetAll()
|
||||
.Where(t => t.IsChapterOnly)
|
||||
.Any(t => ContainsTag(template, t.TagName));
|
||||
|
||||
internal static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>
|
||||
/// EditTemplateDialog: Get template generated filename for portion of path
|
||||
/// </summary>
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template)
|
||||
=> string.IsNullOrWhiteSpace(template)
|
||||
? ""
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
||||
.GetFilePath();
|
||||
|
||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
dirFullPath = dirFullPath?.Trim() ?? "";
|
||||
|
||||
// for non-series, remove <if series-> and <-if series> tags and everything in between
|
||||
// for series, remove <if series-> and <-if series> tags, what's in between will remain
|
||||
template = ifSeriesRegex.Replace(
|
||||
template,
|
||||
string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1");
|
||||
|
||||
var t = template + FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Id, libraryBookDto.AudibleProductId);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Author, libraryBookDto.AuthorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBookDto.FirstAuthor);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBookDto.NarratorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBookDto.FirstNarrator);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, libraryBookDto.SeriesNumber);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Account, libraryBookDto.Account);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
|
||||
|
||||
return fileNamingTemplate;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public IEnumerable<TemplateTags> GetTemplateTags()
|
||||
=> TemplateTags.GetAll()
|
||||
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
||||
.Where(t => IsChapterized || !t.IsChapterOnly);
|
||||
|
||||
public string Sanitize(string template)
|
||||
{
|
||||
var value = template ?? "";
|
||||
|
||||
// don't use alt slash
|
||||
value = value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
// don't allow double slashes
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
||||
while (value.Contains(dbl))
|
||||
value = value.Replace(dbl, sing);
|
||||
|
||||
// trim. don't start or end with slash
|
||||
while (true)
|
||||
{
|
||||
var start = value.Length;
|
||||
value = value
|
||||
.Trim()
|
||||
.Trim(Path.DirectorySeparatorChar);
|
||||
var end = value.Length;
|
||||
if (start == end)
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public class FolderTemplate : Templates
|
||||
{
|
||||
public override string Name => "Folder Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
|
||||
internal FolderTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template)
|
||||
{
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
if (template.Contains(':'))
|
||||
return new[] { ERROR_FULL_PATH_IS_INVALID };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, AudibleFileStorage.BooksDirectory, null)
|
||||
.GetFilePath();
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class FileTemplate : Templates
|
||||
{
|
||||
public override string Name => "File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
|
||||
internal FileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
|
||||
.GetFilePath();
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class ChapterFileTemplate : Templates
|
||||
{
|
||||
public override string Name => "Chapter File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
protected override bool IsChapterized { get; } = true;
|
||||
|
||||
internal ChapterFileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template)
|
||||
{
|
||||
var warnings = GetStandardWarnings(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
// recommended to incl. <ch#> or <ch# 0>
|
||||
if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName))
|
||||
warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
|
||||
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath)
|
||||
{
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
13
LibationFileManager/UtilityExtensions.cs
Normal file
13
LibationFileManager/UtilityExtensions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static void AddParameterReplacement(this FileNamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
||||
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
||||
}
|
||||
}
|
||||
1
LibationFileManager/_InternalsVisible.cs
Normal file
1
LibationFileManager/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationFileManager) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Lucene.Net.Analysis.Standard;
|
||||
using Lucene.Net.Documents;
|
||||
using Lucene.Net.Index;
|
||||
|
||||
1
LibationSearchEngine/_InternalsVisible.cs
Normal file
1
LibationSearchEngine/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationSearchEngine) + ".Tests")]
|
||||
@@ -1,6 +1,6 @@
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Threading;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
@@ -23,20 +24,20 @@ namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
base.Processable_Begin(sender, libraryBook);
|
||||
|
||||
GetCoverArtDelegate = () => FileManager.PictureStorage.GetPictureSynchronously(
|
||||
new FileManager.PictureDefinition(
|
||||
GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
|
||||
new PictureDefinition(
|
||||
libraryBook.Book.PictureId,
|
||||
FileManager.PictureSize._500x500));
|
||||
PictureSize._500x500));
|
||||
|
||||
//Set default values from library
|
||||
AudioDecodable_TitleDiscovered(sender, libraryBook.Book.Title);
|
||||
AudioDecodable_AuthorsDiscovered(sender, string.Join(", ", libraryBook.Book.Authors));
|
||||
AudioDecodable_NarratorsDiscovered(sender, string.Join(", ", libraryBook.Book.NarratorNames));
|
||||
AudioDecodable_CoverImageDiscovered(sender,
|
||||
FileManager.PictureStorage.GetPicture(
|
||||
new FileManager.PictureDefinition(
|
||||
PictureStorage.GetPicture(
|
||||
new PictureDefinition(
|
||||
libraryBook.Book.PictureId,
|
||||
FileManager.PictureSize._80x80)).bytes);
|
||||
PictureSize._80x80)).bytes);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -102,6 +103,15 @@ namespace LibationWinForms.BookLiberation
|
||||
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
|
||||
private void updateRemainingTime(int remaining)
|
||||
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
|
||||
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
|
||||
|
||||
private string formatTime(int seconds)
|
||||
{
|
||||
var timeSpan = new TimeSpan(0, 0, seconds);
|
||||
return
|
||||
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
|
||||
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
|
||||
: $"{seconds} sec";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
@@ -218,12 +218,12 @@ namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||
|
||||
DialogResult? dialogResult = FileManager.Configuration.Instance.BadBook switch
|
||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||
{
|
||||
FileManager.Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
FileManager.Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
FileManager.Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
||||
FileManager.Configuration.BadBookAction.Ask => null,
|
||||
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
||||
Configuration.BadBookAction.Ask => null,
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -112,6 +112,9 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!inputIsValid())
|
||||
return;
|
||||
|
||||
// without transaction, accounts persister will write ANY EDIT immediately to file
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
@@ -129,6 +132,28 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private bool inputIsValid()
|
||||
{
|
||||
var dtos = getRowDtos();
|
||||
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
{
|
||||
MessageBox.Show("Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
{
|
||||
MessageBox.Show("Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void persist(AccountsSettings accountsSettings)
|
||||
{
|
||||
var existingAccounts = accountsSettings.Accounts;
|
||||
@@ -152,11 +177,6 @@ namespace LibationWinForms.Dialogs
|
||||
// upsert each. validation occurs through Account and AccountsSettings
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
throw new Exception("Please enter an account id for all accounts");
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
throw new Exception("Please select a locale (i.e.: country or region) for all accounts");
|
||||
|
||||
var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName);
|
||||
acct.LibraryScan = dto.LibraryScan;
|
||||
acct.AccountName
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -39,7 +40,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
this.Text = Book.Title;
|
||||
|
||||
(var isDefault, var picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
||||
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
this.coverPb.Image = Dinah.Core.Drawing.ImageReader.ToImage(picture);
|
||||
|
||||
var t = @$"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
197
LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs
generated
Normal file
197
LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,197 @@
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class EditTemplateDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.templateTb = new System.Windows.Forms.TextBox();
|
||||
this.templateLbl = new System.Windows.Forms.Label();
|
||||
this.resetToDefaultBtn = new System.Windows.Forms.Button();
|
||||
this.listView1 = new System.Windows.Forms.ListView();
|
||||
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
|
||||
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
|
||||
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
|
||||
this.warningsLbl = new System.Windows.Forms.Label();
|
||||
this.exampleLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 345);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 345);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// templateTb
|
||||
//
|
||||
this.templateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.templateTb.Location = new System.Drawing.Point(12, 27);
|
||||
this.templateTb.Name = "templateTb";
|
||||
this.templateTb.Size = new System.Drawing.Size(779, 23);
|
||||
this.templateTb.TabIndex = 1;
|
||||
this.templateTb.TextChanged += new System.EventHandler(this.templateTb_TextChanged);
|
||||
//
|
||||
// templateLbl
|
||||
//
|
||||
this.templateLbl.AutoSize = true;
|
||||
this.templateLbl.Location = new System.Drawing.Point(12, 9);
|
||||
this.templateLbl.Name = "templateLbl";
|
||||
this.templateLbl.Size = new System.Drawing.Size(89, 15);
|
||||
this.templateLbl.TabIndex = 0;
|
||||
this.templateLbl.Text = "[template desc]";
|
||||
//
|
||||
// resetToDefaultBtn
|
||||
//
|
||||
this.resetToDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
|
||||
this.resetToDefaultBtn.Name = "resetToDefaultBtn";
|
||||
this.resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
|
||||
this.resetToDefaultBtn.TabIndex = 2;
|
||||
this.resetToDefaultBtn.Text = "Reset to default";
|
||||
this.resetToDefaultBtn.UseVisualStyleBackColor = true;
|
||||
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click);
|
||||
//
|
||||
// listView1
|
||||
//
|
||||
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
this.columnHeader2});
|
||||
this.listView1.HideSelection = false;
|
||||
this.listView1.Location = new System.Drawing.Point(12, 56);
|
||||
this.listView1.Name = "listView1";
|
||||
this.listView1.Size = new System.Drawing.Size(328, 283);
|
||||
this.listView1.TabIndex = 3;
|
||||
this.listView1.UseCompatibleStateImageBehavior = false;
|
||||
this.listView1.View = System.Windows.Forms.View.Details;
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
this.columnHeader1.Text = "Tag";
|
||||
this.columnHeader1.Width = 137;
|
||||
//
|
||||
// columnHeader2
|
||||
//
|
||||
this.columnHeader2.Text = "Description";
|
||||
this.columnHeader2.Width = 170;
|
||||
//
|
||||
// richTextBox1
|
||||
//
|
||||
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.richTextBox1.Location = new System.Drawing.Point(346, 74);
|
||||
this.richTextBox1.Name = "richTextBox1";
|
||||
this.richTextBox1.ReadOnly = true;
|
||||
this.richTextBox1.Size = new System.Drawing.Size(574, 185);
|
||||
this.richTextBox1.TabIndex = 5;
|
||||
this.richTextBox1.Text = "";
|
||||
//
|
||||
// warningsLbl
|
||||
//
|
||||
this.warningsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
|
||||
this.warningsLbl.Location = new System.Drawing.Point(346, 262);
|
||||
this.warningsLbl.Name = "warningsLbl";
|
||||
this.warningsLbl.Size = new System.Drawing.Size(574, 77);
|
||||
this.warningsLbl.TabIndex = 6;
|
||||
this.warningsLbl.Text = "[warnings]";
|
||||
//
|
||||
// exampleLbl
|
||||
//
|
||||
this.exampleLbl.AutoSize = true;
|
||||
this.exampleLbl.Location = new System.Drawing.Point(346, 56);
|
||||
this.exampleLbl.Name = "exampleLbl";
|
||||
this.exampleLbl.Size = new System.Drawing.Size(55, 15);
|
||||
this.exampleLbl.TabIndex = 4;
|
||||
this.exampleLbl.Text = "Example:";
|
||||
//
|
||||
// EditTemplateDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 388);
|
||||
this.Controls.Add(this.exampleLbl);
|
||||
this.Controls.Add(this.warningsLbl);
|
||||
this.Controls.Add(this.richTextBox1);
|
||||
this.Controls.Add(this.listView1);
|
||||
this.Controls.Add(this.resetToDefaultBtn);
|
||||
this.Controls.Add(this.templateLbl);
|
||||
this.Controls.Add(this.templateTb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "EditTemplateDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Template";
|
||||
this.Load += new System.EventHandler(this.EditTemplateDialog_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.TextBox templateTb;
|
||||
private System.Windows.Forms.Label templateLbl;
|
||||
private System.Windows.Forms.Button resetToDefaultBtn;
|
||||
private System.Windows.Forms.ListView listView1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader2;
|
||||
private System.Windows.Forms.RichTextBox richTextBox1;
|
||||
private System.Windows.Forms.Label warningsLbl;
|
||||
private System.Windows.Forms.Label exampleLbl;
|
||||
}
|
||||
}
|
||||
173
LibationWinForms/Dialogs/EditTemplateDialog.cs
Normal file
173
LibationWinForms/Dialogs/EditTemplateDialog.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class EditTemplateDialog : Form
|
||||
{
|
||||
// final value. post-validity check
|
||||
public string TemplateText { get; private set; }
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _workingTemplateText;
|
||||
private string workingTemplateText
|
||||
{
|
||||
get => _workingTemplateText;
|
||||
set => _workingTemplateText = template.Sanitize(value);
|
||||
}
|
||||
|
||||
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
|
||||
|
||||
private Configuration config { get; } = Configuration.Instance;
|
||||
|
||||
private Templates template { get; }
|
||||
private string inputTemplateText { get; }
|
||||
|
||||
public EditTemplateDialog() => InitializeComponent();
|
||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
||||
{
|
||||
this.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||
this.inputTemplateText = inputTemplateText ?? "";
|
||||
}
|
||||
|
||||
private void EditTemplateDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
if (template is null)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show($"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
|
||||
return;
|
||||
}
|
||||
|
||||
warningsLbl.Text = "";
|
||||
|
||||
this.Text = $"Edit {template.Name}";
|
||||
|
||||
this.templateLbl.Text = template.Description;
|
||||
resetTextBox(inputTemplateText);
|
||||
|
||||
// populate list view
|
||||
foreach (var tag in template.GetTemplateTags())
|
||||
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }));
|
||||
}
|
||||
|
||||
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate);
|
||||
|
||||
private void templateTb_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
workingTemplateText = templateTb.Text;
|
||||
|
||||
var isFolder = template == Templates.Folder;
|
||||
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
Account = "my account",
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = "Sherlock Holmes",
|
||||
SeriesNumber = "1"
|
||||
};
|
||||
var chapterName = "A Flight for Life";
|
||||
var chapterNumber = 4;
|
||||
var chaptersTotal = 10;
|
||||
|
||||
var books = config.Books;
|
||||
var folder = Templates.Folder.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? workingTemplateText : config.FolderTemplate);
|
||||
var file
|
||||
= template == Templates.ChapterFile
|
||||
? Templates.ChapterFile.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
workingTemplateText,
|
||||
new() { OutputFileName = "", PartsPosition = chapterNumber, PartsTotal = chaptersTotal, Title = chapterName },
|
||||
"")
|
||||
: Templates.File.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? config.FileTemplate : workingTemplateText);
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
warningsLbl.Text
|
||||
= !template.HasWarnings(workingTemplateText)
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
template
|
||||
.GetWarnings(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
var bold = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Bold);
|
||||
var reg = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Regular);
|
||||
|
||||
richTextBox1.Clear();
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText(slashWrap(books));
|
||||
richTextBox1.AppendText(sing);
|
||||
|
||||
if (isFolder)
|
||||
richTextBox1.SelectionFont = bold;
|
||||
|
||||
richTextBox1.AppendText(slashWrap(folder));
|
||||
|
||||
if (isFolder)
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText(sing);
|
||||
|
||||
if (!isFolder)
|
||||
richTextBox1.SelectionFont = bold;
|
||||
|
||||
richTextBox1.AppendText(file);
|
||||
|
||||
if (!isFolder)
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText($".{ext}");
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!template.IsValid(workingTemplateText))
|
||||
{
|
||||
var errors = template
|
||||
.GetErrors(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
TemplateText = workingTemplateText;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
LibationWinForms/Dialogs/EditTemplateDialog.resx
Normal file
60
LibationWinForms/Dialogs/EditTemplateDialog.resx
Normal file
@@ -0,0 +1,60 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,9 +1,8 @@
|
||||
using ApplicationServices;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Login;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Login;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -28,140 +28,152 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginExternalDialog));
|
||||
this.submitBtn = new System.Windows.Forms.Button();
|
||||
this.localeLbl = new System.Windows.Forms.Label();
|
||||
this.usernameLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.copyBtn = new System.Windows.Forms.Button();
|
||||
this.launchBrowserBtn = new System.Windows.Forms.Button();
|
||||
this.instructionsLbl = new System.Windows.Forms.Label();
|
||||
this.responseUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// submitBtn
|
||||
//
|
||||
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.submitBtn.Location = new System.Drawing.Point(665, 400);
|
||||
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.submitBtn.Name = "submitBtn";
|
||||
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.submitBtn.TabIndex = 8;
|
||||
this.submitBtn.Text = "Submit";
|
||||
this.submitBtn.UseVisualStyleBackColor = true;
|
||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||
//
|
||||
// localeLbl
|
||||
//
|
||||
this.localeLbl.AutoSize = true;
|
||||
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.localeLbl.Name = "localeLbl";
|
||||
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
this.localeLbl.TabIndex = 0;
|
||||
this.localeLbl.Text = "Locale: {0}";
|
||||
//
|
||||
// usernameLbl
|
||||
//
|
||||
this.usernameLbl.AutoSize = true;
|
||||
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.usernameLbl.Name = "usernameLbl";
|
||||
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.usernameLbl.TabIndex = 1;
|
||||
this.usernameLbl.Text = "Username: {0}";
|
||||
//
|
||||
// loginUrlLbl
|
||||
//
|
||||
this.loginUrlLbl.AutoSize = true;
|
||||
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
|
||||
this.loginUrlLbl.Name = "loginUrlLbl";
|
||||
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
|
||||
this.loginUrlLbl.TabIndex = 2;
|
||||
this.loginUrlLbl.Text = "Paste this URL into your browser:";
|
||||
//
|
||||
// loginUrlTb
|
||||
//
|
||||
this.loginUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginExternalDialog));
|
||||
this.submitBtn = new System.Windows.Forms.Button();
|
||||
this.localeLbl = new System.Windows.Forms.Label();
|
||||
this.usernameLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.copyBtn = new System.Windows.Forms.Button();
|
||||
this.launchBrowserBtn = new System.Windows.Forms.Button();
|
||||
this.instructionsLbl = new System.Windows.Forms.Label();
|
||||
this.responseUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.tldrLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// submitBtn
|
||||
//
|
||||
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.submitBtn.Location = new System.Drawing.Point(665, 458);
|
||||
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.submitBtn.Name = "submitBtn";
|
||||
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.submitBtn.TabIndex = 8;
|
||||
this.submitBtn.Text = "Submit";
|
||||
this.submitBtn.UseVisualStyleBackColor = true;
|
||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||
//
|
||||
// localeLbl
|
||||
//
|
||||
this.localeLbl.AutoSize = true;
|
||||
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.localeLbl.Name = "localeLbl";
|
||||
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
this.localeLbl.TabIndex = 0;
|
||||
this.localeLbl.Text = "Locale: {0}";
|
||||
//
|
||||
// usernameLbl
|
||||
//
|
||||
this.usernameLbl.AutoSize = true;
|
||||
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.usernameLbl.Name = "usernameLbl";
|
||||
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.usernameLbl.TabIndex = 1;
|
||||
this.usernameLbl.Text = "Username: {0}";
|
||||
//
|
||||
// loginUrlLbl
|
||||
//
|
||||
this.loginUrlLbl.AutoSize = true;
|
||||
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
|
||||
this.loginUrlLbl.Name = "loginUrlLbl";
|
||||
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
|
||||
this.loginUrlLbl.TabIndex = 2;
|
||||
this.loginUrlLbl.Text = "Paste this URL into your browser:";
|
||||
//
|
||||
// loginUrlTb
|
||||
//
|
||||
this.loginUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.loginUrlTb.Location = new System.Drawing.Point(14, 79);
|
||||
this.loginUrlTb.Multiline = true;
|
||||
this.loginUrlTb.Name = "loginUrlTb";
|
||||
this.loginUrlTb.ReadOnly = true;
|
||||
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.loginUrlTb.Size = new System.Drawing.Size(739, 92);
|
||||
this.loginUrlTb.TabIndex = 3;
|
||||
//
|
||||
// copyBtn
|
||||
//
|
||||
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.copyBtn.Location = new System.Drawing.Point(14, 177);
|
||||
this.copyBtn.Name = "copyBtn";
|
||||
this.copyBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.copyBtn.TabIndex = 4;
|
||||
this.copyBtn.Text = "Copy URL to clipboard";
|
||||
this.copyBtn.UseVisualStyleBackColor = true;
|
||||
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
|
||||
//
|
||||
// launchBrowserBtn
|
||||
//
|
||||
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.launchBrowserBtn.Location = new System.Drawing.Point(588, 177);
|
||||
this.launchBrowserBtn.Name = "launchBrowserBtn";
|
||||
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.launchBrowserBtn.TabIndex = 5;
|
||||
this.launchBrowserBtn.Text = "Launch in browser";
|
||||
this.launchBrowserBtn.UseVisualStyleBackColor = true;
|
||||
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
|
||||
//
|
||||
// instructionsLbl
|
||||
//
|
||||
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.instructionsLbl.AutoSize = true;
|
||||
this.instructionsLbl.Location = new System.Drawing.Point(14, 203);
|
||||
this.instructionsLbl.Name = "instructionsLbl";
|
||||
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
|
||||
this.instructionsLbl.TabIndex = 6;
|
||||
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
|
||||
//
|
||||
// responseUrlTb
|
||||
//
|
||||
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.loginUrlTb.Location = new System.Drawing.Point(14, 79);
|
||||
this.loginUrlTb.Multiline = true;
|
||||
this.loginUrlTb.Name = "loginUrlTb";
|
||||
this.loginUrlTb.ReadOnly = true;
|
||||
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.loginUrlTb.Size = new System.Drawing.Size(739, 117);
|
||||
this.loginUrlTb.TabIndex = 3;
|
||||
//
|
||||
// copyBtn
|
||||
//
|
||||
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.copyBtn.Location = new System.Drawing.Point(14, 202);
|
||||
this.copyBtn.Name = "copyBtn";
|
||||
this.copyBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.copyBtn.TabIndex = 4;
|
||||
this.copyBtn.Text = "Copy URL to clipboard";
|
||||
this.copyBtn.UseVisualStyleBackColor = true;
|
||||
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
|
||||
//
|
||||
// launchBrowserBtn
|
||||
//
|
||||
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.launchBrowserBtn.Location = new System.Drawing.Point(589, 202);
|
||||
this.launchBrowserBtn.Name = "launchBrowserBtn";
|
||||
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.launchBrowserBtn.TabIndex = 5;
|
||||
this.launchBrowserBtn.Text = "Launch in browser";
|
||||
this.launchBrowserBtn.UseVisualStyleBackColor = true;
|
||||
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
|
||||
//
|
||||
// instructionsLbl
|
||||
//
|
||||
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.instructionsLbl.AutoSize = true;
|
||||
this.instructionsLbl.Location = new System.Drawing.Point(14, 261);
|
||||
this.instructionsLbl.Name = "instructionsLbl";
|
||||
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
|
||||
this.instructionsLbl.TabIndex = 6;
|
||||
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
|
||||
//
|
||||
// responseUrlTb
|
||||
//
|
||||
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.responseUrlTb.Location = new System.Drawing.Point(14, 296);
|
||||
this.responseUrlTb.Multiline = true;
|
||||
this.responseUrlTb.Name = "responseUrlTb";
|
||||
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
|
||||
this.responseUrlTb.TabIndex = 7;
|
||||
//
|
||||
// LoginExternalDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(766, 440);
|
||||
this.Controls.Add(this.responseUrlTb);
|
||||
this.Controls.Add(this.instructionsLbl);
|
||||
this.Controls.Add(this.launchBrowserBtn);
|
||||
this.Controls.Add(this.copyBtn);
|
||||
this.Controls.Add(this.loginUrlTb);
|
||||
this.Controls.Add(this.loginUrlLbl);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
this.Controls.Add(this.submitBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "LoginExternalDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Audible External Login";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.responseUrlTb.Location = new System.Drawing.Point(14, 354);
|
||||
this.responseUrlTb.Multiline = true;
|
||||
this.responseUrlTb.Name = "responseUrlTb";
|
||||
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
|
||||
this.responseUrlTb.TabIndex = 7;
|
||||
//
|
||||
// tldrLbl
|
||||
//
|
||||
this.tldrLbl.AutoSize = true;
|
||||
this.tldrLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.tldrLbl.Location = new System.Drawing.Point(14, 237);
|
||||
this.tldrLbl.Name = "tldrLbl";
|
||||
this.tldrLbl.Size = new System.Drawing.Size(421, 15);
|
||||
this.tldrLbl.TabIndex = 9;
|
||||
this.tldrLbl.Text = "tl;dr : an ERROR on Amazon is GOOD. Sorry, I can\'t control their weird login";
|
||||
//
|
||||
// LoginExternalDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(766, 498);
|
||||
this.Controls.Add(this.tldrLbl);
|
||||
this.Controls.Add(this.responseUrlTb);
|
||||
this.Controls.Add(this.instructionsLbl);
|
||||
this.Controls.Add(this.launchBrowserBtn);
|
||||
this.Controls.Add(this.copyBtn);
|
||||
this.Controls.Add(this.loginUrlTb);
|
||||
this.Controls.Add(this.loginUrlLbl);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
this.Controls.Add(this.submitBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "LoginExternalDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Audible External Login";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -175,5 +187,6 @@
|
||||
private System.Windows.Forms.Button launchBrowserBtn;
|
||||
private System.Windows.Forms.Label instructionsLbl;
|
||||
private System.Windows.Forms.TextBox responseUrlTb;
|
||||
}
|
||||
private System.Windows.Forms.Label tldrLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
|
||||
namespace LibationWinForms.Login
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
|
||||
namespace LibationWinForms.Login
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace LibationWinForms.Dialogs
|
||||
string dir = "";
|
||||
try
|
||||
{
|
||||
dir = FileManager.Configuration.Instance.LibationFiles;
|
||||
dir = LibationFileManager.Configuration.Instance.LibationFiles;
|
||||
}
|
||||
catch { }
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Login;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Login;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using InternalUtilities;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
840
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
840
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -28,326 +28,517 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.inProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
||||
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAskRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
|
||||
this.splitFilesByChapterCbox = new System.Windows.Forms.CheckBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.logsBtn = new System.Windows.Forms.Button();
|
||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
this.advancedSettingsGb.SuspendLayout();
|
||||
this.badBookGb.SuspendLayout();
|
||||
this.decryptAndConvertGb.SuspendLayout();
|
||||
this.booksGb.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
||||
this.booksLocationDescLbl.TabIndex = 1;
|
||||
this.booksLocationDescLbl.Text = "[book desc]";
|
||||
//
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
|
||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
|
||||
this.inProgressDescLbl.TabIndex = 18;
|
||||
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// advancedSettingsGb
|
||||
//
|
||||
this.advancedSettingsGb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.inProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAskRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
|
||||
this.splitFilesByChapterCbox = new System.Windows.Forms.CheckBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.logsBtn = new System.Windows.Forms.Button();
|
||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
this.tabControl = new System.Windows.Forms.TabControl();
|
||||
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
||||
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
|
||||
this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
|
||||
this.customFileNamingGb = new System.Windows.Forms.GroupBox();
|
||||
this.chapterFileTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.chapterFileTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.chapterFileTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.fileTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.fileTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.fileTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.folderTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.folderTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.folderTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.showImportedStatsCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb.SuspendLayout();
|
||||
this.decryptAndConvertGb.SuspendLayout();
|
||||
this.tabControl.SuspendLayout();
|
||||
this.tab1ImportantSettings.SuspendLayout();
|
||||
this.booksGb.SuspendLayout();
|
||||
this.tab2ImportLibrary.SuspendLayout();
|
||||
this.tab3DownloadDecrypt.SuspendLayout();
|
||||
this.inProgressFilesGb.SuspendLayout();
|
||||
this.customFileNamingGb.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
||||
this.booksLocationDescLbl.TabIndex = 1;
|
||||
this.booksLocationDescLbl.Text = "[book desc]";
|
||||
//
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
this.inProgressDescLbl.Size = new System.Drawing.Size(100, 45);
|
||||
this.inProgressDescLbl.TabIndex = 18;
|
||||
this.inProgressDescLbl.Text = "[in progress desc]\r\n[line 2]\r\n[line 3]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// importEpisodesCb
|
||||
//
|
||||
this.importEpisodesCb.AutoSize = true;
|
||||
this.importEpisodesCb.Location = new System.Drawing.Point(6, 31);
|
||||
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||
this.importEpisodesCb.TabIndex = 2;
|
||||
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
this.downloadEpisodesCb.AutoSize = true;
|
||||
this.downloadEpisodesCb.Location = new System.Drawing.Point(6, 56);
|
||||
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
this.downloadEpisodesCb.TabIndex = 3;
|
||||
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookGb
|
||||
//
|
||||
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||
this.badBookGb.Location = new System.Drawing.Point(371, 6);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(524, 124);
|
||||
this.badBookGb.TabIndex = 13;
|
||||
this.badBookGb.TabStop = false;
|
||||
this.badBookGb.Text = "[bad book desc]";
|
||||
//
|
||||
// badBookIgnoreRb
|
||||
//
|
||||
this.badBookIgnoreRb.AutoSize = true;
|
||||
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||
this.badBookIgnoreRb.TabIndex = 17;
|
||||
this.badBookIgnoreRb.TabStop = true;
|
||||
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookRetryRb
|
||||
//
|
||||
this.badBookRetryRb.AutoSize = true;
|
||||
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||
this.badBookRetryRb.TabIndex = 16;
|
||||
this.badBookRetryRb.TabStop = true;
|
||||
this.badBookRetryRb.Text = "[retry desc]";
|
||||
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAbortRb
|
||||
//
|
||||
this.badBookAbortRb.AutoSize = true;
|
||||
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||
this.badBookAbortRb.TabIndex = 15;
|
||||
this.badBookAbortRb.TabStop = true;
|
||||
this.badBookAbortRb.Text = "[abort desc]";
|
||||
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAskRb
|
||||
//
|
||||
this.badBookAskRb.AutoSize = true;
|
||||
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.badBookAskRb.Name = "badBookAskRb";
|
||||
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||
this.badBookAskRb.TabIndex = 14;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptAndConvertGb
|
||||
//
|
||||
this.decryptAndConvertGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(6, 6);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 9;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
// splitFilesByChapterCbox
|
||||
//
|
||||
this.splitFilesByChapterCbox.AutoSize = true;
|
||||
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(6, 46);
|
||||
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
|
||||
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
|
||||
this.splitFilesByChapterCbox.TabIndex = 13;
|
||||
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
||||
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
//
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
this.allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// convertLossyRb
|
||||
//
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 101);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// convertLosslessRb
|
||||
//
|
||||
this.convertLosslessRb.AutoSize = true;
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 76);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 11;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68);
|
||||
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(875, 52);
|
||||
this.inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
this.logsBtn.Location = new System.Drawing.Point(256, 169);
|
||||
this.logsBtn.Name = "logsBtn";
|
||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
this.logsBtn.TabIndex = 5;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(876, 87);
|
||||
this.booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
this.loggingLevelLbl.AutoSize = true;
|
||||
this.loggingLevelLbl.Location = new System.Drawing.Point(6, 172);
|
||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
this.loggingLevelLbl.TabIndex = 3;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
//
|
||||
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.loggingLevelCb.FormattingEnabled = true;
|
||||
this.loggingLevelCb.Location = new System.Drawing.Point(90, 169);
|
||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
this.loggingLevelCb.TabIndex = 4;
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.badBookGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
|
||||
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Name = "advancedSettingsGb";
|
||||
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
|
||||
this.advancedSettingsGb.TabIndex = 6;
|
||||
this.advancedSettingsGb.TabStop = false;
|
||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// importEpisodesCb
|
||||
//
|
||||
this.importEpisodesCb.AutoSize = true;
|
||||
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
|
||||
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||
this.importEpisodesCb.TabIndex = 7;
|
||||
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
this.downloadEpisodesCb.AutoSize = true;
|
||||
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
|
||||
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
this.downloadEpisodesCb.TabIndex = 8;
|
||||
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookGb
|
||||
//
|
||||
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||
this.badBookGb.Location = new System.Drawing.Point(372, 72);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(529, 124);
|
||||
this.badBookGb.TabIndex = 13;
|
||||
this.badBookGb.TabStop = false;
|
||||
this.badBookGb.Text = "[bad book desc]";
|
||||
//
|
||||
// badBookIgnoreRb
|
||||
//
|
||||
this.badBookIgnoreRb.AutoSize = true;
|
||||
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||
this.badBookIgnoreRb.TabIndex = 17;
|
||||
this.badBookIgnoreRb.TabStop = true;
|
||||
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookRetryRb
|
||||
//
|
||||
this.badBookRetryRb.AutoSize = true;
|
||||
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||
this.badBookRetryRb.TabIndex = 16;
|
||||
this.badBookRetryRb.TabStop = true;
|
||||
this.badBookRetryRb.Text = "[retry desc]";
|
||||
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAbortRb
|
||||
//
|
||||
this.badBookAbortRb.AutoSize = true;
|
||||
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||
this.badBookAbortRb.TabIndex = 15;
|
||||
this.badBookAbortRb.TabStop = true;
|
||||
this.badBookAbortRb.Text = "[abort desc]";
|
||||
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAskRb
|
||||
//
|
||||
this.badBookAskRb.AutoSize = true;
|
||||
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.badBookAskRb.Name = "badBookAskRb";
|
||||
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||
this.badBookAskRb.TabIndex = 14;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptAndConvertGb
|
||||
//
|
||||
this.decryptAndConvertGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 9;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
// splitFilesByChapterCbox
|
||||
//
|
||||
this.splitFilesByChapterCbox.AutoSize = true;
|
||||
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(6, 46);
|
||||
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
|
||||
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
|
||||
this.splitFilesByChapterCbox.TabIndex = 13;
|
||||
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
||||
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
//
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
this.allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// convertLossyRb
|
||||
//
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 101);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// convertLosslessRb
|
||||
//
|
||||
this.convertLosslessRb.AutoSize = true;
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 76);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 11;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.tabControl.Controls.Add(this.tab1ImportantSettings);
|
||||
this.tabControl.Controls.Add(this.tab2ImportLibrary);
|
||||
this.tabControl.Controls.Add(this.tab3DownloadDecrypt);
|
||||
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
this.tabControl.Name = "tabControl";
|
||||
this.tabControl.SelectedIndex = 0;
|
||||
this.tabControl.Size = new System.Drawing.Size(909, 478);
|
||||
this.tabControl.TabIndex = 100;
|
||||
//
|
||||
// tab1ImportantSettings
|
||||
//
|
||||
this.tab1ImportantSettings.Controls.Add(this.booksGb);
|
||||
this.tab1ImportantSettings.Controls.Add(this.logsBtn);
|
||||
this.tab1ImportantSettings.Controls.Add(this.loggingLevelCb);
|
||||
this.tab1ImportantSettings.Controls.Add(this.loggingLevelLbl);
|
||||
this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab1ImportantSettings.Name = "tab1ImportantSettings";
|
||||
this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab1ImportantSettings.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab1ImportantSettings.TabIndex = 0;
|
||||
this.tab1ImportantSettings.Text = "Important settings";
|
||||
this.tab1ImportantSettings.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
|
||||
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(894, 52);
|
||||
this.inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
this.logsBtn.Location = new System.Drawing.Point(262, 147);
|
||||
this.logsBtn.Name = "logsBtn";
|
||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
this.logsBtn.TabIndex = 5;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.booksGb.Controls.Add(this.booksSelectControl);
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
this.booksGb.Location = new System.Drawing.Point(6, 6);
|
||||
this.booksGb.Name = "booksGb";
|
||||
this.booksGb.Size = new System.Drawing.Size(889, 129);
|
||||
this.booksGb.TabIndex = 0;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
// tab2ImportLibrary
|
||||
//
|
||||
this.tab2ImportLibrary.Controls.Add(this.showImportedStatsCb);
|
||||
this.tab2ImportLibrary.Controls.Add(this.importEpisodesCb);
|
||||
this.tab2ImportLibrary.Controls.Add(this.downloadEpisodesCb);
|
||||
this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab2ImportLibrary.Name = "tab2ImportLibrary";
|
||||
this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab2ImportLibrary.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab2ImportLibrary.TabIndex = 1;
|
||||
this.tab2ImportLibrary.Text = "Import library";
|
||||
this.tab2ImportLibrary.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// tab3DownloadDecrypt
|
||||
//
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.inProgressFilesGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.customFileNamingGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.decryptAndConvertGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.badBookGb);
|
||||
this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
|
||||
this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab3DownloadDecrypt.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab3DownloadDecrypt.TabIndex = 2;
|
||||
this.tab3DownloadDecrypt.Text = "Download/Decrypt";
|
||||
this.tab3DownloadDecrypt.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressFilesGb
|
||||
//
|
||||
this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
||||
this.booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.inProgressFilesGb.Location = new System.Drawing.Point(7, 299);
|
||||
this.inProgressFilesGb.Name = "inProgressFilesGb";
|
||||
this.inProgressFilesGb.Size = new System.Drawing.Size(888, 128);
|
||||
this.inProgressFilesGb.TabIndex = 21;
|
||||
this.inProgressFilesGb.TabStop = false;
|
||||
this.inProgressFilesGb.Text = "In progress files";
|
||||
//
|
||||
// customFileNamingGb
|
||||
//
|
||||
this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksGb.Controls.Add(this.booksSelectControl);
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
this.booksGb.Location = new System.Drawing.Point(12, 12);
|
||||
this.booksGb.Name = "booksGb";
|
||||
this.booksGb.Size = new System.Drawing.Size(908, 129);
|
||||
this.booksGb.TabIndex = 0;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
this.loggingLevelLbl.AutoSize = true;
|
||||
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
|
||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
this.loggingLevelLbl.TabIndex = 3;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
//
|
||||
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.loggingLevelCb.FormattingEnabled = true;
|
||||
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
|
||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
this.loggingLevelCb.TabIndex = 4;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||
this.Controls.Add(this.logsBtn);
|
||||
this.Controls.Add(this.loggingLevelCb);
|
||||
this.Controls.Add(this.loggingLevelLbl);
|
||||
this.Controls.Add(this.booksGb);
|
||||
this.Controls.Add(this.advancedSettingsGb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.advancedSettingsGb.ResumeLayout(false);
|
||||
this.advancedSettingsGb.PerformLayout();
|
||||
this.badBookGb.ResumeLayout(false);
|
||||
this.badBookGb.PerformLayout();
|
||||
this.decryptAndConvertGb.ResumeLayout(false);
|
||||
this.decryptAndConvertGb.PerformLayout();
|
||||
this.booksGb.ResumeLayout(false);
|
||||
this.booksGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateLbl);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateLbl);
|
||||
this.customFileNamingGb.Location = new System.Drawing.Point(7, 136);
|
||||
this.customFileNamingGb.Name = "customFileNamingGb";
|
||||
this.customFileNamingGb.Size = new System.Drawing.Size(888, 157);
|
||||
this.customFileNamingGb.TabIndex = 20;
|
||||
this.customFileNamingGb.TabStop = false;
|
||||
this.customFileNamingGb.Text = "Custom file naming";
|
||||
//
|
||||
// chapterFileTemplateBtn
|
||||
//
|
||||
this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterFileTemplateBtn.Location = new System.Drawing.Point(808, 124);
|
||||
this.chapterFileTemplateBtn.Name = "chapterFileTemplateBtn";
|
||||
this.chapterFileTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.chapterFileTemplateBtn.TabIndex = 8;
|
||||
this.chapterFileTemplateBtn.Text = "Edit...";
|
||||
this.chapterFileTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.chapterFileTemplateBtn.Click += new System.EventHandler(this.chapterFileTemplateBtn_Click);
|
||||
//
|
||||
// chapterFileTemplateTb
|
||||
//
|
||||
this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125);
|
||||
this.chapterFileTemplateTb.Name = "chapterFileTemplateTb";
|
||||
this.chapterFileTemplateTb.ReadOnly = true;
|
||||
this.chapterFileTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.chapterFileTemplateTb.TabIndex = 7;
|
||||
//
|
||||
// chapterFileTemplateLbl
|
||||
//
|
||||
this.chapterFileTemplateLbl.AutoSize = true;
|
||||
this.chapterFileTemplateLbl.Location = new System.Drawing.Point(6, 107);
|
||||
this.chapterFileTemplateLbl.Name = "chapterFileTemplateLbl";
|
||||
this.chapterFileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.chapterFileTemplateLbl.TabIndex = 6;
|
||||
this.chapterFileTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// fileTemplateBtn
|
||||
//
|
||||
this.fileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.fileTemplateBtn.Location = new System.Drawing.Point(808, 80);
|
||||
this.fileTemplateBtn.Name = "fileTemplateBtn";
|
||||
this.fileTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.fileTemplateBtn.TabIndex = 5;
|
||||
this.fileTemplateBtn.Text = "Edit...";
|
||||
this.fileTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.fileTemplateBtn.Click += new System.EventHandler(this.fileTemplateBtn_Click);
|
||||
//
|
||||
// fileTemplateTb
|
||||
//
|
||||
this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.fileTemplateTb.Location = new System.Drawing.Point(6, 81);
|
||||
this.fileTemplateTb.Name = "fileTemplateTb";
|
||||
this.fileTemplateTb.ReadOnly = true;
|
||||
this.fileTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.fileTemplateTb.TabIndex = 4;
|
||||
//
|
||||
// fileTemplateLbl
|
||||
//
|
||||
this.fileTemplateLbl.AutoSize = true;
|
||||
this.fileTemplateLbl.Location = new System.Drawing.Point(6, 63);
|
||||
this.fileTemplateLbl.Name = "fileTemplateLbl";
|
||||
this.fileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.fileTemplateLbl.TabIndex = 3;
|
||||
this.fileTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// folderTemplateBtn
|
||||
//
|
||||
this.folderTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.folderTemplateBtn.Location = new System.Drawing.Point(807, 36);
|
||||
this.folderTemplateBtn.Name = "folderTemplateBtn";
|
||||
this.folderTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.folderTemplateBtn.TabIndex = 2;
|
||||
this.folderTemplateBtn.Text = "Edit...";
|
||||
this.folderTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.folderTemplateBtn.Click += new System.EventHandler(this.folderTemplateBtn_Click);
|
||||
//
|
||||
// folderTemplateTb
|
||||
//
|
||||
this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.folderTemplateTb.Location = new System.Drawing.Point(5, 37);
|
||||
this.folderTemplateTb.Name = "folderTemplateTb";
|
||||
this.folderTemplateTb.ReadOnly = true;
|
||||
this.folderTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.folderTemplateTb.TabIndex = 1;
|
||||
//
|
||||
// folderTemplateLbl
|
||||
//
|
||||
this.folderTemplateLbl.AutoSize = true;
|
||||
this.folderTemplateLbl.Location = new System.Drawing.Point(5, 19);
|
||||
this.folderTemplateLbl.Name = "folderTemplateLbl";
|
||||
this.folderTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.folderTemplateLbl.TabIndex = 0;
|
||||
this.folderTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// showImportedStatsCb
|
||||
//
|
||||
this.showImportedStatsCb.AutoSize = true;
|
||||
this.showImportedStatsCb.Location = new System.Drawing.Point(6, 6);
|
||||
this.showImportedStatsCb.Name = "showImportedStatsCb";
|
||||
this.showImportedStatsCb.Size = new System.Drawing.Size(168, 19);
|
||||
this.showImportedStatsCb.TabIndex = 1;
|
||||
this.showImportedStatsCb.Text = "[show imported stats desc]";
|
||||
this.showImportedStatsCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.badBookGb.ResumeLayout(false);
|
||||
this.badBookGb.PerformLayout();
|
||||
this.decryptAndConvertGb.ResumeLayout(false);
|
||||
this.decryptAndConvertGb.PerformLayout();
|
||||
this.tabControl.ResumeLayout(false);
|
||||
this.tab1ImportantSettings.ResumeLayout(false);
|
||||
this.tab1ImportantSettings.PerformLayout();
|
||||
this.booksGb.ResumeLayout(false);
|
||||
this.booksGb.PerformLayout();
|
||||
this.tab2ImportLibrary.ResumeLayout(false);
|
||||
this.tab2ImportLibrary.PerformLayout();
|
||||
this.tab3DownloadDecrypt.ResumeLayout(false);
|
||||
this.inProgressFilesGb.ResumeLayout(false);
|
||||
this.inProgressFilesGb.PerformLayout();
|
||||
this.customFileNamingGb.ResumeLayout(false);
|
||||
this.customFileNamingGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
@@ -356,13 +547,11 @@
|
||||
private System.Windows.Forms.Label inProgressDescLbl;
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.GroupBox advancedSettingsGb;
|
||||
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private DirectorySelectControl inProgressSelectControl;
|
||||
private System.Windows.Forms.RadioButton convertLossyRb;
|
||||
private System.Windows.Forms.RadioButton convertLosslessRb;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
private System.Windows.Forms.Label loggingLevelLbl;
|
||||
private System.Windows.Forms.ComboBox loggingLevelCb;
|
||||
@@ -375,5 +564,22 @@
|
||||
private System.Windows.Forms.CheckBox downloadEpisodesCb;
|
||||
private System.Windows.Forms.CheckBox importEpisodesCb;
|
||||
private System.Windows.Forms.CheckBox splitFilesByChapterCbox;
|
||||
private System.Windows.Forms.TabControl tabControl;
|
||||
private System.Windows.Forms.TabPage tab1ImportantSettings;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.TabPage tab2ImportLibrary;
|
||||
private System.Windows.Forms.TabPage tab3DownloadDecrypt;
|
||||
private System.Windows.Forms.GroupBox inProgressFilesGb;
|
||||
private System.Windows.Forms.GroupBox customFileNamingGb;
|
||||
private System.Windows.Forms.Button chapterFileTemplateBtn;
|
||||
private System.Windows.Forms.TextBox chapterFileTemplateTb;
|
||||
private System.Windows.Forms.Label chapterFileTemplateLbl;
|
||||
private System.Windows.Forms.Button fileTemplateBtn;
|
||||
private System.Windows.Forms.TextBox fileTemplateTb;
|
||||
private System.Windows.Forms.Label fileTemplateLbl;
|
||||
private System.Windows.Forms.Button folderTemplateBtn;
|
||||
private System.Windows.Forms.TextBox folderTemplateTb;
|
||||
private System.Windows.Forms.Label folderTemplateLbl;
|
||||
private System.Windows.Forms.CheckBox showImportedStatsCb;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -26,6 +27,7 @@ namespace LibationWinForms.Dialogs
|
||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||
}
|
||||
|
||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
@@ -45,6 +47,7 @@ namespace LibationWinForms.Dialogs
|
||||
"Books");
|
||||
booksSelectControl.SelectDirectory(config.Books);
|
||||
|
||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
@@ -78,6 +81,13 @@ namespace LibationWinForms.Dialogs
|
||||
_ => this.badBookAskRb
|
||||
};
|
||||
rb.Checked = true;
|
||||
|
||||
folderTemplateLbl.Text = desc(nameof(config.FolderTemplate));
|
||||
fileTemplateLbl.Text = desc(nameof(config.FileTemplate));
|
||||
chapterFileTemplateLbl.Text = desc(nameof(config.ChapterFileTemplate));
|
||||
folderTemplateTb.Text = config.FolderTemplate;
|
||||
fileTemplateTb.Text = config.FileTemplate;
|
||||
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
||||
}
|
||||
|
||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||
@@ -95,28 +105,56 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
|
||||
|
||||
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
|
||||
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
|
||||
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
|
||||
private static void editTemplate(Templates template, TextBox textBox)
|
||||
{
|
||||
var form = new EditTemplateDialog(template, textBox.Text);
|
||||
if (form.ShowDialog() == DialogResult.OK)
|
||||
textBox.Text = form.TemplateText;
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
#region validation
|
||||
static void validationError(string text, string caption)
|
||||
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
if (string.IsNullOrWhiteSpace(newBooks))
|
||||
{
|
||||
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
validationError("Cannot set Books Location to blank", "Location is blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(newBooks))
|
||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
if (booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (booksSelectControl.SelectedDirectoryIsKnown)
|
||||
Directory.CreateDirectory(newBooks);
|
||||
validationError($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
||||
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
||||
return;
|
||||
}
|
||||
if (!Templates.File.IsValid(fileTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
||||
return;
|
||||
}
|
||||
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
||||
return;
|
||||
}
|
||||
#endregion
|
||||
|
||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsKnown)
|
||||
Directory.CreateDirectory(newBooks);
|
||||
|
||||
config.Books = newBooks;
|
||||
|
||||
{
|
||||
@@ -130,6 +168,7 @@ namespace LibationWinForms.Dialogs
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
@@ -145,6 +184,10 @@ namespace LibationWinForms.Dialogs
|
||||
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
||||
: Configuration.BadBookAction.Ask;
|
||||
|
||||
config.FolderTemplate = folderTemplateTb.Text;
|
||||
config.FileTemplate = fileTemplateTb.Text;
|
||||
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Drawing;
|
||||
using Dinah.Core.Threading;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
namespace LibationWinForms
|
||||
@@ -334,7 +334,8 @@ namespace LibationWinForms
|
||||
var totalProcessed = dialog.TotalBooksProcessed;
|
||||
var newAdded = dialog.NewBooksAdded;
|
||||
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
if (Configuration.Instance.ShowImportedStats)
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using FileManager;
|
||||
using System.Drawing;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user