mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-01 18:38:01 -05:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
153e1b92bf | ||
|
|
fc5ae7403a | ||
|
|
13149eff08 | ||
|
|
9c53d9bf87 | ||
|
|
bc9625fece | ||
|
|
7e00162ef2 | ||
|
|
af38750e29 | ||
|
|
314f4850bc | ||
|
|
9ff2a83ba3 | ||
|
|
2ab466c570 | ||
|
|
184ba84600 | ||
|
|
99dddb1af4 | ||
|
|
48eca3f5af | ||
|
|
71192cc2ee | ||
|
|
29c7344540 | ||
|
|
6411d23744 | ||
|
|
1a74736115 | ||
|
|
7c11ecb3a7 | ||
|
|
fd7c833de0 | ||
|
|
7fec8b0d7e | ||
|
|
52622fadbb | ||
|
|
57255e0aec | ||
|
|
17ecfa132d | ||
|
|
d1365c3d7d | ||
|
|
c33891a4bc | ||
|
|
9a63f57147 | ||
|
|
839a62cb07 | ||
|
|
dc598e466e | ||
|
|
b698697256 | ||
|
|
f802d1524f | ||
|
|
0cb18f9e1a | ||
|
|
ba722487d8 | ||
|
|
eff2634b32 | ||
|
|
1470aefd42 | ||
|
|
b7fd87b09c | ||
|
|
ab82a1656d | ||
|
|
71387e94d8 | ||
|
|
503379079b | ||
|
|
1ae767087f | ||
|
|
cfd2b7b7aa | ||
|
|
2c42b4c585 | ||
|
|
d3a9ff539e | ||
|
|
58f01bd642 | ||
|
|
38806740e1 | ||
|
|
df583e73c2 | ||
|
|
e787d33e5a | ||
|
|
91db665428 | ||
|
|
94d155cff2 |
@@ -27,7 +27,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
|
||||
|
||||
### Linux and Mac
|
||||
|
||||
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592))
|
||||
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592), [Linux and WINE](https://github.com/rmcrackan/Libation/issues/28#issuecomment-1161111014))
|
||||
|
||||
### Settings
|
||||
|
||||
|
||||
@@ -66,4 +66,4 @@
|
||||
|
||||
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
|
||||
|
||||
I made this for myself and I want to share it with the great programming and audible/audiobook communiites which have been so generous with their time and help.
|
||||
I made this for myself and I want to share it with the great programming and audible/audiobook communities which have been so generous with their time and help.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.9" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,31 +2,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
||||
private List<string> multiPartFilePaths { get; } = new List<string>();
|
||||
|
||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
|
||||
: base(outFileName, cacheDirectory, dlOptions) { }
|
||||
|
||||
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
|
||||
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
|
||||
["Step 3: Cleanup"] = Step_Cleanup,
|
||||
};
|
||||
public override async Task<bool> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
|
||||
//Step 1
|
||||
Serilog.Log.Information("Begin Get Aaxc Metadata");
|
||||
if (await Task.Run(Step_GetMetadata))
|
||||
Serilog.Log.Information("Completed Get Aaxc Metadata");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Get Aaxc Metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 2
|
||||
Serilog.Log.Information("Begin Download Decrypted Audiobook");
|
||||
if (await Step_DownloadAudiobookAsMultipleFilesPerChapter())
|
||||
Serilog.Log.Information("Completed Download Decrypted Audiobook");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Download Decrypted Audiobook");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 3
|
||||
Serilog.Log.Information("Begin Cleanup");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Cleanup");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Cleanup");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -53,7 +87,7 @@ The book will be split into the following files:
|
||||
|
||||
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()
|
||||
private async Task<bool> Step_DownloadAudiobookAsMultipleFilesPerChapter()
|
||||
{
|
||||
var zeroProgress = Step_DownloadAudiobook_Start();
|
||||
|
||||
@@ -86,9 +120,9 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
|
||||
result = ConvertToMultiMp4a(splitChapters);
|
||||
result = await ConvertToMultiMp4a(splitChapters);
|
||||
else
|
||||
result = ConvertToMultiMp3(splitChapters);
|
||||
result = await ConvertToMultiMp3(splitChapters);
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
Step_DownloadAudiobook_End(zeroProgress);
|
||||
@@ -96,10 +130,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
return result == ConversionResult.NoErrorsDetected;
|
||||
}
|
||||
|
||||
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||
private Task<ConversionResult> ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
return AaxFile.ConvertToMultiMp4a
|
||||
return AaxFile.ConvertToMultiMp4aAsync
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
|
||||
@@ -107,10 +141,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
);
|
||||
}
|
||||
|
||||
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
private Task<ConversionResult> ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
return AaxFile.ConvertToMultiMp3
|
||||
return AaxFile.ConvertToMultiMp3Async
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => Callback(++chapterCount, splitChapters, newSplitCallback),
|
||||
@@ -141,7 +175,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
|
||||
{
|
||||
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
|
||||
fileName = FileUtility.GetValidFilename(fileName);
|
||||
fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters);
|
||||
|
||||
multiPartFilePaths.Add(fileName);
|
||||
|
||||
|
||||
@@ -1,31 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
|
||||
: base(outFileName, cacheDirectory, dlOptions) { }
|
||||
|
||||
["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,
|
||||
};
|
||||
public override async Task<bool> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
|
||||
//Step 1
|
||||
Serilog.Log.Information("Begin Step 1: Get Aaxc Metadata");
|
||||
if (await Task.Run(Step_GetMetadata))
|
||||
Serilog.Log.Information("Completed Step 1: Get Aaxc Metadata");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 1: Get Aaxc Metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 2
|
||||
Serilog.Log.Information("Begin Step 2: Download Decrypted Audiobook");
|
||||
if (await Step_DownloadAudiobookAsSingleFile())
|
||||
Serilog.Log.Information("Completed Step 2: Download Decrypted Audiobook");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 2: Download Decrypted Audiobook");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 3
|
||||
Serilog.Log.Information("Begin Step 3: Create Cue");
|
||||
if (await Task.Run(Step_CreateCue))
|
||||
Serilog.Log.Information("Completed Step 3: Create Cue");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 3: Create Cue");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 4
|
||||
Serilog.Log.Information("Begin Step 4: Cleanup");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Step 4: Cleanup");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 4: Cleanup");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Step_DownloadAudiobookAsSingleFile()
|
||||
private async Task<bool> Step_DownloadAudiobookAsSingleFile()
|
||||
{
|
||||
var zeroProgress = Step_DownloadAudiobook_Start();
|
||||
|
||||
@@ -37,8 +80,8 @@ namespace AaxDecrypter
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
var decryptionResult
|
||||
= DownloadOptions.OutputFormat == OutputFormat.M4b
|
||||
? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength)
|
||||
: AaxFile.ConvertToMp3(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength);
|
||||
? await AaxFile.ConvertToMp4aAsync(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength)
|
||||
: await AaxFile.ConvertToMp3Async(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength);
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
DownloadOptions.ChapterInfo = AaxFile.Chapters;
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
@@ -31,7 +30,6 @@ namespace AaxDecrypter
|
||||
// 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; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
|
||||
private string jsonDownloadState { get; }
|
||||
@@ -64,15 +62,7 @@ namespace AaxDecrypter
|
||||
OnRetrievedCoverArt(coverArt);
|
||||
}
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, _) = Steps.Run();
|
||||
|
||||
if (!IsSuccess)
|
||||
Serilog.Log.Logger.Error("Conversion failed");
|
||||
|
||||
return IsSuccess;
|
||||
}
|
||||
public abstract Task<bool> RunAsync();
|
||||
|
||||
protected void OnRetrievedTitle(string title)
|
||||
=> RetrievedTitle?.Invoke(this, title);
|
||||
@@ -103,7 +93,7 @@ namespace AaxDecrypter
|
||||
try
|
||||
{
|
||||
var path = Path.ChangeExtension(OutputFileName, ".cue");
|
||||
path = FileUtility.GetValidFilename(path);
|
||||
path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters);
|
||||
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
|
||||
OnFileCreated(path);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
public interface IDownloadOptions
|
||||
{
|
||||
FileManager.ReplacementCharacters ReplacementCharacters { get; }
|
||||
string DownloadUrl { get; }
|
||||
string UserAgent { get; }
|
||||
string AudibleKey { get; }
|
||||
|
||||
@@ -11,15 +11,5 @@ namespace AaxDecrypter
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,14 @@ namespace AaxDecrypter
|
||||
private void Update()
|
||||
{
|
||||
RequestHeaders = HttpRequest.Headers;
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
try
|
||||
{
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "An error was encountered while saving the download progress to JSON");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -211,7 +218,7 @@ namespace AaxDecrypter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
|
||||
/// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.
|
||||
/// </summary>
|
||||
private void DownloadFile()
|
||||
{
|
||||
@@ -245,9 +252,6 @@ namespace AaxDecrypter
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
|
||||
if (!IsCancelled && WritePosition < ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
|
||||
@@ -257,7 +261,11 @@ namespace AaxDecrypter
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
|
||||
IsCancelled = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +409,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
if (!hasBegunDownloading)
|
||||
BeginDownloading();
|
||||
|
||||
|
||||
var toRead = Math.Min(count, Length - Position);
|
||||
WaitToPosition(Position + toRead);
|
||||
return _readFile.Read(buffer, offset, count);
|
||||
@@ -426,14 +434,20 @@ namespace AaxDecrypter
|
||||
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
|
||||
private void WaitToPosition(long requiredPosition)
|
||||
{
|
||||
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
|
||||
while (WritePosition < requiredPosition
|
||||
&& hasBegunDownloading
|
||||
&& !IsCancelled
|
||||
&& !downloadEnded.WaitOne(0))
|
||||
{
|
||||
downloadedPiece.WaitOne(100);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
IsCancelled = true;
|
||||
|
||||
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
|
||||
while (downloadEnded is not null && !downloadEnded.WaitOne(100)) ;
|
||||
|
||||
_readFile.Close();
|
||||
_writeFile.Close();
|
||||
|
||||
@@ -2,29 +2,63 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
|
||||
: base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
{
|
||||
Name = "Download Mp3 Audiobook",
|
||||
: base(outFileName, cacheDirectory, dlLic) { }
|
||||
|
||||
["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 async Task<bool> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
|
||||
//Step 1
|
||||
Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata");
|
||||
if (await Task.Run(Step_GetMetadata))
|
||||
Serilog.Log.Information("Completed Step 1: Get Mp3 Metadata");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 1: Get Mp3 Metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 2
|
||||
Serilog.Log.Information("Begin Step 2: Download Audiobook");
|
||||
if (await Task.Run(Step_DownloadAudiobookAsSingleFile))
|
||||
Serilog.Log.Information("Completed Step 2: Download Audiobook");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 2: Download Audiobook");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Step 3
|
||||
Serilog.Log.Information("Begin Step 3: Cleanup");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Step 3: Cleanup");
|
||||
else
|
||||
{
|
||||
Serilog.Log.Information("Failed to Complete Step 3: Cleanup");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serilog.Log.Information("Completed download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error encountered in download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
@@ -67,8 +101,8 @@ namespace AaxDecrypter
|
||||
}
|
||||
|
||||
CloseInputFileStream();
|
||||
|
||||
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName);
|
||||
|
||||
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters);
|
||||
SetOutputFileName(realOutputFileName);
|
||||
OnFileCreated(realOutputFileName);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>8.0.3.1</Version>
|
||||
<Version>8.1.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -117,6 +117,9 @@ namespace AppScaffolding
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.ReplacementCharacters)))
|
||||
config.ReplacementCharacters = FileManager.ReplacementCharacters.Default;
|
||||
|
||||
if (!config.Exists(nameof(config.FolderTemplate)))
|
||||
config.FolderTemplate = Templates.Folder.DefaultTemplate;
|
||||
|
||||
@@ -470,7 +473,7 @@ namespace AppScaffolding
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occured while running database migrations in {0}", nameof(migrate_from_7_10_1));
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while running database migrations in {0}", nameof(migrate_from_7_10_1));
|
||||
config.SetObject($"{nameof(migrate_from_7_10_1)}_ThrewError", true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace ApplicationServices
|
||||
if (newParents >= 0)
|
||||
{
|
||||
//If any episodes are still orphaned, their series have been
|
||||
//removed from the catalog and wel'll never be able to find them.
|
||||
//removed from the catalog and we'll never be able to find them.
|
||||
|
||||
//only do this if findAndAddMissingParents returned >= 0. If it
|
||||
//returned < 0, an error happened and there's still a chance that
|
||||
@@ -251,7 +251,7 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occured while trying to remove orphaned episodes from the database");
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace ApplicationServices
|
||||
.DistinctBy(s => s.Series.AudibleSeriesId)
|
||||
.ToList();
|
||||
|
||||
// The Catalog endpointdoes not require authentication.
|
||||
// The Catalog endpoint does not require authentication.
|
||||
var api = new ApiUnauthenticated(accounts[0].Locale);
|
||||
|
||||
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
|
||||
@@ -308,7 +308,7 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occured while trying to scan for orphaned episode parents.");
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -321,7 +321,7 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
|
||||
{
|
||||
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension
|
||||
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension
|
||||
|
||||
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace AudibleUtilities
|
||||
|
||||
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
|
||||
|
||||
//await and add all episides from all parents
|
||||
//await and add all episodes from all parents
|
||||
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
|
||||
items.AddRange(epList);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="4.0.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="4.2.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -101,7 +101,8 @@ namespace DtoImporterService
|
||||
// absence of categories is also possible
|
||||
|
||||
// CATEGORY HACK: only use the 1st 2 categories
|
||||
// (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";)
|
||||
// after we support full arbitrary-depth category trees and multiple categories per book, the real impl will be something like this
|
||||
// var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";
|
||||
var lastCategory
|
||||
= item.Categories.Length == 0 ? ""
|
||||
: item.Categories.Length == 1 ? item.Categories[0].CategoryId
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace FileLiberator
|
||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
||||
if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue;
|
||||
|
||||
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
||||
m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
|
||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||
|
||||
fileSize = m4bBook.InputStream.Length;
|
||||
@@ -55,7 +55,7 @@ namespace FileLiberator
|
||||
|
||||
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
||||
var lameConfig = GetLameOptions(Configuration.Instance);
|
||||
var result = await Task.Run(() => m4bBook.ConvertToMp3(mp3File, lameConfig));
|
||||
var result = await m4bBook.ConvertToMp3Async(mp3File, lameConfig);
|
||||
m4bBook.InputStream.Close();
|
||||
mp3File.Close();
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace FileLiberator
|
||||
return new StatusHandler { "Cancelled" };
|
||||
}
|
||||
|
||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
|
||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters);
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
}
|
||||
return new StatusHandler();
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace FileLiberator
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(abDownloader.Run);
|
||||
var success = await abDownloader.RunAsync();
|
||||
|
||||
return success;
|
||||
}
|
||||
@@ -165,6 +165,8 @@ namespace FileLiberator
|
||||
LameConfig = GetLameOptions(config)
|
||||
};
|
||||
|
||||
var chapters = getChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
|
||||
|
||||
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
|
||||
{
|
||||
long startMs = dlOptions.TrimOutputToChapterLength ?
|
||||
@@ -172,15 +174,15 @@ namespace FileLiberator
|
||||
|
||||
dlOptions.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs));
|
||||
|
||||
for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++)
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
var chapter = contentLic.ContentMetadata.ChapterInfo.Chapters[i];
|
||||
var chapter = chapters[i];
|
||||
long chapLenMs = chapter.LengthMs;
|
||||
|
||||
if (i == 0)
|
||||
chapLenMs -= startMs;
|
||||
|
||||
if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1)
|
||||
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
||||
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||
|
||||
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||
@@ -190,6 +192,37 @@ namespace FileLiberator
|
||||
return dlOptions;
|
||||
}
|
||||
|
||||
private List<AudibleApi.Common.Chapter> getChapters(IEnumerable<AudibleApi.Common.Chapter> chapters)
|
||||
{
|
||||
List<AudibleApi.Common.Chapter> chaps = new();
|
||||
|
||||
foreach (var c in chapters)
|
||||
{
|
||||
if (c.Chapters is not null)
|
||||
{
|
||||
var firstSub = new AudibleApi.Common.Chapter
|
||||
{
|
||||
Title = $"{c.Title}: {c.Chapters[0].Title}",
|
||||
StartOffsetMs = c.StartOffsetMs,
|
||||
StartOffsetSec = c.StartOffsetSec,
|
||||
LengthMs = c.LengthMs + c.Chapters[0].LengthMs
|
||||
};
|
||||
|
||||
chaps.Add(firstSub);
|
||||
|
||||
var children = getChapters(c.Chapters[1..]);
|
||||
|
||||
foreach (var child in children)
|
||||
child.Title = string.IsNullOrEmpty(c.Title) ? child.Title : $"{c.Title}: {child.Title}";
|
||||
|
||||
chaps.AddRange(children);
|
||||
}
|
||||
else
|
||||
chaps.Add(c);
|
||||
}
|
||||
return chaps;
|
||||
}
|
||||
|
||||
private static void downloadValidation(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
@@ -237,10 +270,10 @@ namespace FileLiberator
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
|
||||
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)
|
||||
// propagate 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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using AAXClean;
|
||||
using Dinah.Core;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -23,6 +24,7 @@ namespace FileLiberator
|
||||
public NAudio.Lame.LameConfig LameConfig { get; set; }
|
||||
public bool Downsample { get; set; }
|
||||
public bool MatchSourceBitrate { get; set; }
|
||||
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
|
||||
|
||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||
|
||||
@@ -79,8 +79,13 @@ namespace FileManager
|
||||
//Stop raising events
|
||||
fileSystemWatcher?.Dispose();
|
||||
|
||||
//Calling CompleteAdding() will cause background scanner to terminate.
|
||||
directoryChangesEvents?.CompleteAdding();
|
||||
try
|
||||
{
|
||||
//Calling CompleteAdding() will cause background scanner to terminate.
|
||||
directoryChangesEvents?.CompleteAdding();
|
||||
}
|
||||
// if directoryChangesEvents is non-null and isDisposed, this exception is thrown. there's no other way to check >:(
|
||||
catch (ObjectDisposedException) { }
|
||||
|
||||
//Wait for background scanner to terminate before reinitializing.
|
||||
backgroundScanner?.Wait();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="4.4.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="4.4.1.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -11,24 +12,46 @@ namespace FileManager
|
||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileNamingTemplate(string template) : base(template) { }
|
||||
|
||||
/// <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 LongPath GetFilePath(bool returnFirstExisting = false)
|
||||
public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
{
|
||||
int lastSlash = Template.LastIndexOf('\\');
|
||||
string fileName =
|
||||
Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ?
|
||||
FileUtility.RemoveLastCharacter(Template) :
|
||||
Template;
|
||||
|
||||
var directoryName = lastSlash >= 0 ? Template[..(lastSlash + 1)] : string.Empty;
|
||||
var filename = lastSlash >= 0 ? Template[(lastSlash + 1)..] : Template;
|
||||
List<string> pathParts = new();
|
||||
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements));
|
||||
|
||||
while (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
var file = Path.GetFileName(fileName);
|
||||
|
||||
if (Path.IsPathRooted(Template) && file == string.Empty)
|
||||
{
|
||||
pathParts.Add(fileName);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
file = replaceFileName(file, paramReplacements);
|
||||
fileName = Path.GetDirectoryName(fileName);
|
||||
pathParts.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
pathParts.Reverse();
|
||||
|
||||
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting);
|
||||
}
|
||||
|
||||
private string replaceFileName(string filename, Dictionary<string,string> paramReplacements)
|
||||
{
|
||||
List<StringBuilder> filenameParts = new();
|
||||
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value));
|
||||
|
||||
//Build the filename in parts, replacing replacement parameters with
|
||||
//their values, and storing the parts in a list.
|
||||
while(!string.IsNullOrEmpty(filename))
|
||||
while (!string.IsNullOrEmpty(filename))
|
||||
{
|
||||
int openIndex = filename.IndexOf('<');
|
||||
int closeIndex = filename.IndexOf('>');
|
||||
@@ -59,30 +82,24 @@ namespace FileManager
|
||||
|
||||
//Remove 1 character from the end of the longest filename part until
|
||||
//the total filename is less than max filename length
|
||||
while(filenameParts.Sum(p => p.Length) > LongPath.MaxFilenameLength)
|
||||
while (filenameParts.Sum(p => p.Length) > LongPath.MaxFilenameLength)
|
||||
{
|
||||
int maxLength = filenameParts.Max(p => p.Length);
|
||||
var maxEntry = filenameParts.First(p => p.Length == maxLength);
|
||||
|
||||
maxEntry.Remove(maxLength - 1, 1);
|
||||
}
|
||||
|
||||
filename = string.Join("", filenameParts);
|
||||
|
||||
return FileUtility.GetValidFilename(directoryName + filename, IllegalCharacterReplacements, returnFirstExisting);
|
||||
return string.Join("", filenameParts);
|
||||
}
|
||||
|
||||
private string formatValue(object value)
|
||||
private string formatValue(object value, ReplacementCharacters replacements)
|
||||
{
|
||||
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.
|
||||
return value
|
||||
.ToString()
|
||||
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
||||
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
||||
return replacements.ReplaceInvalidFilenameChars(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ namespace FileManager
|
||||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
|
||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, illegalCharacterReplacements);
|
||||
path = GetSafePath(path, replacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
@@ -77,36 +77,19 @@ namespace FileManager
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
// 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 LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "")
|
||||
/// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
var pathNoPrefix = path.PathWithoutPrefix;
|
||||
|
||||
pathNoPrefix = replaceColons(pathNoPrefix, "꞉");
|
||||
pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix);
|
||||
pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements);
|
||||
pathNoPrefix = replacements.ReplaceInvalidPathChars(pathNoPrefix);
|
||||
pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
|
||||
|
||||
return pathNoPrefix;
|
||||
}
|
||||
|
||||
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 removeDoubleSlashes(string path)
|
||||
{
|
||||
if (path.Length < 2)
|
||||
@@ -122,60 +105,6 @@ namespace FileManager
|
||||
return path[0] + remainder;
|
||||
}
|
||||
|
||||
private static string replaceIllegalWithUnicodeAnalog(string path)
|
||||
{
|
||||
char[] replaced = path.ToCharArray();
|
||||
|
||||
char GetQuote(int position)
|
||||
{
|
||||
if (
|
||||
position == 0
|
||||
|| (position > 0
|
||||
&& position < replaced.Length
|
||||
&& !char.IsLetter(replaced[position - 1])
|
||||
&& !char.IsNumber(replaced[position - 1])
|
||||
)
|
||||
) return '“';
|
||||
else if (
|
||||
position == replaced.Length - 1
|
||||
|| (position >= 0
|
||||
&& position < replaced.Length - 1
|
||||
&& !char.IsLetter(replaced[position + 1])
|
||||
&& !char.IsNumber(replaced[position + 1])
|
||||
)
|
||||
) return '”';
|
||||
else return '"';
|
||||
}
|
||||
|
||||
for (int i = 0; i < replaced.Length; i++)
|
||||
{
|
||||
replaced[i] = replaced[i] switch
|
||||
{
|
||||
'?' => '?',
|
||||
'*' => '✱',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'"' => GetQuote(i),
|
||||
_ => replaced[i]
|
||||
};
|
||||
}
|
||||
return new string(replaced);
|
||||
}
|
||||
|
||||
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 removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
|
||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
@@ -191,7 +120,7 @@ namespace FileManager
|
||||
// regex is easier by ending with separator
|
||||
fullfilename += Path.DirectorySeparatorChar;
|
||||
fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, Path.DirectorySeparatorChar.ToString());
|
||||
// take seperator back off
|
||||
// take separator back off
|
||||
fullfilename = RemoveLastCharacter(fullfilename);
|
||||
|
||||
fullfilename = removeDoubleSlashes(fullfilename);
|
||||
@@ -206,9 +135,9 @@ namespace FileManager
|
||||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination)
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
|
||||
{
|
||||
destination = GetValidFilename(destination);
|
||||
destination = GetValidFilename(destination, replacements);
|
||||
SaferMove(source, destination);
|
||||
return destination;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace FileManager
|
||||
|
||||
private const int MAX_PATH = 260;
|
||||
private const string LONG_PATH_PREFIX = "\\\\?\\";
|
||||
private static readonly StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
|
||||
public string Path { get; init; }
|
||||
public override string ToString() => Path;
|
||||
@@ -71,18 +70,24 @@ namespace FileManager
|
||||
//
|
||||
// "Based on the above settings, 8dot3 name creation is [enabled/disabled] on c:"
|
||||
//
|
||||
//To enable short names on all volumes on the system, run the following command
|
||||
//To enable short names on a volume on the system, run the following command
|
||||
//from an elevated command prompt:
|
||||
//
|
||||
// fsutil 8dot3name set c: 0
|
||||
//
|
||||
//or for all volumes on the system:
|
||||
//
|
||||
// fsutil 8dot3name set 0
|
||||
//
|
||||
//Note that after enabling 8dot3 names on a volume, they will only be available
|
||||
//for newly-created entries in ther file system. Existing entries made while
|
||||
//8dot3 names were disabled will not be reachable by short paths.
|
||||
|
||||
if (Path is null) return null;
|
||||
GetShortPathName(Path, longPathBuffer, MaxPathLength);
|
||||
return longPathBuffer.ToString();
|
||||
|
||||
StringBuilder shortPathBuffer = new(MaxPathLength);
|
||||
GetShortPathName(Path, shortPathBuffer, MaxPathLength);
|
||||
return shortPathBuffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +97,8 @@ namespace FileManager
|
||||
get
|
||||
{
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
GetLongPathName(Path, longPathBuffer, MaxPathLength);
|
||||
return longPathBuffer.ToString();
|
||||
}
|
||||
|
||||
271
Source/FileManager/ReplacementCharacters.cs
Normal file
271
Source/FileManager/ReplacementCharacters.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class Replacement
|
||||
{
|
||||
public const int FIXED_COUNT = 6;
|
||||
|
||||
internal const char QUOTE_MARK = '"';
|
||||
[JsonIgnore] public bool Mandatory { get; internal set; }
|
||||
[JsonProperty] public char CharacterToReplace { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; private set; }
|
||||
[JsonProperty] public string Description { get; private set; }
|
||||
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
|
||||
|
||||
public Replacement(char charToReplace, string replacementString, string description)
|
||||
{
|
||||
CharacterToReplace = charToReplace;
|
||||
ReplacementString = replacementString;
|
||||
Description = description;
|
||||
}
|
||||
private Replacement(char charToReplace, string replacementString, string description, bool mandatory)
|
||||
: this(charToReplace, replacementString, description)
|
||||
{
|
||||
Mandatory = mandatory;
|
||||
}
|
||||
|
||||
public void Update(char charToReplace, string replacementString, string description)
|
||||
{
|
||||
ReplacementString = replacementString;
|
||||
|
||||
if (!Mandatory)
|
||||
{
|
||||
CharacterToReplace = charToReplace;
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
|
||||
public static Replacement OtherInvalid(string replacement) => new(default, replacement, "All other invalid characters", true);
|
||||
public static Replacement FilenameForwardSlash(string replacement) => new('/', replacement, "Forward Slash (Filename Only)", true);
|
||||
public static Replacement FilenameBackSlash(string replacement) => new('\\', replacement, "Back Slash (Filename Only)", true);
|
||||
public static Replacement OpenQuote(string replacement) => new('"', replacement, "Open Quote", true);
|
||||
public static Replacement CloseQuote(string replacement) => new('"', replacement, "Close Quote", true);
|
||||
public static Replacement OtherQuote(string replacement) => new('"', replacement, "Other Quote", true);
|
||||
public static Replacement Colon(string replacement) => new(':', replacement, "Colon");
|
||||
public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk");
|
||||
public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark");
|
||||
public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket");
|
||||
public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket");
|
||||
public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line");
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||
public class ReplacementCharacters
|
||||
{
|
||||
public static readonly ReplacementCharacters Default = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("∕"),
|
||||
Replacement.FilenameBackSlash(""),
|
||||
Replacement.OpenQuote("“"),
|
||||
Replacement.CloseQuote("”"),
|
||||
Replacement.OtherQuote("""),
|
||||
Replacement.OpenAngleBracket("<"),
|
||||
Replacement.CloseAngleBracket(">"),
|
||||
Replacement.Colon("꞉"),
|
||||
Replacement.Asterisk("✱"),
|
||||
Replacement.QuestionMark("?"),
|
||||
Replacement.Pipe("⏐"),
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters LoFiDefault = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
Replacement.OpenQuote("'"),
|
||||
Replacement.CloseQuote("'"),
|
||||
Replacement.OtherQuote("'"),
|
||||
Replacement.OpenAngleBracket("{"),
|
||||
Replacement.CloseAngleBracket("}"),
|
||||
Replacement.Colon("-"),
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters Barebones = new()
|
||||
{
|
||||
Replacements = new List<Replacement>()
|
||||
{
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
Replacement.OpenQuote("_"),
|
||||
Replacement.CloseQuote("_"),
|
||||
Replacement.OtherQuote("_"),
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly char[] invalidChars = 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();
|
||||
|
||||
public IReadOnlyList<Replacement> Replacements { get; init; }
|
||||
private string DefaultReplacement => Replacements[0].ReplacementString;
|
||||
private Replacement ForwardSlash => Replacements[1];
|
||||
private Replacement BackSlash => Replacements[2];
|
||||
private string OpenQuote => Replacements[3].ReplacementString;
|
||||
private string CloseQuote => Replacements[4].ReplacementString;
|
||||
private string OtherQuote => Replacements[5].ReplacementString;
|
||||
|
||||
private string GetFilenameCharReplacement(char toReplace, char preceding, char succeding)
|
||||
{
|
||||
if (toReplace == ForwardSlash.CharacterToReplace)
|
||||
return ForwardSlash.ReplacementString;
|
||||
else if (toReplace == BackSlash.CharacterToReplace)
|
||||
return BackSlash.ReplacementString;
|
||||
else return GetPathCharReplacement(toReplace, preceding, succeding);
|
||||
}
|
||||
private string GetPathCharReplacement(char toReplace, char preceding, char succeding)
|
||||
{
|
||||
if (toReplace == Replacement.QUOTE_MARK)
|
||||
{
|
||||
if (preceding == default ||
|
||||
(preceding != default
|
||||
&& !char.IsLetter(preceding)
|
||||
&& !char.IsNumber(preceding)
|
||||
&& (char.IsLetter(succeding) || char.IsNumber(succeding))
|
||||
)
|
||||
)
|
||||
return OpenQuote;
|
||||
else if (succeding == default ||
|
||||
(succeding != default
|
||||
&& !char.IsLetter(succeding)
|
||||
&& !char.IsNumber(succeding)
|
||||
&& (char.IsLetter(preceding) || char.IsNumber(preceding))
|
||||
)
|
||||
)
|
||||
return CloseQuote;
|
||||
else
|
||||
return OtherQuote;
|
||||
}
|
||||
|
||||
for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++)
|
||||
{
|
||||
var r = Replacements[i];
|
||||
if (r.CharacterToReplace == toReplace)
|
||||
return r.ReplacementString;
|
||||
}
|
||||
return DefaultReplacement;
|
||||
}
|
||||
|
||||
|
||||
public static bool ContainsInvalid(string path)
|
||||
=> path.Any(c => invalidChars.Contains(c));
|
||||
|
||||
public string ReplaceInvalidFilenameChars(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName)) return string.Empty;
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < fileName.Length; i++)
|
||||
{
|
||||
var c = fileName[i];
|
||||
|
||||
if (invalidChars.Contains(c) || c == ForwardSlash.CharacterToReplace || c == BackSlash.CharacterToReplace)
|
||||
{
|
||||
char preceding = i > 0 ? fileName[i - 1] : default;
|
||||
char succeeding = i < fileName.Length - 1 ? fileName[i + 1] : default;
|
||||
builder.Append(GetFilenameCharReplacement(c, preceding, succeeding));
|
||||
}
|
||||
else
|
||||
builder.Append(c);
|
||||
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public string ReplaceInvalidPathChars(string pathStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pathStr)) return string.Empty;
|
||||
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < pathStr.Length; i++)
|
||||
{
|
||||
var c = pathStr[i];
|
||||
|
||||
if (!invalidChars.Contains(c) || (c == ':' && i == 1 && Path.IsPathRooted(pathStr)))
|
||||
builder.Append(c);
|
||||
else
|
||||
{
|
||||
char preceding = i > 0 ? pathStr[i - 1] : default;
|
||||
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
|
||||
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
|
||||
}
|
||||
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#region JSON Converter
|
||||
internal class ReplacementCharactersConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(ReplacementCharacters);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var replaceArr = jObj[nameof(Replacement)];
|
||||
IReadOnlyList<Replacement> dict = replaceArr
|
||||
.ToObject<Replacement[]>().ToList();
|
||||
|
||||
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
||||
//If not, reset to default.
|
||||
|
||||
var default0 = Replacement.OtherInvalid("");
|
||||
var default1 = Replacement.FilenameForwardSlash("");
|
||||
var default2 = Replacement.FilenameBackSlash("");
|
||||
var default3 = Replacement.OpenQuote("");
|
||||
var default4 = Replacement.CloseQuote("");
|
||||
var default5 = Replacement.OtherQuote("");
|
||||
|
||||
if (dict.Count < Replacement.FIXED_COUNT ||
|
||||
dict[0].CharacterToReplace != default0.CharacterToReplace || dict[0].Description != default0.Description ||
|
||||
dict[1].CharacterToReplace != default1.CharacterToReplace || dict[1].Description != default1.Description ||
|
||||
dict[2].CharacterToReplace != default2.CharacterToReplace || dict[2].Description != default2.Description ||
|
||||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
|
||||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
|
||||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString))
|
||||
)
|
||||
{
|
||||
dict = ReplacementCharacters.Default.Replacements;
|
||||
}
|
||||
//First FIXED_COUNT are mandatory
|
||||
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
|
||||
dict[i].Mandatory = true;
|
||||
|
||||
return new ReplacementCharacters { Replacements = dict };
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
ReplacementCharacters replacements = (ReplacementCharacters)value;
|
||||
|
||||
var propertyNames = replacements.Replacements
|
||||
.Select(c => JObject.FromObject(c)).ToList();
|
||||
|
||||
var prop = new JProperty(nameof(Replacement), new JArray(propertyNames));
|
||||
|
||||
var obj = new JObject();
|
||||
obj.AddFirst(prop);
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -284,6 +284,13 @@ namespace LibationFileManager
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("Edit how illegal filename characters are replaced")]
|
||||
public ReplacementCharacters ReplacementCharacters
|
||||
{
|
||||
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
|
||||
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
|
||||
}
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
|
||||
@@ -54,10 +54,7 @@ namespace LibationFileManager
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
if (template.Contains(':')
|
||||
|| template.Contains(Path.DirectorySeparatorChar)
|
||||
|| template.Contains(Path.AltDirectorySeparatorChar)
|
||||
)
|
||||
if (ReplacementCharacters.ContainsInvalid(template.Replace("<","").Replace(">","")))
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
@@ -106,7 +103,7 @@ namespace LibationFileManager
|
||||
=> string.IsNullOrWhiteSpace(template)
|
||||
? ""
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
||||
.GetFilePath().PathWithoutPrefix;
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix;
|
||||
|
||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
@@ -126,7 +123,7 @@ namespace LibationFileManager
|
||||
var t = template + FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
@@ -200,6 +197,10 @@ namespace LibationFileManager
|
||||
if (template.Contains(':'))
|
||||
return new[] { ERROR_FULL_PATH_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 (ReplacementCharacters.ContainsInvalid(template.Replace("<", "").Replace(">", "")))
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ namespace LibationFileManager
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null)
|
||||
.GetFilePath();
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ namespace LibationFileManager
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
|
||||
.GetFilePath(returnFirstExisting);
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -268,8 +269,9 @@ namespace LibationFileManager
|
||||
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)
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
|
||||
{
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
@@ -277,7 +279,7 @@ namespace LibationFileManager
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilePath(replacements).PathWithoutPrefix;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
MessageBoxLib.ShowAdminAlert(
|
||||
this,
|
||||
$"An error occured while exporting account:\r\n{account.AccountName}",
|
||||
$"An error occurred while exporting account:\r\n{account.AccountName}",
|
||||
"Error Exporting Account",
|
||||
ex);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
MessageBoxLib.ShowAdminAlert(
|
||||
this,
|
||||
$"An error occured while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?",
|
||||
$"An error occurred while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?",
|
||||
"Error Importing Account",
|
||||
ex);
|
||||
}
|
||||
|
||||
171
Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs
generated
Normal file
171
Source/LibationWinForms/Dialogs/EditReplacementChars.Designer.cs
generated
Normal file
@@ -0,0 +1,171 @@
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class EditReplacementChars
|
||||
{
|
||||
/// <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.dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
this.defaultsBtn = new System.Windows.Forms.Button();
|
||||
this.loFiDefaultsBtn = new System.Windows.Forms.Button();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.minDefaultBtn = new System.Windows.Forms.Button();
|
||||
this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// dataGridView1
|
||||
//
|
||||
this.dataGridView1.AllowUserToResizeColumns = false;
|
||||
this.dataGridView1.AllowUserToResizeRows = false;
|
||||
this.dataGridView1.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.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.charToReplaceCol,
|
||||
this.replacementStringCol,
|
||||
this.descriptionCol});
|
||||
this.dataGridView1.Location = new System.Drawing.Point(12, 12);
|
||||
this.dataGridView1.Name = "dataGridView1";
|
||||
this.dataGridView1.RowTemplate.Height = 25;
|
||||
this.dataGridView1.Size = new System.Drawing.Size(498, 393);
|
||||
this.dataGridView1.TabIndex = 0;
|
||||
this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit);
|
||||
this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow);
|
||||
this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize);
|
||||
//
|
||||
// defaultsBtn
|
||||
//
|
||||
this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.defaultsBtn.Location = new System.Drawing.Point(12, 430);
|
||||
this.defaultsBtn.Name = "defaultsBtn";
|
||||
this.defaultsBtn.Size = new System.Drawing.Size(64, 25);
|
||||
this.defaultsBtn.TabIndex = 1;
|
||||
this.defaultsBtn.Text = "Defaults";
|
||||
this.defaultsBtn.UseVisualStyleBackColor = true;
|
||||
this.defaultsBtn.Click += new System.EventHandler(this.defaultsBtn_Click);
|
||||
//
|
||||
// loFiDefaultsBtn
|
||||
//
|
||||
this.loFiDefaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.loFiDefaultsBtn.Location = new System.Drawing.Point(82, 430);
|
||||
this.loFiDefaultsBtn.Name = "loFiDefaultsBtn";
|
||||
this.loFiDefaultsBtn.Size = new System.Drawing.Size(84, 25);
|
||||
this.loFiDefaultsBtn.TabIndex = 1;
|
||||
this.loFiDefaultsBtn.Text = "LoFi Defaults";
|
||||
this.loFiDefaultsBtn.UseVisualStyleBackColor = true;
|
||||
this.loFiDefaultsBtn.Click += new System.EventHandler(this.loFiDefaultsBtn_Click);
|
||||
//
|
||||
// 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(428, 430);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(82, 25);
|
||||
this.saveBtn.TabIndex = 1;
|
||||
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.Location = new System.Drawing.Point(340, 430);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(82, 25);
|
||||
this.cancelBtn.TabIndex = 1;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// minDefaultBtn
|
||||
//
|
||||
this.minDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.minDefaultBtn.Location = new System.Drawing.Point(172, 430);
|
||||
this.minDefaultBtn.Name = "minDefaultBtn";
|
||||
this.minDefaultBtn.Size = new System.Drawing.Size(80, 25);
|
||||
this.minDefaultBtn.TabIndex = 1;
|
||||
this.minDefaultBtn.Text = "Barebones";
|
||||
this.minDefaultBtn.UseVisualStyleBackColor = true;
|
||||
this.minDefaultBtn.Click += new System.EventHandler(this.minDefaultBtn_Click);
|
||||
//
|
||||
// charToReplaceCol
|
||||
//
|
||||
this.charToReplaceCol.HeaderText = "Char to Replace";
|
||||
this.charToReplaceCol.MinimumWidth = 70;
|
||||
this.charToReplaceCol.Name = "charToReplaceCol";
|
||||
this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.charToReplaceCol.Width = 70;
|
||||
//
|
||||
// replacementStringCol
|
||||
//
|
||||
this.replacementStringCol.HeaderText = "Replacement Text";
|
||||
this.replacementStringCol.MinimumWidth = 85;
|
||||
this.replacementStringCol.Name = "replacementStringCol";
|
||||
this.replacementStringCol.Width = 85;
|
||||
//
|
||||
// descriptionCol
|
||||
//
|
||||
this.descriptionCol.HeaderText = "Description";
|
||||
this.descriptionCol.MinimumWidth = 100;
|
||||
this.descriptionCol.Name = "descriptionCol";
|
||||
this.descriptionCol.Width = 200;
|
||||
//
|
||||
// EditReplacementChars
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(522, 467);
|
||||
this.Controls.Add(this.minDefaultBtn);
|
||||
this.Controls.Add(this.loFiDefaultsBtn);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.Controls.Add(this.defaultsBtn);
|
||||
this.Controls.Add(this.dataGridView1);
|
||||
this.Name = "EditReplacementChars";
|
||||
this.Text = "Character Replacements";
|
||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.Button defaultsBtn;
|
||||
private System.Windows.Forms.Button loFiDefaultsBtn;
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.Button minDefaultBtn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol;
|
||||
}
|
||||
}
|
||||
144
Source/LibationWinForms/Dialogs/EditReplacementChars.cs
Normal file
144
Source/LibationWinForms/Dialogs/EditReplacementChars.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class EditReplacementChars : Form
|
||||
{
|
||||
Configuration config;
|
||||
public EditReplacementChars()
|
||||
{
|
||||
InitializeComponent();
|
||||
dataGridView1_Resize(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public EditReplacementChars(Configuration config) : this()
|
||||
{
|
||||
this.config = config;
|
||||
LoadTable(config.ReplacementCharacters.Replacements);
|
||||
}
|
||||
|
||||
private void LoadTable(IReadOnlyList<Replacement> replacements)
|
||||
{
|
||||
dataGridView1.Rows.Clear();
|
||||
for (int i = 0; i < replacements.Count; i++)
|
||||
{
|
||||
var r = replacements[i];
|
||||
|
||||
int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description);
|
||||
dataGridView1.Rows[row].Tag = r;
|
||||
|
||||
|
||||
if (r.Mandatory)
|
||||
{
|
||||
dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true;
|
||||
dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true;
|
||||
dataGridView1.Rows[row].Cells[charToReplaceCol.Index].Style.BackColor = System.Drawing.Color.LightGray;
|
||||
dataGridView1.Rows[row].Cells[descriptionCol.Index].Style.BackColor = System.Drawing.Color.LightGray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dataGridView1_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e)
|
||||
{
|
||||
if (e.Row?.Tag is Replacement r && r.Mandatory)
|
||||
e.Cancel = true;
|
||||
}
|
||||
|
||||
private void loFiDefaultsBtn_Click(object sender, EventArgs e)
|
||||
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
|
||||
|
||||
private void defaultsBtn_Click(object sender, EventArgs e)
|
||||
=> LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
|
||||
private void minDefaultBtn_Click(object sender, EventArgs e)
|
||||
=> LoadTable(ReplacementCharacters.Barebones.Replacements);
|
||||
|
||||
|
||||
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0) return;
|
||||
|
||||
dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty;
|
||||
|
||||
var charToReplaceStr = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString();
|
||||
var replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty;
|
||||
var description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty;
|
||||
|
||||
//Validate the whole row. If it passes all validation, add or update the row's tag.
|
||||
if (string.IsNullOrEmpty(charToReplaceStr) && replacement == string.Empty && description == string.Empty)
|
||||
{
|
||||
//Invalid entry, so delete row
|
||||
var row = dataGridView1.Rows[e.RowIndex];
|
||||
if (!row.IsNewRow)
|
||||
{
|
||||
BeginInvoke(new MethodInvoker(delegate
|
||||
{
|
||||
dataGridView1.Rows.Remove(row);
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (string.IsNullOrEmpty(charToReplaceStr))
|
||||
{
|
||||
dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace";
|
||||
}
|
||||
else if (charToReplaceStr.Length > 1)
|
||||
{
|
||||
dataGridView1.Rows[e.RowIndex].ErrorText = $"Only 1 {charToReplaceCol.HeaderText} per entry";
|
||||
}
|
||||
else if (e.RowIndex >= Replacement.FIXED_COUNT &&
|
||||
dataGridView1.Rows
|
||||
.Cast<DataGridViewRow>()
|
||||
.Where(r => r.Index != e.RowIndex)
|
||||
.Select(r => r.Tag)
|
||||
.OfType<Replacement>()
|
||||
.Any(r => r.CharacterToReplace == charToReplaceStr[0])
|
||||
)
|
||||
{
|
||||
dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced";
|
||||
}
|
||||
else if (ReplacementCharacters.ContainsInvalid(replacement))
|
||||
{
|
||||
dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters";
|
||||
}
|
||||
else
|
||||
{
|
||||
//valid entry. Add or update Replacement in row's Tag
|
||||
var charToReplace = charToReplaceStr[0];
|
||||
|
||||
if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing)
|
||||
existing.Update(charToReplace, replacement, description);
|
||||
else
|
||||
dataGridView1.Rows[e.RowIndex].Tag = new Replacement(charToReplace, replacement, description);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var replacements = dataGridView1.Rows
|
||||
.Cast<DataGridViewRow>()
|
||||
.Select(r => r.Tag)
|
||||
.OfType<Replacement>()
|
||||
.ToList();
|
||||
|
||||
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void dataGridView1_Resize(object sender, EventArgs e)
|
||||
{
|
||||
dataGridView1.Columns[^1].Width = dataGridView1.Width - dataGridView1.Columns.Cast<DataGridViewColumn>().Sum(c => c == dataGridView1.Columns[^1] ? 0 : c.Width) - dataGridView1.RowHeadersWidth - 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Source/LibationWinForms/Dialogs/EditReplacementChars.resx
Normal file
69
Source/LibationWinForms/Dialogs/EditReplacementChars.resx
Normal file
@@ -0,0 +1,69 @@
|
||||
<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>
|
||||
<metadata name="charToReplaceCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="replacementStringCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="descriptionCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -140,12 +140,11 @@ namespace LibationWinForms.Dialogs
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
if (isChapterTitle)
|
||||
{
|
||||
richTextBox1.SelectionFont = bold;
|
||||
|
||||
richTextBox1.AppendText(chapterTitle);
|
||||
|
||||
if (isChapterTitle)
|
||||
richTextBox1.AppendText(chapterTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
richTextBox1.AppendText(slashWrap(books));
|
||||
richTextBox1.AppendText(sing);
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
|
||||
this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
|
||||
this.customFileNamingGb = new System.Windows.Forms.GroupBox();
|
||||
this.editCharreplacementBtn = new System.Windows.Forms.Button();
|
||||
this.chapterFileTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.chapterFileTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.chapterFileTemplateLbl = new System.Windows.Forms.Label();
|
||||
@@ -148,7 +149,7 @@
|
||||
// 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(667, 441);
|
||||
this.saveBtn.Location = new System.Drawing.Point(667, 461);
|
||||
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);
|
||||
@@ -161,7 +162,7 @@
|
||||
//
|
||||
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(785, 441);
|
||||
this.cancelBtn.Location = new System.Drawing.Point(785, 461);
|
||||
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);
|
||||
@@ -308,7 +309,7 @@
|
||||
// inProgressSelectControl
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| 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";
|
||||
@@ -328,7 +329,7 @@
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| 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";
|
||||
@@ -356,8 +357,8 @@
|
||||
// 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)));
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.tabControl.Controls.Add(this.tab1ImportantSettings);
|
||||
this.tabControl.Controls.Add(this.tab2ImportLibrary);
|
||||
this.tabControl.Controls.Add(this.tab3DownloadDecrypt);
|
||||
@@ -365,7 +366,7 @@
|
||||
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
this.tabControl.Name = "tabControl";
|
||||
this.tabControl.SelectedIndex = 0;
|
||||
this.tabControl.Size = new System.Drawing.Size(862, 423);
|
||||
this.tabControl.Size = new System.Drawing.Size(862, 443);
|
||||
this.tabControl.TabIndex = 100;
|
||||
//
|
||||
// tab1ImportantSettings
|
||||
@@ -377,7 +378,7 @@
|
||||
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(854, 395);
|
||||
this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 415);
|
||||
this.tab1ImportantSettings.TabIndex = 0;
|
||||
this.tab1ImportantSettings.Text = "Important settings";
|
||||
this.tab1ImportantSettings.UseVisualStyleBackColor = true;
|
||||
@@ -385,7 +386,7 @@
|
||||
// booksGb
|
||||
//
|
||||
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksGb.Controls.Add(this.saveEpisodesToSeriesFolderCbox);
|
||||
this.booksGb.Controls.Add(this.booksSelectControl);
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
@@ -416,7 +417,7 @@
|
||||
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(854, 395);
|
||||
this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 415);
|
||||
this.tab2ImportLibrary.TabIndex = 1;
|
||||
this.tab2ImportLibrary.Text = "Import library";
|
||||
this.tab2ImportLibrary.UseVisualStyleBackColor = true;
|
||||
@@ -459,7 +460,7 @@
|
||||
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(854, 395);
|
||||
this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 415);
|
||||
this.tab3DownloadDecrypt.TabIndex = 2;
|
||||
this.tab3DownloadDecrypt.Text = "Download/Decrypt";
|
||||
this.tab3DownloadDecrypt.UseVisualStyleBackColor = true;
|
||||
@@ -467,10 +468,10 @@
|
||||
// inProgressFilesGb
|
||||
//
|
||||
this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.inProgressFilesGb.Location = new System.Drawing.Point(7, 251);
|
||||
this.inProgressFilesGb.Location = new System.Drawing.Point(6, 281);
|
||||
this.inProgressFilesGb.Name = "inProgressFilesGb";
|
||||
this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128);
|
||||
this.inProgressFilesGb.TabIndex = 21;
|
||||
@@ -480,7 +481,8 @@
|
||||
// customFileNamingGb
|
||||
//
|
||||
this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.customFileNamingGb.Controls.Add(this.editCharreplacementBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl);
|
||||
@@ -492,11 +494,22 @@
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateLbl);
|
||||
this.customFileNamingGb.Location = new System.Drawing.Point(7, 88);
|
||||
this.customFileNamingGb.Name = "customFileNamingGb";
|
||||
this.customFileNamingGb.Size = new System.Drawing.Size(841, 157);
|
||||
this.customFileNamingGb.Size = new System.Drawing.Size(841, 187);
|
||||
this.customFileNamingGb.TabIndex = 20;
|
||||
this.customFileNamingGb.TabStop = false;
|
||||
this.customFileNamingGb.Text = "Custom file naming";
|
||||
//
|
||||
// editCharreplacementBtn
|
||||
//
|
||||
this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158);
|
||||
this.editCharreplacementBtn.Name = "editCharreplacementBtn";
|
||||
this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23);
|
||||
this.editCharreplacementBtn.TabIndex = 8;
|
||||
this.editCharreplacementBtn.Text = "[edit char replacement desc]";
|
||||
this.editCharreplacementBtn.UseVisualStyleBackColor = true;
|
||||
this.editCharreplacementBtn.Click += new System.EventHandler(this.editCharreplacementBtn_Click);
|
||||
//
|
||||
// chapterFileTemplateBtn
|
||||
//
|
||||
this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
@@ -511,7 +524,7 @@
|
||||
// chapterFileTemplateTb
|
||||
//
|
||||
this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125);
|
||||
this.chapterFileTemplateTb.Name = "chapterFileTemplateTb";
|
||||
this.chapterFileTemplateTb.ReadOnly = true;
|
||||
@@ -541,7 +554,7 @@
|
||||
// fileTemplateTb
|
||||
//
|
||||
this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.fileTemplateTb.Location = new System.Drawing.Point(6, 81);
|
||||
this.fileTemplateTb.Name = "fileTemplateTb";
|
||||
this.fileTemplateTb.ReadOnly = true;
|
||||
@@ -571,7 +584,7 @@
|
||||
// folderTemplateTb
|
||||
//
|
||||
this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.folderTemplateTb.Location = new System.Drawing.Point(5, 37);
|
||||
this.folderTemplateTb.Name = "folderTemplateTb";
|
||||
this.folderTemplateTb.ReadOnly = true;
|
||||
@@ -603,7 +616,7 @@
|
||||
this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab4AudioFileOptions.Name = "tab4AudioFileOptions";
|
||||
this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 395);
|
||||
this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 415);
|
||||
this.tab4AudioFileOptions.TabIndex = 3;
|
||||
this.tab4AudioFileOptions.Text = "Audio File Options";
|
||||
this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
||||
@@ -633,7 +646,7 @@
|
||||
// chapterTitleTemplateTb
|
||||
//
|
||||
this.chapterTitleTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterTitleTemplateTb.Location = new System.Drawing.Point(6, 22);
|
||||
this.chapterTitleTemplateTb.Name = "chapterTitleTemplateTb";
|
||||
this.chapterTitleTemplateTb.ReadOnly = true;
|
||||
@@ -1017,7 +1030,7 @@
|
||||
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(886, 484);
|
||||
this.ClientSize = new System.Drawing.Size(886, 504);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
@@ -1141,5 +1154,6 @@
|
||||
private System.Windows.Forms.GroupBox chapterTitleTemplateGb;
|
||||
private System.Windows.Forms.Button chapterTitleTemplateBtn;
|
||||
private System.Windows.Forms.TextBox chapterTitleTemplateTb;
|
||||
private System.Windows.Forms.Button editCharreplacementBtn;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,19 @@ namespace LibationWinForms.Dialogs
|
||||
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 void editCharreplacementBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = new EditReplacementChars(config);
|
||||
form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
form.ShowDialog(this);
|
||||
}
|
||||
|
||||
private void Load_DownloadDecrypt(Configuration config)
|
||||
{
|
||||
inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||
editCharreplacementBtn.Text = desc(nameof(config.ReplacementCharacters));
|
||||
|
||||
badBookGb.Text = desc(nameof(config.BadBook));
|
||||
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
|
||||
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
|
||||
|
||||
@@ -33,7 +33,18 @@ namespace LibationWinForms
|
||||
.ToArray();
|
||||
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts));
|
||||
try
|
||||
{
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
|
||||
if (InvokeRequired)
|
||||
await Invoke(importAsync);
|
||||
else
|
||||
await importAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error invoking auto-scan");
|
||||
}
|
||||
};
|
||||
|
||||
// load init state to menu checkbox
|
||||
|
||||
@@ -3,7 +3,7 @@ using ApplicationServices;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
// This is for the Scanning notificationin the upper right. This shown for manual scanning and auto-scan
|
||||
// This is for the Scanning notification in the upper right. This shown for manual scanning and auto-scan
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_ScanNotification()
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.3" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.2.1" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -42,8 +42,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
public ToolStripButton popoutBtn = new();
|
||||
|
||||
private System.Threading.SynchronizationContext syncContext { get; } = System.Threading.SynchronizationContext.Current;
|
||||
|
||||
public ProcessQueueControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -103,6 +101,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
AddToQueue(procs);
|
||||
}
|
||||
|
||||
@@ -121,6 +120,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
AddToQueue(procs);
|
||||
}
|
||||
|
||||
@@ -138,44 +138,57 @@ namespace LibationWinForms.ProcessQueue
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
AddToQueue(procs);
|
||||
}
|
||||
|
||||
private void AddToQueue(IEnumerable<ProcessBook> pbook)
|
||||
{
|
||||
syncContext.Post(_ =>
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
Queue.Enqueue(pbook);
|
||||
if (!Running)
|
||||
QueueRunner = QueueLoop();
|
||||
},
|
||||
null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
DateTime StartingTime;
|
||||
private async Task QueueLoop()
|
||||
{
|
||||
StartingTime = DateTime.Now;
|
||||
counterTimer.Start();
|
||||
|
||||
while (Queue.MoveNext())
|
||||
try
|
||||
{
|
||||
var nextBook = Queue.Current;
|
||||
Serilog.Log.Logger.Information("Begin processing queue");
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
StartingTime = DateTime.Now;
|
||||
counterTimer.Start();
|
||||
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
else if (result == ProcessBookResult.FailedAbort)
|
||||
Queue.ClearQueue();
|
||||
else if (result == ProcessBookResult.FailedSkip)
|
||||
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
|
||||
while (Queue.MoveNext())
|
||||
{
|
||||
var nextBook = Queue.Current;
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
|
||||
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
else if (result == ProcessBookResult.FailedAbort)
|
||||
Queue.ClearQueue();
|
||||
else if (result == ProcessBookResult.FailedSkip)
|
||||
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
|
||||
}
|
||||
Serilog.Log.Logger.Information("Completed processing queue");
|
||||
|
||||
Queue_CompletedCountChanged(this, 0);
|
||||
counterTimer.Stop();
|
||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||
UpdateAllControls();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
|
||||
}
|
||||
Queue_CompletedCountChanged(this, 0);
|
||||
counterTimer.Stop();
|
||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||
UpdateAllControls();
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
@@ -212,7 +225,8 @@ namespace LibationWinForms.ProcessQueue
|
||||
private async void cancelAllBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Queue.ClearQueue();
|
||||
await Queue.Current?.CancelAsync();
|
||||
if (Queue.Current is not null)
|
||||
await Queue.Current.CancelAsync();
|
||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||
UpdateAllControls();
|
||||
}
|
||||
@@ -281,35 +295,41 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param>
|
||||
private void UpdateControl(int queueIndex, string propertyName = null)
|
||||
{
|
||||
int i = queueIndex - FirstVisible;
|
||||
|
||||
if (i > NumVisible || i < 0) return;
|
||||
|
||||
var proc = Queue[queueIndex];
|
||||
|
||||
syncContext.Send(_ =>
|
||||
try
|
||||
{
|
||||
Panels[i].SuspendLayout();
|
||||
if (propertyName is null or nameof(proc.Cover))
|
||||
Panels[i].SetCover(proc.Cover);
|
||||
if (propertyName is null or nameof(proc.BookText))
|
||||
Panels[i].SetBookInfo(proc.BookText);
|
||||
int i = queueIndex - FirstVisible;
|
||||
|
||||
if (proc.Result != ProcessBookResult.None)
|
||||
if (i > NumVisible || i < 0) return;
|
||||
|
||||
var proc = Queue[queueIndex];
|
||||
|
||||
Invoke(() =>
|
||||
{
|
||||
Panels[i].SetResult(proc.Result);
|
||||
return;
|
||||
}
|
||||
Panels[i].SuspendLayout();
|
||||
if (propertyName is null or nameof(proc.Cover))
|
||||
Panels[i].SetCover(proc.Cover);
|
||||
if (propertyName is null or nameof(proc.BookText))
|
||||
Panels[i].SetBookInfo(proc.BookText);
|
||||
|
||||
if (propertyName is null or nameof(proc.Status))
|
||||
Panels[i].SetStatus(proc.Status);
|
||||
if (propertyName is null or nameof(proc.Progress))
|
||||
Panels[i].SetProgrss(proc.Progress);
|
||||
if (propertyName is null or nameof(proc.TimeRemaining))
|
||||
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
||||
Panels[i].ResumeLayout();
|
||||
},
|
||||
null);
|
||||
if (proc.Result != ProcessBookResult.None)
|
||||
{
|
||||
Panels[i].SetResult(proc.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyName is null or nameof(proc.Status))
|
||||
Panels[i].SetStatus(proc.Status);
|
||||
if (propertyName is null or nameof(proc.Progress))
|
||||
Panels[i].SetProgrss(proc.Progress);
|
||||
if (propertyName is null or nameof(proc.TimeRemaining))
|
||||
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
||||
Panels[i].ResumeLayout();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error updating the queued item's display.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAllControls()
|
||||
@@ -328,36 +348,44 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// <param name="panelClicked">The clicked control to update</param>
|
||||
private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked)
|
||||
{
|
||||
ProcessBook item = Queue[queueIndex];
|
||||
if (buttonName == nameof(panelClicked.cancelBtn))
|
||||
try
|
||||
{
|
||||
await item.CancelAsync();
|
||||
Queue.RemoveQueued(item);
|
||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||
ProcessBook item = Queue[queueIndex];
|
||||
if (buttonName == nameof(panelClicked.cancelBtn))
|
||||
{
|
||||
if (item is not null)
|
||||
await item.CancelAsync();
|
||||
Queue.RemoveQueued(item);
|
||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveFirstBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
|
||||
UpdateAllControls();
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveUpBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.OneUp);
|
||||
UpdateControl(queueIndex);
|
||||
if (queueIndex > 0)
|
||||
UpdateControl(queueIndex - 1);
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveDownBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.OneDown);
|
||||
UpdateControl(queueIndex);
|
||||
if (queueIndex + 1 < Queue.Count)
|
||||
UpdateControl(queueIndex + 1);
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveLastBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.Last);
|
||||
UpdateAllControls();
|
||||
}
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveFirstBtn))
|
||||
catch(Exception ex)
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
|
||||
UpdateAllControls();
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveUpBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.OneUp);
|
||||
UpdateControl(queueIndex);
|
||||
if (queueIndex > 0)
|
||||
UpdateControl(queueIndex - 1);
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveDownBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.OneDown);
|
||||
UpdateControl(queueIndex);
|
||||
if (queueIndex + 1 < Queue.Count)
|
||||
UpdateControl(queueIndex + 1);
|
||||
}
|
||||
else if (buttonName == nameof(panelClicked.moveLastBtn))
|
||||
{
|
||||
Queue.MoveQueuePosition(item, QueuePosition.Last);
|
||||
UpdateAllControls();
|
||||
Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace LibationWinForms
|
||||
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settigns in default location
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace LibationWinForms
|
||||
return;
|
||||
|
||||
var dialogResult = MessageBox.Show(string.Format(
|
||||
$"There is a new version avilable. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."),
|
||||
$"There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."),
|
||||
"Update Available",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Information);
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace FileNamingTemplateTests
|
||||
[TestClass]
|
||||
public class GetFilePath
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
|
||||
[TestMethod]
|
||||
public void equiv_GetValidFilename()
|
||||
{
|
||||
@@ -19,81 +21,33 @@ namespace FileNamingTemplateTests
|
||||
sb.Append('0', 300);
|
||||
var longText = sb.ToString();
|
||||
|
||||
var expectedOld = "C:\\foo\\bar\\my_ book 00000000000000000000000000000000000000000 [ID123456].txt";
|
||||
var expectedNew = "C:\\foo\\bar\\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt";
|
||||
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
|
||||
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
|
||||
|
||||
f1.Should().Be(expectedOld);
|
||||
f2.Should().Be(expectedNew);
|
||||
}
|
||||
private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
|
||||
|
||||
filename ??= "";
|
||||
|
||||
// sanitize. omit invalid characters. exception: colon => underscore
|
||||
filename = filename.Replace(":", "_");
|
||||
filename = FileUtility.GetSafeFileName(filename);
|
||||
|
||||
if (filename.Length > 50)
|
||||
filename = filename.Substring(0, 50);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadataSuffix))
|
||||
filename += $" [{metadataSuffix}]";
|
||||
|
||||
// extension is null when this method is used for directory names
|
||||
extension = FileUtility.GetStandardizedExtension(extension);
|
||||
|
||||
// ensure uniqueness
|
||||
var fullfilename = Path.Combine(dirFullPath, filename + extension);
|
||||
var i = 0;
|
||||
while (File.Exists(fullfilename))
|
||||
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
|
||||
|
||||
return fullfilename;
|
||||
}
|
||||
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
{
|
||||
var template = $"<title> [<id>]";
|
||||
|
||||
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
|
||||
fileNamingTemplate.AddParameterReplacement("title", filename);
|
||||
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void equiv_GetMultipartFileName()
|
||||
{
|
||||
var expected = @"C:\foo\bar\my file - 002 - title.txt";
|
||||
var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title");
|
||||
var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
|
||||
|
||||
f1.Should().Be(expected);
|
||||
f1.Should().Be(f2);
|
||||
f2.Should().Be(expected);
|
||||
}
|
||||
private static string OLD_GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
{
|
||||
// 1-9 => 1-9
|
||||
// 10-99 => 01-99
|
||||
// 100-999 => 001-999
|
||||
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
|
||||
|
||||
string extension = Path.GetExtension(originalPath);
|
||||
|
||||
var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros}";
|
||||
if (!string.IsNullOrWhiteSpace(suffix))
|
||||
filenameBase += $" - {suffix}";
|
||||
|
||||
// Replace illegal path characters with spaces
|
||||
var fileName = FileUtility.GetSafeFileName(filenameBase, " ");
|
||||
var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
|
||||
return path;
|
||||
}
|
||||
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
{
|
||||
// 1-9 => 1-9
|
||||
@@ -103,10 +57,10 @@ namespace FileNamingTemplateTests
|
||||
|
||||
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
|
||||
var fileNamingTemplate = new FileNamingTemplate(t);
|
||||
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
||||
return fileNamingTemplate.GetFilePath().PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -114,7 +68,7 @@ namespace FileNamingTemplateTests
|
||||
{
|
||||
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
|
||||
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
||||
fileNamingTemplate.GetFilePath().PathWithoutPrefix.Should().Be(@"\foo\slashes.txt");
|
||||
fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(@"\foo\sl∕as∕he∕s.txt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,49 +12,88 @@ namespace FileUtilityTests
|
||||
[TestClass]
|
||||
public class GetSafePath
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null));
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
|
||||
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones;
|
||||
|
||||
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
|
||||
[TestMethod]
|
||||
[DataRow("http://test.com/a/b/c", @"http꞉\test.com\a\b\c")]
|
||||
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
|
||||
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
|
||||
|
||||
[TestMethod]
|
||||
// non-empty replacement
|
||||
[DataRow("abc*abc.txt", "abc✱abc.txt")]
|
||||
// standardize slashes
|
||||
[DataRow(@"a/b\c/d", @"a\b\c\d")]
|
||||
// remove illegal chars
|
||||
[DataRow("a*?:z.txt", "a✱?꞉z.txt")]
|
||||
// retain drive letter path colon
|
||||
[DataRow(@"C:\az.txt", @"C:\az.txt")]
|
||||
// replace all other colons
|
||||
[DataRow(@"a\b:c\d.txt", @"a\b꞉c\d.txt")]
|
||||
// remove empty directories
|
||||
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
|
||||
[DataRow(@"C:\""foo\<id>", @"C:\“foo\<id>")]
|
||||
public void DefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Default).PathWithoutPrefix);
|
||||
|
||||
[TestMethod]
|
||||
// non-empty replacement
|
||||
[DataRow("abc*abc.txt", "abc_abc.txt")]
|
||||
// standardize slashes
|
||||
[DataRow(@"a/b\c/d", @"a\b\c\d")]
|
||||
// remove illegal chars
|
||||
[DataRow("a*?:z.txt", "a__-z.txt")]
|
||||
// retain drive letter path colon
|
||||
[DataRow(@"C:\az.txt", @"C:\az.txt")]
|
||||
// replace all other colons
|
||||
[DataRow(@"a\b:c\d.txt", @"a\b-c\d.txt")]
|
||||
// remove empty directories
|
||||
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
|
||||
[DataRow(@"C:\""foo\<id>", @"C:\'foo\{id}")]
|
||||
public void LoFiDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, LoFiDefault).PathWithoutPrefix);
|
||||
|
||||
[TestMethod]
|
||||
// empty replacement
|
||||
[DataRow("abc*abc.txt", "", "abc✱abc.txt")]
|
||||
// non-empty replacement
|
||||
[DataRow("abc*abc.txt", "ZZZ", "abc✱abc.txt")]
|
||||
[DataRow("abc*abc.txt", "abc_abc.txt")]
|
||||
// standardize slashes
|
||||
[DataRow(@"a/b\c/d", "Z", @"a\b\c\d")]
|
||||
[DataRow(@"a/b\c/d", @"a\b\c\d")]
|
||||
// remove illegal chars
|
||||
[DataRow("a*?:z.txt", "Z", "a✱?꞉z.txt")]
|
||||
[DataRow("a*?:z.txt", "a___z.txt")]
|
||||
// retain drive letter path colon
|
||||
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
|
||||
[DataRow(@"C:\az.txt", @"C:\az.txt")]
|
||||
// replace all other colons
|
||||
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\b꞉c\d.txt")]
|
||||
[DataRow(@"a\b:c\d.txt", @"a\b_c\d.txt")]
|
||||
// remove empty directories
|
||||
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
|
||||
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\“foo\<id>")]
|
||||
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement).PathWithoutPrefix);
|
||||
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
|
||||
[DataRow(@"C:\""foo\<id>", @"C:\_foo\_id_")]
|
||||
public void BarebonesDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Barebones).PathWithoutPrefix);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetSafeFileName
|
||||
{
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
|
||||
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones;
|
||||
|
||||
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
|
||||
[TestMethod]
|
||||
[DataRow("http://test.com/a/b/c", "httptest.comabc")]
|
||||
public void url_null_replacement(string inStr, string outStr) => ReplacementTests(inStr, null, outStr);
|
||||
[DataRow("http://test.com/a/b/c", "http꞉∕∕test.com∕a∕b∕c")]
|
||||
public void url_null_replacement(string inStr, string outStr) => DefaultReplacementTest(inStr, outStr);
|
||||
|
||||
[TestMethod]
|
||||
// empty replacement
|
||||
[DataRow("http://test.com/a/b/c", "", "httptest.comabc")]
|
||||
// single char replace
|
||||
[DataRow("http://test.com/a/b/c", "_", "http___test.com_a_b_c")]
|
||||
// multi char replace
|
||||
[DataRow("http://test.com/a/b/c", "!!!", "http!!!!!!!!!test.com!!!a!!!b!!!c")]
|
||||
public void ReplacementTests(string inStr, string replacement, string outStr) => FileUtility.GetSafeFileName(inStr, replacement).Should().Be(outStr);
|
||||
[DataRow("http://test.com/a/b/c", "http꞉∕∕test.com∕a∕b∕c")]
|
||||
public void DefaultReplacementTest(string inStr, string outStr) => Default.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
|
||||
|
||||
[TestMethod]
|
||||
// empty replacement
|
||||
[DataRow("http://test.com/a/b/c", "http-__test.com_a_b_c")]
|
||||
public void LoFiDefaultReplacementTest(string inStr, string outStr) => LoFiDefault.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
|
||||
|
||||
[TestMethod]
|
||||
// empty replacement
|
||||
[DataRow("http://test.com/a/b/c", "http___test.com_a_b_c")]
|
||||
public void BarebonesDefaultReplacementTest(string inStr, string outStr) => Barebones.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
@@ -117,6 +156,8 @@ namespace FileUtilityTests
|
||||
[TestClass]
|
||||
public class GetValidFilename
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
|
||||
[TestMethod]
|
||||
// dot-files
|
||||
[DataRow(@"C:\a bc\x y z\.f i l e.txt")]
|
||||
@@ -134,7 +175,7 @@ namespace FileUtilityTests
|
||||
// file end dots
|
||||
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
|
||||
public void Tests(string input, string expected)
|
||||
=> FileUtility.GetValidFilename(input).PathWithoutPrefix.Should().Be(expected);
|
||||
=> FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using FluentAssertions;
|
||||
using LibationFileManager;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -51,6 +52,9 @@ namespace TemplatesTests
|
||||
[TestClass]
|
||||
public class getFileNamingTemplate
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(null, "asin", @"C:\", "ext")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
@@ -73,28 +77,28 @@ namespace TemplatesTests
|
||||
[DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")]
|
||||
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
|
||||
.GetFilePath()
|
||||
.GetFilePath(Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_empty()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.GetFilePath(Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(@"C:\a\b\foobar.ext");
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_no_series()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.GetFilePath(Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(@"C:\a\b\foobar.ext");
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_with_series()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.GetFilePath(Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
|
||||
}
|
||||
@@ -387,11 +391,13 @@ namespace Templates_ChapterFile_Tests
|
||||
[TestClass]
|
||||
public class GetPortionFilename
|
||||
{
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("asin", "[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")]
|
||||
[DataRow("asin", "<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")]
|
||||
public void Tests(string asin, string template, string dir, string ext, int pos, int total, string chapter, string expected)
|
||||
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir)
|
||||
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ STRUCTURE
|
||||
* Folders in the solution are numbered. Eg: "4 Domain (db)"
|
||||
* All projects should only refer to other projects in the same folder or to projects in folders with smaller numbers.
|
||||
* 1 Core Libraries
|
||||
This is code which has roughly equivilent priority and knowledge as the BCL. In practice, if code is this universal then it doesn't live here long and is instead moved into Dinah.Core.
|
||||
This is code which has roughly equivalent priority and knowledge as the BCL. In practice, if code is this universal then it doesn't live here long and is instead moved into Dinah.Core.
|
||||
* 2 Utilities (domain ignorant)
|
||||
Stand-alone libraries with no knowledge of anything having to do with Libation or other programs. In theory any of these should be able to one day be converted to a nuget pkg
|
||||
* 3 Domain Internal Utilities (db ignorant)
|
||||
|
||||
Reference in New Issue
Block a user