mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-01 10:28:21 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4acd5d208 | ||
|
|
381440db4c | ||
|
|
00c8be1f7e | ||
|
|
d665122aa2 | ||
|
|
bb40df5fa3 | ||
|
|
e3c9f70dff | ||
|
|
b351033cec | ||
|
|
18f69bc73d | ||
|
|
39fe7b79d2 | ||
|
|
85769d797b | ||
|
|
9a80f18e1c | ||
|
|
aec8305e52 | ||
|
|
a672174a9b | ||
|
|
6f490b4491 | ||
|
|
5917d059e4 |
@@ -20,6 +20,13 @@
|
||||
|
||||
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
|
||||
|
||||
##### Which version? Chardonnay vs Classic
|
||||
|
||||
Nearly 100% of the difference is look and feel -- it's a matter of preference.
|
||||
|
||||
Chardonnay has an updated look and will work and look the same on Windows, Mac, and Linux.
|
||||
Classic is Windows only. It has an older look because it's built with older, duller, and more mature technology. This tech has built into it better support for things like accessibility for screen readers.
|
||||
|
||||
### Installation
|
||||
|
||||
* Windows
|
||||
|
||||
@@ -23,7 +23,7 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<id\>|Audible book ID (ASIN)|Text|
|
||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||
|\<title\>|Full title|Text|
|
||||
|\<title short\>|Title. Stop at first colon|Text|
|
||||
|\<author\>|Author(s)|Text|
|
||||
@@ -39,16 +39,18 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
|\<locale\>|Region/country|Text|
|
||||
|\<year\>|Year published|Integer|
|
||||
|\<language\>|Book's language|Text|
|
||||
|\<language short\>|Book's language abbreviated. Eg: ENG|Text|
|
||||
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|
||||
|\<file date\>|File creation date/time.|DateTime|
|
||||
|\<pub date\>|Audiobook publication date|DateTime|
|
||||
|\<date added\>|Date the book added to your Audible account|DateTime|
|
||||
|\<ch count\>|Number of chapters **†**|Integer|
|
||||
|\<ch title\>|Chapter title **†**|Text|
|
||||
|\<ch#\>|Chapter number **†**|Integer|
|
||||
|\<ch# 0\>|Chapter number with leading zeros **†**|Integer|
|
||||
|\<ch count\> **‡**|Number of chapters|Integer|
|
||||
|\<ch title\> **‡**|Chapter title|Text|
|
||||
|\<ch#\> **‡**|Chapter number|Integer|
|
||||
|\<ch# 0\> **‡**|Chapter number with leading zeros|Integer|
|
||||
|
||||
**†** Only valid for Chapter Filename and Chapter Tile Metadata
|
||||
**†** Does not support custom formatting
|
||||
|
||||
**‡** Only valid for Chapter Filename and Chapter Tile Metadata
|
||||
|
||||
To change how these properties are displayed, [read about custom formatters](#tag-formatters)
|
||||
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
using AAXClean;
|
||||
using Dinah.Core.Net.Http;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
{
|
||||
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
|
||||
{
|
||||
public event EventHandler<AppleTags> RetrievedMetadata;
|
||||
|
||||
protected AaxFile AaxFile { get; private set; }
|
||||
private Mp4Operation aaxConversion;
|
||||
protected Mp4Operation AaxConversion
|
||||
{
|
||||
get => aaxConversion;
|
||||
set
|
||||
{
|
||||
if (aaxConversion is not null)
|
||||
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
if (value is not null)
|
||||
{
|
||||
aaxConversion = value;
|
||||
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
}
|
||||
}
|
||||
}
|
||||
protected Mp4Operation AaxConversion { get; set; }
|
||||
|
||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions) { }
|
||||
@@ -45,12 +29,6 @@ namespace AaxDecrypter
|
||||
FinalizeDownload();
|
||||
}
|
||||
|
||||
protected override void FinalizeDownload()
|
||||
{
|
||||
AaxConversion = null;
|
||||
base.FinalizeDownload();
|
||||
}
|
||||
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
AaxFile = new AaxFile(InputFileStream);
|
||||
@@ -82,24 +60,5 @@ namespace AaxDecrypter
|
||||
|
||||
return !IsCanceled;
|
||||
}
|
||||
|
||||
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var remainingSecsToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = e.ProcessPosition / e.TotalDuration;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = 100 * progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -8,6 +10,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||
{
|
||||
private readonly AverageSpeed averageSpeed = new();
|
||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions)
|
||||
{
|
||||
@@ -35,7 +38,10 @@ namespace AaxDecrypter
|
||||
&& DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||
{
|
||||
outputFile.Close();
|
||||
await (AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName));
|
||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||
await AaxConversion;
|
||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||
}
|
||||
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
@@ -46,6 +52,27 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
private void AaxConversion_MoovProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = 100d * (1 - remainingTimeToProcess / e.TotalDuration.TotalSeconds);
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
|
||||
private Mp4Operation decryptAsync(Stream outputFile)
|
||||
=> DownloadOptions.OutputFormat == OutputFormat.Mp3
|
||||
? AaxFile.ConvertToMp3Async
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace AaxDecrypter
|
||||
protected string OutputFileName { get; }
|
||||
protected IDownloadOptions DownloadOptions { get; }
|
||||
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
|
||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||
|
||||
private readonly NetworkFileStreamPersister nfsPersister;
|
||||
private readonly DownloadProgress zeroProgress;
|
||||
@@ -65,13 +66,47 @@ namespace AaxDecrypter
|
||||
|
||||
public async Task<bool> RunAsync()
|
||||
{
|
||||
var progressTask = Task.Run(reportProgress);
|
||||
|
||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
||||
(bool success, var elapsed) = await AsyncSteps.RunAsync();
|
||||
|
||||
await progressTask;
|
||||
|
||||
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
||||
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
||||
|
||||
return success;
|
||||
|
||||
async Task reportProgress()
|
||||
{
|
||||
AverageSpeed averageSpeed = new();
|
||||
|
||||
while (InputFileStream.CanRead && InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
{
|
||||
averageSpeed.AddPosition(InputFilePosition);
|
||||
|
||||
var estSecsRemaining = (InputFileStream.Length - InputFilePosition) / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estSecsRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estSecsRemaining));
|
||||
|
||||
var progressPercent = 100d * InputFilePosition / InputFileStream.Length;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = InputFilePosition,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
OnDecryptTimeRemaining(TimeSpan.Zero);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task CancelAsync();
|
||||
@@ -101,6 +136,7 @@ namespace AaxDecrypter
|
||||
protected virtual void FinalizeDownload()
|
||||
{
|
||||
nfsPersister?.Dispose();
|
||||
OnDecryptTimeRemaining(TimeSpan.Zero);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
}
|
||||
|
||||
|
||||
171
Source/AaxDecrypter/AverageSpeed.cs
Normal file
171
Source/AaxDecrypter/AverageSpeed.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AaxDecrypter;
|
||||
|
||||
public static class LinqStats
|
||||
{
|
||||
public static (double mean, double stdDev) BasicStatisticsBy<T>(this IEnumerable<T> values, Func<T, double> selector)
|
||||
{
|
||||
var count = values.Count();
|
||||
var mean = values.Average(selector);
|
||||
|
||||
return (mean, Math.Sqrt(values.Sum(s => Math.Pow(selector(s) - mean, 2)) / (count - 1)));
|
||||
}
|
||||
|
||||
public static bool T_Test_2By<T>(this IEnumerable<T> values, Func<T, double> selector, IEnumerable<T> secondGroup, Significance confidence)
|
||||
{
|
||||
var n1 = values.Count();
|
||||
var n2 = secondGroup.Count();
|
||||
var n = n1 + n2;
|
||||
|
||||
if (n1 < 3 || n2 < 3) return false;
|
||||
|
||||
(var mean1, var stdDev1) = values.BasicStatisticsBy(selector);
|
||||
(var mean2, var stdDev2) = secondGroup.BasicStatisticsBy(selector);
|
||||
|
||||
var pooledStdDev = Math.Sqrt((((n1 - 1) * (stdDev1 * stdDev1)) + ((n2 - 1) * (stdDev2 * stdDev2))) / (n1 + n2 - 2));
|
||||
|
||||
var testStat = Math.Abs(mean1 - mean2) / (pooledStdDev * Math.Sqrt(1d / n1 + 1d / n2));
|
||||
var crit = T_Stat(Math.Min(n - 2, MAX_DEGREES_FREEDOM), confidence);
|
||||
|
||||
return testStat > crit;
|
||||
}
|
||||
|
||||
public static bool T_Test_1By<T>(this IEnumerable<T> values, Func<T, double> selector, double testMean, Significance confidence)
|
||||
{
|
||||
var n = values.Count();
|
||||
|
||||
if (n < 2) return false;
|
||||
|
||||
(var sampleMean, var sampleStdDev) = values.BasicStatisticsBy(selector);
|
||||
|
||||
var testStat = Math.Abs(sampleMean - testMean) / (sampleStdDev / Math.Sqrt(n));
|
||||
var crit = T_Stat(Math.Min(n - 1, MAX_DEGREES_FREEDOM), confidence);
|
||||
|
||||
return testStat > crit;
|
||||
}
|
||||
|
||||
private static double T_Stat(int degreesFreedom, Significance confidence)
|
||||
{
|
||||
ArgumentValidator.EnsureBetweenInclusive(degreesFreedom, nameof(degreesFreedom), MIN_DEGREES_FREEDOM, MAX_DEGREES_FREEDOM);
|
||||
|
||||
return T_TABLE[(int)confidence][degreesFreedom - MIN_DEGREES_FREEDOM];
|
||||
}
|
||||
|
||||
static LinqStats()
|
||||
{
|
||||
T_TABLE = new double[][] { T_Table_01, T_Table_05, T_Table_10, T_Table_15, T_Table_20, T_Table_25 };
|
||||
}
|
||||
|
||||
private const int MIN_DEGREES_FREEDOM = 1;
|
||||
private const int MAX_DEGREES_FREEDOM = 201;
|
||||
/// <summary>
|
||||
/// 2-tailed t-Distribution critical values at 75%, 80%, 85%,
|
||||
/// 90%, 95%, and 99% confidence for 1 - 201 degrees of freedom.
|
||||
/// </summary>
|
||||
private readonly static double[][] T_TABLE;
|
||||
private readonly static double[] T_Table_25 = { 2.414213562, 1.603567451, 1.422625281, 1.344397556, 1.300949037, 1.273349309, 1.254278682, 1.240318261, 1.229659173, 1.221255395, 1.214460246, 1.208852542, 1.204146242, 1.200140298, 1.196689284, 1.193685414, 1.191047107, 1.188711483, 1.186629298, 1.184761434, 1.183076432, 1.181548697, 1.180157199, 1.178884497, 1.177716003, 1.176639425, 1.175644329, 1.174721803, 1.173864189, 1.173064871, 1.1723181, 1.17161886, 1.170962753, 1.17034591, 1.169764906, 1.169216709, 1.168698615, 1.168208212, 1.167743338, 1.167302049, 1.166882595, 1.166483396, 1.166103019, 1.165740162, 1.165393644, 1.165062385, 1.164745398, 1.164441782, 1.164150707, 1.163871412, 1.163603196, 1.163345413, 1.163097467, 1.162858803, 1.162628911, 1.162407316, 1.162193577, 1.161987283, 1.161788052, 1.161595527, 1.161409375, 1.161229286, 1.161054967, 1.160886145, 1.160722566, 1.160563987, 1.160410184, 1.160260944, 1.160116066, 1.159975363, 1.159838656, 1.159705777, 1.159576569, 1.15945088, 1.15932857, 1.159209503, 1.159093552, 1.158980598, 1.158870524, 1.158763222, 1.158658589, 1.158556526, 1.15845694, 1.158359742, 1.158264847, 1.158172173, 1.158081645, 1.157993188, 1.157906731, 1.157822209, 1.157739556, 1.157658712, 1.157579617, 1.157502216, 1.157426454, 1.157352281, 1.157279646, 1.157208502, 1.157138804, 1.157070509, 1.157003573, 1.156937958, 1.156873624, 1.156810534, 1.156748653, 1.156687945, 1.156628379, 1.156569922, 1.156512543, 1.156456213, 1.156400904, 1.156346587, 1.156293237, 1.156240827, 1.156189334, 1.156138733, 1.156089001, 1.156040117, 1.155992058, 1.155944804, 1.155898335, 1.155852631, 1.155807674, 1.155763446, 1.155719928, 1.155677105, 1.155634959, 1.155593475, 1.155552637, 1.15551243, 1.155472839, 1.155433851, 1.155395452, 1.155357629, 1.155320368, 1.155283658, 1.155247486, 1.155211841, 1.15517671, 1.155142084, 1.15510795, 1.1550743, 1.155041122, 1.155008406, 1.154976144, 1.154944326, 1.154912942, 1.154881984, 1.154851443, 1.154821311, 1.15479158, 1.154762241, 1.154733287, 1.154704711, 1.154676505, 1.154648662, 1.154621175, 1.154594037, 1.154567242, 1.154540783, 1.154514654, 1.154488849, 1.154463361, 1.154438185, 1.154413316, 1.154388747, 1.154364474, 1.15434049, 1.154316792, 1.154293373, 1.154270229, 1.154247355, 1.154224746, 1.154202398, 1.154180307, 1.154158467, 1.154136875, 1.154115526, 1.154094417, 1.154073543, 1.1540529, 1.154032485, 1.154012294, 1.153992323, 1.153972568, 1.153953027, 1.153933695, 1.15391457, 1.153895647, 1.153876925, 1.153858399, 1.153840066, 1.153821925, 1.15380397, 1.153786201, 1.153768613, 1.153751204, 1.153733972, 1.153716914, 1.153700026 };
|
||||
private readonly static double[] T_Table_20 = { 3.077683537, 1.885618083, 1.637744354, 1.533206274, 1.475884049, 1.439755747, 1.414923928, 1.39681531, 1.383028738, 1.372183641, 1.363430318, 1.356217334, 1.350171289, 1.345030374, 1.340605608, 1.336757167, 1.33337939, 1.330390944, 1.327728209, 1.325340707, 1.323187874, 1.321236742, 1.31946024, 1.317835934, 1.316345073, 1.314971864, 1.313702913, 1.312526782, 1.311433647, 1.310415025, 1.309463549, 1.308572793, 1.307737124, 1.306951587, 1.306211802, 1.305513886, 1.304854381, 1.304230204, 1.303638589, 1.303077053, 1.302543359, 1.302035487, 1.301551608, 1.30109006, 1.300649332, 1.300228048, 1.299824947, 1.299438879, 1.299068785, 1.298713694, 1.298372713, 1.298045016, 1.297729843, 1.297426488, 1.2971343, 1.296852673, 1.296581044, 1.29631889, 1.296065725, 1.295821094, 1.295584571, 1.295355762, 1.295134294, 1.29491982, 1.294712013, 1.294510568, 1.294315197, 1.294125629, 1.293941609, 1.293762898, 1.293589269, 1.293420507, 1.293256413, 1.293096793, 1.292941469, 1.292790268, 1.292643029, 1.292499597, 1.292359828, 1.292223583, 1.29209073, 1.291961144, 1.291834705, 1.291711301, 1.291590824, 1.291473171, 1.291358243, 1.291245948, 1.291136195, 1.291028899, 1.290923979, 1.290821356, 1.290720956, 1.290622708, 1.290526543, 1.290432395, 1.290340202, 1.290249904, 1.290161442, 1.290074761, 1.289989809, 1.289906533, 1.289824884, 1.289744816, 1.289666283, 1.289589241, 1.289513648, 1.289439464, 1.289366649, 1.289295166, 1.289224979, 1.289156054, 1.289088355, 1.289021851, 1.28895651, 1.288892302, 1.288829199, 1.288767171, 1.288706191, 1.288646234, 1.288587273, 1.288529284, 1.288472243, 1.288416127, 1.288360913, 1.288306581, 1.288253109, 1.288200477, 1.288148665, 1.288097654, 1.288047427, 1.287997964, 1.287949248, 1.287901264, 1.287853994, 1.287807422, 1.287761534, 1.287716314, 1.287671748, 1.287627821, 1.287584521, 1.287541833, 1.287499745, 1.287458245, 1.287417319, 1.287376957, 1.287337146, 1.287297876, 1.287259135, 1.287220914, 1.2871832, 1.287145985, 1.287109259, 1.287073012, 1.287037235, 1.287001918, 1.286967053, 1.286932631, 1.286898644, 1.286865084, 1.286831942, 1.286799212, 1.286766884, 1.286734952, 1.286703409, 1.286672248, 1.286641461, 1.286611042, 1.286580985, 1.286551283, 1.286521929, 1.286492918, 1.286464244, 1.286435901, 1.286407882, 1.286380184, 1.286352799, 1.286325724, 1.286298952, 1.286272479, 1.286246299, 1.286220408, 1.286194801, 1.286169474, 1.286144421, 1.286119638, 1.286095122, 1.286070867, 1.28604687, 1.286023127, 1.285999633, 1.285976384, 1.285953377, 1.285930609, 1.285908074, 1.285885771, 1.285863694, 1.285841842, 1.285820209, 1.285798794 };
|
||||
private readonly static double[] T_Table_15 = { 4.16529977, 2.281930588, 1.924319657, 1.778192164, 1.699362566, 1.650173154, 1.616591737, 1.59222144, 1.573735785, 1.559235933, 1.547559766, 1.537956495, 1.529919606, 1.523095061, 1.517227969, 1.51213017, 1.507659754, 1.503707672, 1.500188756, 1.497035518, 1.494193795, 1.491619612, 1.489276897, 1.487135783, 1.485171326, 1.483362535, 1.481691617, 1.48014339, 1.478704821, 1.477364662, 1.47611315, 1.474941772, 1.473843072, 1.47281049, 1.471838233, 1.470921166, 1.470054719, 1.469234815, 1.468457801, 1.467720399, 1.467019655, 1.466352901, 1.465717725, 1.465111933, 1.464533534, 1.463980712, 1.463451805, 1.462945295, 1.46245979, 1.461994009, 1.461546775, 1.461117, 1.460703683, 1.460305896, 1.45992278, 1.459553538, 1.45919743, 1.458853767, 1.458521908, 1.458201256, 1.457891251, 1.457591373, 1.457301133, 1.457020074, 1.456747768, 1.45648381, 1.456227824, 1.455979454, 1.455738365, 1.455504241, 1.455276784, 1.455055715, 1.454840767, 1.45463169, 1.454428246, 1.454230212, 1.454037373, 1.453849529, 1.453666487, 1.453488066, 1.453314093, 1.453144404, 1.452978842, 1.452817259, 1.452659513, 1.452505469, 1.452354998, 1.452207977, 1.452064289, 1.451923821, 1.451786468, 1.451652126, 1.451520697, 1.451392088, 1.451266209, 1.451142973, 1.451022299, 1.450904108, 1.450788323, 1.450674871, 1.450563684, 1.450454694, 1.450347836, 1.450243048, 1.450140271, 1.450039448, 1.449940523, 1.449843444, 1.449748158, 1.449654617, 1.449562773, 1.449472581, 1.449383997, 1.449296977, 1.449211481, 1.449127468, 1.449044902, 1.448963744, 1.448883959, 1.448805513, 1.448728372, 1.448652503, 1.448577876, 1.44850446, 1.448432226, 1.448361146, 1.448291192, 1.448222337, 1.448154557, 1.448087826, 1.44802212, 1.447957415, 1.447893688, 1.447830919, 1.447769085, 1.447708165, 1.44764814, 1.44758899, 1.447530695, 1.447473238, 1.447416601, 1.447360765, 1.447305715, 1.447251433, 1.447197905, 1.447145113, 1.447093044, 1.447041682, 1.446991013, 1.446941023, 1.446891698, 1.446843026, 1.446794994, 1.446747588, 1.446700797, 1.446654609, 1.446609012, 1.446563996, 1.446519548, 1.446475659, 1.446432318, 1.446389514, 1.446347238, 1.44630548, 1.44626423, 1.44622348, 1.44618322, 1.446143442, 1.446104137, 1.446065296, 1.446026911, 1.445988975, 1.44595148, 1.445914417, 1.44587778, 1.445841561, 1.445805753, 1.445770349, 1.445735343, 1.445700727, 1.445666495, 1.445632641, 1.445599159, 1.445566042, 1.445533284, 1.445500881, 1.445468825, 1.445437112, 1.445405736, 1.445374691, 1.445343973, 1.445313576, 1.445283495, 1.445253726, 1.445224264, 1.445195103, 1.445166239, 1.445137668, 1.445109385, 1.445081387 };
|
||||
private readonly static double[] T_Table_10 = { 6.313751515, 2.91998558, 2.353363435, 2.131846786, 2.015048373, 1.943180281, 1.894578605, 1.859548038, 1.833112933, 1.812461123, 1.795884819, 1.782287556, 1.770933396, 1.761310136, 1.753050356, 1.745883676, 1.739606726, 1.734063607, 1.729132812, 1.724718243, 1.720742903, 1.717144374, 1.713871528, 1.71088208, 1.708140761, 1.70561792, 1.703288446, 1.701130934, 1.699127027, 1.697260887, 1.695518783, 1.693888748, 1.692360309, 1.690924255, 1.689572458, 1.688297714, 1.68709362, 1.68595446, 1.684875122, 1.683851013, 1.682878002, 1.681952357, 1.681070703, 1.680229977, 1.679427393, 1.678660414, 1.677926722, 1.677224196, 1.676550893, 1.675905025, 1.67528495, 1.674689154, 1.674116237, 1.673564906, 1.673033965, 1.672522303, 1.672028888, 1.671552762, 1.671093032, 1.670648865, 1.670219484, 1.669804163, 1.669402222, 1.669013025, 1.668635976, 1.668270514, 1.667916114, 1.667572281, 1.667238549, 1.666914479, 1.666599658, 1.666293696, 1.665996224, 1.665706893, 1.665425373, 1.665151353, 1.664884537, 1.664624645, 1.664371409, 1.664124579, 1.663883913, 1.663649184, 1.663420175, 1.663196679, 1.6629785, 1.662765449, 1.662557349, 1.662354029, 1.662155326, 1.661961084, 1.661771155, 1.661585397, 1.661403674, 1.661225855, 1.661051817, 1.66088144, 1.66071461, 1.660551217, 1.660391156, 1.660234326, 1.66008063, 1.659929976, 1.659782273, 1.659637437, 1.659495383, 1.659356034, 1.659219312, 1.659085144, 1.658953458, 1.658824187, 1.658697265, 1.658572629, 1.658450216, 1.658329969, 1.65821183, 1.658095744, 1.657981659, 1.657869522, 1.657759285, 1.657650899, 1.657544319, 1.657439499, 1.657336397, 1.65723497, 1.657135178, 1.657036982, 1.656940344, 1.656845226, 1.656751594, 1.656659413, 1.656568649, 1.65647927, 1.656391244, 1.656304542, 1.656219133, 1.656134988, 1.65605208, 1.655970382, 1.655889868, 1.655810511, 1.655732287, 1.655655173, 1.655579143, 1.655504177, 1.655430251, 1.655357345, 1.655285437, 1.655214506, 1.655144534, 1.6550755, 1.655007387, 1.654940175, 1.654873847, 1.654808385, 1.654743774, 1.654679996, 1.654617035, 1.654554875, 1.654493503, 1.654432901, 1.654373057, 1.654313957, 1.654255585, 1.654197929, 1.654140976, 1.654084713, 1.654029128, 1.653974208, 1.653919942, 1.653866317, 1.653813324, 1.653760949, 1.653709184, 1.653658017, 1.653607437, 1.653557435, 1.653508002, 1.653459126, 1.6534108, 1.653363013, 1.653315758, 1.653269024, 1.653222803, 1.653177088, 1.653131869, 1.653087138, 1.653042889, 1.652999113, 1.652955802, 1.652912949, 1.652870547, 1.652828589, 1.652787068, 1.652745977, 1.65270531, 1.652665059, 1.652625219, 1.652585784, 1.652546746, 1.652508101 };
|
||||
private readonly static double[] T_Table_05 = { 12.70620474, 4.30265273, 3.182446305, 2.776445105, 2.570581836, 2.446911851, 2.364624252, 2.306004135, 2.262157163, 2.228138852, 2.20098516, 2.17881283, 2.160368656, 2.144786688, 2.131449546, 2.119905299, 2.109815578, 2.10092204, 2.093024054, 2.085963447, 2.079613845, 2.073873068, 2.06865761, 2.063898562, 2.059538553, 2.055529439, 2.051830516, 2.048407142, 2.045229642, 2.042272456, 2.039513446, 2.036933343, 2.034515297, 2.032244509, 2.030107928, 2.028094001, 2.026192463, 2.024394164, 2.02269092, 2.02107539, 2.01954097, 2.018081703, 2.016692199, 2.015367574, 2.014103389, 2.012895599, 2.011740514, 2.010634758, 2.009575237, 2.008559112, 2.00758377, 2.006646805, 2.005745995, 2.004879288, 2.004044783, 2.003240719, 2.002465459, 2.001717484, 2.000995378, 2.000297822, 1.999623585, 1.998971517, 1.998340543, 1.997729654, 1.997137908, 1.996564419, 1.996008354, 1.995468931, 1.994945415, 1.994437112, 1.993943368, 1.993463567, 1.992997126, 1.992543495, 1.992102154, 1.99167261, 1.991254395, 1.990847069, 1.99045021, 1.990063421, 1.989686323, 1.989318557, 1.98895978, 1.988609667, 1.988267907, 1.987934206, 1.987608282, 1.987289865, 1.9869787, 1.986674541, 1.986377154, 1.986086317, 1.985801814, 1.985523442, 1.985251004, 1.984984312, 1.984723186, 1.984467455, 1.984216952, 1.983971519, 1.983731003, 1.983495259, 1.983264145, 1.983037526, 1.982815274, 1.982597262, 1.98238337, 1.982173483, 1.98196749, 1.981765282, 1.981566757, 1.981371815, 1.981180359, 1.980992298, 1.980807541, 1.980626002, 1.980447599, 1.980272249, 1.980099876, 1.979930405, 1.979763763, 1.979599878, 1.979438685, 1.979280117, 1.979124109, 1.978970602, 1.978819535, 1.97867085, 1.978524491, 1.978380405, 1.978238539, 1.978098842, 1.977961264, 1.977825758, 1.977692277, 1.977560777, 1.977431212, 1.977303542, 1.977177724, 1.97705372, 1.976931489, 1.976810994, 1.976692198, 1.976575066, 1.976459563, 1.976345655, 1.976233309, 1.976122494, 1.976013178, 1.975905331, 1.975798924, 1.975693928, 1.975590315, 1.975488058, 1.975387131, 1.975287508, 1.975189163, 1.975092073, 1.974996213, 1.97490156, 1.974808092, 1.974715786, 1.974624621, 1.974534576, 1.97444563, 1.974357764, 1.974270957, 1.974185191, 1.974100447, 1.974016708, 1.973933954, 1.973852169, 1.973771337, 1.97369144, 1.973612462, 1.973534388, 1.973457202, 1.973380889, 1.973305434, 1.973230823, 1.973157042, 1.973084077, 1.973011915, 1.972940542, 1.972869946, 1.972800114, 1.972731033, 1.972662692, 1.972595079, 1.972528182, 1.97246199, 1.972396491, 1.972331676, 1.972267533, 1.972204051, 1.972141222, 1.972079034, 1.972017478, 1.971956544, 1.971896224 };
|
||||
private readonly static double[] T_Table_01 = { 63.65674116, 9.924843201, 5.84090931, 4.604094871, 4.032142984, 3.707428021, 3.499483297, 3.355387331, 3.249835542, 3.169272673, 3.105806516, 3.054539589, 3.012275839, 2.976842734, 2.946712883, 2.920781622, 2.89823052, 2.878440473, 2.860934606, 2.84533971, 2.831359558, 2.818756061, 2.807335684, 2.796939505, 2.787435814, 2.778714533, 2.770682957, 2.763262455, 2.756385904, 2.749995654, 2.744041919, 2.738481482, 2.733276642, 2.728394367, 2.723805589, 2.71948463, 2.715408722, 2.711557602, 2.707913184, 2.704459267, 2.701181304, 2.698066186, 2.695102079, 2.692278266, 2.689585019, 2.687013492, 2.684555618, 2.682204027, 2.679951974, 2.677793271, 2.675722234, 2.673733631, 2.671822636, 2.669984796, 2.668215988, 2.666512398, 2.664870482, 2.663286954, 2.661758752, 2.660283029, 2.658857127, 2.657478565, 2.656145025, 2.654854337, 2.653604469, 2.652393515, 2.651219685, 2.650081299, 2.648976774, 2.647904624, 2.646863444, 2.645851913, 2.644868782, 2.643912872, 2.642983067, 2.642078313, 2.641197611, 2.640340015, 2.639504627, 2.638690596, 2.637897113, 2.63712341, 2.636368757, 2.635632458, 2.634913852, 2.634212309, 2.633527229, 2.632858038, 2.632204191, 2.631565166, 2.630940463, 2.630329608, 2.629732145, 2.629147638, 2.628575671, 2.628015844, 2.627467774, 2.626931096, 2.626405457, 2.625890521, 2.625385965, 2.624891476, 2.624406758, 2.623931523, 2.623465496, 2.623008411, 2.622560015, 2.622120061, 2.621688313, 2.621264543, 2.620848534, 2.620440073, 2.620038957, 2.619644989, 2.619257981, 2.618877749, 2.618504116, 2.618136914, 2.617775976, 2.617421145, 2.617072266, 2.616729191, 2.616391776, 2.616059883, 2.615733377, 2.615412127, 2.615096008, 2.614784899, 2.61447868, 2.614177238, 2.613880461, 2.613588242, 2.613300477, 2.613017065, 2.612737908, 2.61246291, 2.61219198, 2.611925028, 2.611661966, 2.611402711, 2.611147181, 2.610895295, 2.610646976, 2.61040215, 2.610160742, 2.609922682, 2.609687901, 2.609456331, 2.609227907, 2.609002566, 2.608780245, 2.608560883, 2.608344423, 2.608130807, 2.60791998, 2.607711886, 2.607506474, 2.607303692, 2.607103489, 2.606905817, 2.606710628, 2.606517876, 2.606327515, 2.606139501, 2.605953791, 2.605770342, 2.605589114, 2.605410067, 2.605233162, 2.605058359, 2.604885623, 2.604714916, 2.604546204, 2.60437945, 2.604214622, 2.604051686, 2.60389061, 2.603731363, 2.603573912, 2.603418229, 2.603264282, 2.603112045, 2.602961487, 2.602812582, 2.602665303, 2.602519622, 2.602375515, 2.602232955, 2.602091918, 2.60195238, 2.601814317, 2.601677705, 2.601542523, 2.601408747, 2.601276355, 2.601145327, 2.601015642, 2.600887278, 2.600760216, 2.600634436 };
|
||||
}
|
||||
|
||||
public enum Significance
|
||||
{
|
||||
P01,
|
||||
P05,
|
||||
P10,
|
||||
P15,
|
||||
P20,
|
||||
P25
|
||||
}
|
||||
|
||||
public class AverageSpeed
|
||||
{
|
||||
/// <summary>Average speed in units per second</summary>
|
||||
public double Average { get; private set; }
|
||||
public TimeSpan SlowWindow { get; }
|
||||
public TimeSpan FastWindow { get; }
|
||||
public Significance SlowSignificance { get; }
|
||||
public Significance FastSignificance { get; }
|
||||
|
||||
private DateTime start;
|
||||
private TimeSpan lastTime;
|
||||
private double lastPosition = double.NaN;
|
||||
|
||||
private readonly record struct Point(TimeSpan Time, double Velocity);
|
||||
private readonly LinkedList<Point> speeds = new();
|
||||
private const int MAX_SPEEDS = 200;
|
||||
|
||||
public AverageSpeed() : this(TimeSpan.FromSeconds(15), Significance.P10, TimeSpan.FromSeconds(3), Significance.P01) { }
|
||||
|
||||
/// <param name="slowWindow">Total moving average time window</param>
|
||||
/// <param name="slowSignificance">T-test signifance level at which the newest speed will be considered different from the slow window's mean speed.</param>
|
||||
/// <param name="fastWindow">A shorter moving window of the most resent speeds. The average speed in <paramref name="fastWindow"/> is compared to the average speed in the rest of <paramref name="slowWindow"/> to quickly detect large changes in speed.</param>
|
||||
/// <param name="fastSignificance">T-test significance level at which the mean speed in <paramref name="fastWindow"/> will be considered different from the mean speed of the remainder of <paramref name="slowWindow"/>.</param>
|
||||
public AverageSpeed(TimeSpan slowWindow, Significance slowSignificance, TimeSpan fastWindow, Significance fastSignificance)
|
||||
{
|
||||
SlowWindow = ArgumentValidator.EnsureGreaterThan(slowWindow, nameof(slowWindow), fastWindow);
|
||||
FastWindow = ArgumentValidator.EnsureGreaterThan(fastWindow, nameof(fastWindow), TimeSpan.Zero);
|
||||
SlowSignificance = slowSignificance;
|
||||
FastSignificance = fastSignificance;
|
||||
}
|
||||
|
||||
/// <summary>Add a new position to the moving average</summary>
|
||||
public void AddPosition(double position)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
if (start == default)
|
||||
start = now;
|
||||
|
||||
var time = now - start;
|
||||
|
||||
while (speeds.Count > MAX_SPEEDS || (speeds.Count > 2 && time - speeds.First.Value.Time > SlowWindow))
|
||||
speeds.RemoveFirst();
|
||||
|
||||
if (!double.IsNaN(lastPosition))
|
||||
{
|
||||
var newSpeed = (position - lastPosition) / (time - lastTime).TotalSeconds;
|
||||
speeds.AddLast(new Point(time, newSpeed));
|
||||
}
|
||||
|
||||
lastTime = time;
|
||||
lastPosition = position;
|
||||
|
||||
Average = ComputeNextAverage();
|
||||
}
|
||||
|
||||
private double ComputeNextAverage()
|
||||
{
|
||||
if (speeds.Count == 0)
|
||||
return 0;
|
||||
else if (speeds.Count == 1)
|
||||
return speeds.Last.Value.Velocity;
|
||||
else
|
||||
{
|
||||
var n_newest = speeds.Count(s => s.Time > lastTime.Subtract(FastWindow));
|
||||
|
||||
var n_oldest = speeds.Count - n_newest;
|
||||
|
||||
if (speeds.Take(n_oldest).T_Test_2By(s => s.Velocity, speeds.TakeLast(n_newest), FastSignificance))
|
||||
{
|
||||
//Speeds in FastWindow are significantly different from reset of speeds in SlowWindow.
|
||||
//Discard older speeds and keep only speeds in FastWindow
|
||||
for (; n_oldest > 0; n_oldest--)
|
||||
speeds.RemoveFirst();
|
||||
|
||||
return speeds.Average(s => s.Velocity);
|
||||
}
|
||||
else
|
||||
return
|
||||
speeds.T_Test_1By(s => s.Velocity, Average, SlowSignificance)
|
||||
? speeds.Average(s => s.Velocity)
|
||||
: Average;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,10 +238,10 @@ namespace AaxDecrypter
|
||||
#region Download Stream Reader
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanRead => true;
|
||||
public override bool CanRead => _readFile.CanRead;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanSeek => _readFile.CanSeek;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanWrite => false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -7,6 +6,8 @@ namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
{
|
||||
protected override long InputFilePosition => InputFileStream.WritePosition;
|
||||
|
||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
|
||||
: base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
@@ -25,31 +26,9 @@ namespace AaxDecrypter
|
||||
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
// MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
|
||||
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
|
||||
{
|
||||
var rate = InputFileStream.WritePosition / (DateTime.Now - startTime).TotalSeconds;
|
||||
|
||||
var estTimeRemaining = (InputFileStream.Length - InputFileStream.WritePosition) / rate;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = 100d * InputFileStream.WritePosition / InputFileStream.Length;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = InputFileStream.WritePosition,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
|
||||
while (InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
if (IsCanceled)
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.2.1.1</Version>
|
||||
<Version>9.2.3.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="5.0.0" />
|
||||
|
||||
@@ -16,8 +16,7 @@ namespace FileLiberator
|
||||
{
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4Operation Mp4Operation;
|
||||
private TimeSpan bookDuration;
|
||||
private long fileSize;
|
||||
private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
|
||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||
|
||||
public override Task CancelAsync() => Mp4Operation?.CancelAsync() ?? Task.CompletedTask;
|
||||
@@ -45,9 +44,6 @@ namespace FileLiberator
|
||||
|
||||
var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
|
||||
|
||||
bookDuration = m4bBook.Duration;
|
||||
fileSize = m4bBook.InputStream.Length;
|
||||
|
||||
OnTitleDiscovered(m4bBook.AppleTags.Title);
|
||||
OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor);
|
||||
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
||||
@@ -105,20 +101,22 @@ namespace FileLiberator
|
||||
|
||||
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var remainingSecsToProcess = (bookDuration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / bookDuration.TotalSeconds;
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / e.TotalDuration.TotalSeconds;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)(fileSize * progressPercent),
|
||||
TotalBytesToReceive = fileSize
|
||||
BytesReceived = (long)e.ProcessPosition.TotalSeconds,
|
||||
TotalBytesToReceive = (long)e.TotalDuration.TotalSeconds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileLiberator) + ".Tests")]
|
||||
@@ -62,10 +62,10 @@ namespace FileManager
|
||||
|
||||
public static implicit operator LongPath(string path)
|
||||
{
|
||||
if (!IsWindows) return new LongPath(path);
|
||||
|
||||
if (path is null) return null;
|
||||
|
||||
if (!IsWindows) return new LongPath(path);
|
||||
|
||||
//File I/O functions in the Windows API convert "/" to "\" as part of converting
|
||||
//the name to an NT-style name, except when using the "\\?\" prefix
|
||||
path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);
|
||||
|
||||
@@ -20,17 +20,21 @@ internal interface IClosingPropertyTag : IPropertyTag
|
||||
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag);
|
||||
}
|
||||
|
||||
public class ConditionalTagClass<TClass> : TagClass
|
||||
public class ConditionalTagCollection<TClass> : TagCollection
|
||||
{
|
||||
public ConditionalTagClass(bool caseSensative = true) :base(typeof(TClass), caseSensative) { }
|
||||
public ConditionalTagCollection(bool caseSensative = true) :base(typeof(TClass), caseSensative) { }
|
||||
|
||||
public void RegisterCondition(ITemplateTag templateTag, Func<TClass, bool> propertyGetter)
|
||||
/// <summary>
|
||||
/// Register a conditional tag.
|
||||
/// </summary>
|
||||
/// <param name="propertyGetter">A Func to get the condition's <see cref="bool"/> value from <see cref="TClass"/></param>
|
||||
public void Add(ITemplateTag templateTag, Func<TClass, bool> propertyGetter)
|
||||
{
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
|
||||
AddPropertyTag(new ConditionalTag(templateTag, Options, expr));
|
||||
}
|
||||
|
||||
|
||||
private class ConditionalTag : TagBase, IClosingPropertyTag
|
||||
{
|
||||
public Regex NameCloseMatcher { get; }
|
||||
@@ -51,14 +55,12 @@ public class ConditionalTagClass<TClass> : TagClass
|
||||
propertyTag = this;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
exactName = null;
|
||||
propertyTag = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
exactName = null;
|
||||
propertyTag = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ExpressionValue) : ExpressionValue;
|
||||
protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ValueExpression) : ValueExpression;
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ public class NamingTemplate
|
||||
{
|
||||
public string TemplateText { get; private set; }
|
||||
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
|
||||
public IEnumerable<ITemplateTag> TagsRegistered => Classes.SelectMany(p => p.TemplateTags).DistinctBy(f => f.TagName);
|
||||
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
|
||||
public IEnumerable<string> Warnings => errors.Concat(warnings);
|
||||
public IEnumerable<string> Errors => errors;
|
||||
|
||||
private Delegate templateToString;
|
||||
private readonly List<string> warnings = new();
|
||||
private readonly List<string> errors = new();
|
||||
private readonly IEnumerable<TagClass> Classes;
|
||||
private readonly IEnumerable<TagCollection> TagCollections;
|
||||
private readonly List<ITemplateTag> _tagsInUse = new();
|
||||
|
||||
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
||||
@@ -25,21 +25,18 @@ public class NamingTemplate
|
||||
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the <see cref="NamingTemplate"/> to
|
||||
/// Invoke the <see cref="NamingTemplate"/>
|
||||
/// </summary>
|
||||
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagClass{TClass}"/> and <see cref="ConditionalTagClass{TClass}"/></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
|
||||
public TemplatePart Evaluate(params object[] propertyClasses)
|
||||
{
|
||||
//Match propertyClasses to the arguments required by templateToString.DynamicInvoke()
|
||||
var delegateArgTypes = templateToString.GetType().GenericTypeArguments[..^1];
|
||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||
// First parameter is "this", so ignore it.
|
||||
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
|
||||
|
||||
object[] args = new object[delegateArgTypes.Length];
|
||||
|
||||
for (int i = 0; i < delegateArgTypes.Length; i++)
|
||||
args[i] = propertyClasses.First(o => o.GetType() == delegateArgTypes[i]);
|
||||
|
||||
if (args.Any(a => a is null))
|
||||
object[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i.GetType(), (_, i) => i).ToArray();
|
||||
|
||||
if (args.Length != delegateArgTypes.Count())
|
||||
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
||||
|
||||
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart;
|
||||
@@ -47,22 +44,17 @@ public class NamingTemplate
|
||||
|
||||
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
||||
/// <param name="template">The template string to parse</param>
|
||||
/// <param name="tagClasses">A collection of <see cref="TagClass"/> with
|
||||
/// <param name="tagCollections">A collection of <see cref="TagCollection"/> with
|
||||
/// properties registered to match to the <paramref name="template"/></param>
|
||||
public static NamingTemplate Parse(string template, IEnumerable<TagClass> tagClasses)
|
||||
public static NamingTemplate Parse(string template, IEnumerable<TagCollection> tagCollections)
|
||||
{
|
||||
var namingTemplate = new NamingTemplate(tagClasses);
|
||||
var namingTemplate = new NamingTemplate(tagCollections);
|
||||
try
|
||||
{
|
||||
BinaryNode intermediate = namingTemplate.IntermediateParse(template);
|
||||
Expression evalTree = GetExpressionTree(intermediate);
|
||||
|
||||
List<ParameterExpression> parameters = new();
|
||||
|
||||
foreach (var tagclass in tagClasses)
|
||||
parameters.Add(tagclass.Parameter);
|
||||
|
||||
namingTemplate.templateToString = Expression.Lambda(evalTree, parameters).Compile();
|
||||
namingTemplate.templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter)).Compile();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -71,9 +63,9 @@ public class NamingTemplate
|
||||
return namingTemplate;
|
||||
}
|
||||
|
||||
private NamingTemplate(IEnumerable<TagClass> properties)
|
||||
private NamingTemplate(IEnumerable<TagCollection> properties)
|
||||
{
|
||||
Classes = properties;
|
||||
TagCollections = properties;
|
||||
}
|
||||
|
||||
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
|
||||
@@ -84,7 +76,7 @@ public class NamingTemplate
|
||||
else if (node.IsConditional) return Expression.Condition(node.Expression, concatExpression(node), TemplatePart.Blank);
|
||||
else return concatExpression(node);
|
||||
|
||||
Expression concatExpression(BinaryNode node)
|
||||
static Expression concatExpression(BinaryNode node)
|
||||
=> TemplatePart.CreateConcatenation(GetExpressionTree(node.LeftChild), GetExpressionTree(node.RightChild));
|
||||
}
|
||||
|
||||
@@ -100,8 +92,8 @@ public class NamingTemplate
|
||||
|
||||
TemplateText = templateString;
|
||||
|
||||
BinaryNode currentNode = BinaryNode.CreateRoot();
|
||||
BinaryNode topNode = currentNode;
|
||||
BinaryNode topNode = BinaryNode.CreateRoot();
|
||||
BinaryNode currentNode = topNode;
|
||||
List<char> literalChars = new();
|
||||
|
||||
while (templateString.Length > 0)
|
||||
@@ -170,7 +162,7 @@ public class NamingTemplate
|
||||
{
|
||||
if (literalChars.Count != 0)
|
||||
{
|
||||
currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(new string(literalChars.ToArray())));
|
||||
currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(string.Concat(literalChars)));
|
||||
literalChars.Clear();
|
||||
}
|
||||
}
|
||||
@@ -178,11 +170,12 @@ public class NamingTemplate
|
||||
|
||||
private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression)
|
||||
{
|
||||
foreach (var pc in Classes)
|
||||
foreach (var pc in TagCollections)
|
||||
{
|
||||
if (pc.StartsWith(template, out exactName, out propertyTag, out valueExpression))
|
||||
return true;
|
||||
}
|
||||
|
||||
exactName = null;
|
||||
valueExpression = null;
|
||||
propertyTag = null;
|
||||
@@ -191,11 +184,12 @@ public class NamingTemplate
|
||||
|
||||
private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
||||
{
|
||||
foreach (var pc in Classes)
|
||||
foreach (var pc in TagCollections)
|
||||
{
|
||||
if (pc.StartsWithClosing(template, out exactName, out closingPropertyTag))
|
||||
return true;
|
||||
}
|
||||
|
||||
exactName = null;
|
||||
closingPropertyTag = null;
|
||||
return false;
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
||||
|
||||
public class PropertyTagClass<TClass> : TagClass
|
||||
{
|
||||
public PropertyTagClass(bool caseSensative = true) : base(typeof(TClass), caseSensative) { }
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type property.
|
||||
/// </summary>
|
||||
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
|
||||
public void RegisterProperty<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, PropertyFormatter<U> formatter = null)
|
||||
where U : struct
|
||||
=> RegisterPropertyInternal(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a non-nullable value type property
|
||||
/// </summary>
|
||||
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
|
||||
public void RegisterProperty<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, PropertyFormatter<U> formatter = null)
|
||||
where U : struct
|
||||
=> RegisterPropertyInternal(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a string type property.
|
||||
/// </summary>
|
||||
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the string property and a formatting string and returnes the value formatted to string</param>
|
||||
public void RegisterProperty(ITemplateTag templateTag, Func<TClass, string> propertyGetter, PropertyFormatter<string> formatter = null)
|
||||
=> RegisterPropertyInternal(templateTag, propertyGetter, formatter);
|
||||
|
||||
private void RegisterPropertyInternal(ITemplateTag templateTag, Delegate propertyGetter, Delegate formatter)
|
||||
{
|
||||
if (formatter?.Target is not null)
|
||||
throw new ArgumentException($"{nameof(formatter)} must be a static method");
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
|
||||
AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter?.Method));
|
||||
}
|
||||
|
||||
private class PropertyTag : TagBase
|
||||
{
|
||||
private readonly Func<Expression, Type, string, Expression> createToStringExpression;
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, MethodInfo formatter)
|
||||
: base(templateTag, propertyExpression)
|
||||
{
|
||||
var regexStr = formatter is null ? @$"^<{TemplateTag.TagName}>" : @$"^<{TemplateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>";
|
||||
NameMatcher = new Regex(regexStr, options);
|
||||
|
||||
//Create the ToString() expression for the TagBase.ExpressionValue's type.
|
||||
//If a formatter delegate was registered for this property, use that.
|
||||
//Otherwise use the object.Tostring() method.
|
||||
createToStringExpression
|
||||
= formatter is null
|
||||
? (expValue, retTyp, format) => Expression.Call(expValue, retTyp.GetMethod(nameof(object.ToString), Array.Empty<Type>()))
|
||||
: (expValue, retTyp, format) => Expression.Call(null, formatter, Expression.Constant(templateTag), expValue, Expression.Constant(format));
|
||||
}
|
||||
|
||||
protected override Expression GetTagExpression(string exactName, string formatString)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(ReturnType);
|
||||
|
||||
Expression toStringExpression
|
||||
= ReturnType == typeof(string)
|
||||
? createToStringExpression(Expression.Coalesce(ExpressionValue, Expression.Constant("")), ReturnType, formatString)
|
||||
: underlyingType is null
|
||||
? createToStringExpression(ExpressionValue, ReturnType, formatString)
|
||||
: Expression.Condition(
|
||||
Expression.PropertyOrField(ExpressionValue, "HasValue"),
|
||||
createToStringExpression(Expression.PropertyOrField(ExpressionValue, "Value"), underlyingType, formatString),
|
||||
Expression.Constant(""));
|
||||
|
||||
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
||||
|
||||
public class PropertyTagCollection<TClass> : TagCollection
|
||||
{
|
||||
private readonly Dictionary<Type, MulticastDelegate> defaultFormatters = new();
|
||||
|
||||
public PropertyTagCollection(bool caseSensative = true, params MulticastDelegate[] defaultFormatters) : base(typeof(TClass), caseSensative)
|
||||
{
|
||||
foreach (var formatter in defaultFormatters)
|
||||
{
|
||||
var parameters = formatter.Method.GetParameters();
|
||||
|
||||
if (formatter.Method.ReturnType != typeof(string)
|
||||
|| parameters.Length != 3
|
||||
|| parameters[0].ParameterType != typeof(ITemplateTag)
|
||||
|| parameters[2].ParameterType != typeof(string))
|
||||
throw new ArgumentException($"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter<T>({nameof(ITemplateTag)}, T, {nameof(String)})]");
|
||||
|
||||
this.defaultFormatters[parameters[1].ParameterType] = formatter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
||||
where TProperty : struct
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a nullable value type <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
|
||||
/// <param name="toString">ToString function that accepts the <typeparamref name="TProperty"/> property and returnes a string</param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, Func<TProperty, string> toString)
|
||||
where TProperty : struct
|
||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||
|
||||
/// <summary>
|
||||
/// Register a <typeparamref name="TClass"/> property
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
|
||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||
|
||||
/// <summary>
|
||||
/// Register a <typeparamref name="TClass"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam>
|
||||
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
|
||||
/// <param name="toString">ToString function that accepts the <typeparamref name="TProperty"/> property and returnes a string</param>
|
||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TProperty, string> toString)
|
||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||
|
||||
private void RegisterWithFormatter<TProperty, TPropertyValue>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
|
||||
if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null)
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
|
||||
else
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
|
||||
}
|
||||
|
||||
private void RegisterWithToString<TProperty, TPropertyValue>
|
||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TPropertyValue, string> toString)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||
|
||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, toString ?? ToStringFunc));
|
||||
}
|
||||
|
||||
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
|
||||
|
||||
private PropertyFormatter<T> GetDefaultFormatter<T>()
|
||||
{
|
||||
try
|
||||
{
|
||||
var del = defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(T)).Value;
|
||||
return del is null ? null : Delegate.CreateDelegate(typeof(PropertyFormatter<T>), del.Target, del.Method) as PropertyFormatter<T>;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private class PropertyTag<TPropertyValue> : TagBase
|
||||
{
|
||||
private Func<Expression, string, Expression> CreateToStringExpression { get; }
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
||||
: base(templateTag, propertyGetter)
|
||||
{
|
||||
NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options);
|
||||
CreateToStringExpression = (expVal, format) =>
|
||||
Expression.Call(
|
||||
formatter.Target is null ? null : Expression.Constant(formatter.Target),
|
||||
formatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
Expression.Constant(format));
|
||||
}
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString)
|
||||
: base(templateTag, propertyGetter)
|
||||
{
|
||||
NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}>", options);
|
||||
CreateToStringExpression = (expVal, _) =>
|
||||
Expression.Call(
|
||||
toString.Target is null ? null : Expression.Constant(toString.Target),
|
||||
toString.Method,
|
||||
expVal);
|
||||
}
|
||||
|
||||
protected override Expression GetTagExpression(string exactName, string formatString)
|
||||
{
|
||||
Expression toStringExpression
|
||||
= !ReturnType.IsValueType
|
||||
? Expression.Condition(
|
||||
Expression.Equal(ValueExpression, Expression.Constant(null)),
|
||||
Expression.Constant(""),
|
||||
CreateToStringExpression(ValueExpression, formatString))
|
||||
: Nullable.GetUnderlyingType(ReturnType) is null
|
||||
? CreateToStringExpression(ValueExpression, formatString)
|
||||
: Expression.Condition(
|
||||
Expression.PropertyOrField(ValueExpression, "HasValue"),
|
||||
CreateToStringExpression(Expression.PropertyOrField(ValueExpression, "Value"), formatString),
|
||||
Expression.Constant(""));
|
||||
|
||||
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ internal interface IPropertyTag
|
||||
Type ReturnType { get; }
|
||||
|
||||
/// <summary>The <see cref="Regex"/> used to match <see cref="TemplateTag"/> in template strings.</summary>
|
||||
public Regex NameMatcher { get; }
|
||||
Regex NameMatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the template string starts with <see cref="TemplateTag"/>, and if it does parse the tag to an <see cref="Expression"/>
|
||||
@@ -29,13 +29,13 @@ internal abstract class TagBase : IPropertyTag
|
||||
{
|
||||
public ITemplateTag TemplateTag { get; }
|
||||
public Regex NameMatcher { get; protected init; }
|
||||
public Type ReturnType => ExpressionValue.Type;
|
||||
protected Expression ExpressionValue { get; }
|
||||
public Type ReturnType => ValueExpression.Type;
|
||||
protected Expression ValueExpression { get; }
|
||||
|
||||
protected TagBase(ITemplateTag templateTag, Expression propertyExpression)
|
||||
{
|
||||
TemplateTag = templateTag;
|
||||
ExpressionValue = propertyExpression;
|
||||
ValueExpression = propertyExpression;
|
||||
}
|
||||
|
||||
/// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary>
|
||||
@@ -52,12 +52,10 @@ internal abstract class TagBase : IPropertyTag
|
||||
propertyValue = GetTagExpression(exactName, match.Groups.Count == 2 ? match.Groups[1].Value.Trim() : "");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
exactName = null;
|
||||
propertyValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
exactName = null;
|
||||
propertyValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
@@ -6,19 +7,18 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
|
||||
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
|
||||
public abstract class TagClass
|
||||
public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
{
|
||||
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagClass"/>'s TClass type.</summary>
|
||||
public ParameterExpression Parameter { get; }
|
||||
/// <summary>The <see cref="ITemplateTag"/>s registered with this <see cref="TagClass"/> </summary>
|
||||
public IEnumerable<ITemplateTag> TemplateTags => PropertyTags.Select(p => p.TemplateTag);
|
||||
/// <summary>The <see cref="ITemplateTag"/>s registered with this <see cref="TagCollection"/> </summary>
|
||||
public IEnumerator<ITemplateTag> GetEnumerator() => PropertyTags.Select(p => p.TemplateTag).GetEnumerator();
|
||||
|
||||
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagCollection"/>'s TClass type.</summary>
|
||||
internal ParameterExpression Parameter { get; }
|
||||
protected RegexOptions Options { get; } = RegexOptions.Compiled;
|
||||
private protected List<IPropertyTag> PropertyTags { get; } = new();
|
||||
private List<IPropertyTag> PropertyTags { get; } = new();
|
||||
|
||||
protected TagClass(Type classType, bool caseSensative = true)
|
||||
protected TagCollection(Type classType, bool caseSensative = true)
|
||||
{
|
||||
Parameter = Expression.Parameter(classType, classType.Name);
|
||||
Options |= caseSensative ? RegexOptions.None : RegexOptions.IgnoreCase;
|
||||
@@ -42,6 +42,7 @@ public abstract class TagClass
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
propertyValue = null;
|
||||
propertyTag = null;
|
||||
exactName = null;
|
||||
@@ -74,4 +75,6 @@ public abstract class TagClass
|
||||
if (!PropertyTags.Any(c => c.TemplateTag.TagName == propertyTag.TemplateTag.TagName))
|
||||
PropertyTags.Add(propertyTag);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class TemplatePart : IEnumerable<TemplatePart>
|
||||
public ITemplateTag TemplateTag { get; }
|
||||
|
||||
/// <summary>The evaluated string.</summary>
|
||||
public string Value { get; set; }
|
||||
public string Value { get; }
|
||||
|
||||
private TemplatePart previous;
|
||||
private TemplatePart next;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileManager) + ".Tests")]
|
||||
@@ -93,6 +93,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacOSConfigApp", "LoadByOS\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{5F65A509-26E3-4B02-B403-EEB6F0EF391F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationUiBase", "LibationUiBase\LibationUiBase.csproj", "{E90C4651-AF11-41B4-A839-10082D0391F9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hangover", "Hangover", "{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI", "{53758A35-1C7E-4702-9B96-433ABA457B37}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -207,6 +215,10 @@ Global
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -218,21 +230,21 @@ Global
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {47E27674-595D-4F7A-8CFB-127E768E1D1E}
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F} = {185AC9FF-381E-4AA1-B649-9771F4917214}
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
@@ -241,6 +253,10 @@ Global
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{53758A35-1C7E-4702-9B96-433ABA457B37} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{47E27674-595D-4F7A-8CFB-127E768E1D1E} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace LibationAvalonia.Dialogs
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.resetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.EditingTemplate.Name}";
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.resetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.EditingTemplate.Name}";
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.EditingTemplate.Description;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
TemplateEditor
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
|
||||
<IsPublishable>true</IsPublishable>
|
||||
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
@@ -82,12 +80,6 @@
|
||||
<None Remove="Assets\up.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
@@ -117,10 +109,13 @@
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationUiBase\LibationUiBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="glass-with-glow_256.svg">
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public interface ILogForm
|
||||
{
|
||||
void WriteLine(string text);
|
||||
}
|
||||
|
||||
// decouple serilog and form. include convenience factory method
|
||||
public class LogMe
|
||||
{
|
||||
public event EventHandler<string> LogInfo;
|
||||
public event EventHandler<string> LogErrorString;
|
||||
public event EventHandler<(Exception, string)> LogError;
|
||||
|
||||
private LogMe()
|
||||
{
|
||||
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
}
|
||||
private static ILogForm LogForm;
|
||||
public static LogMe RegisterForm<T>(T form) where T : ILogForm
|
||||
{
|
||||
var logMe = new LogMe();
|
||||
|
||||
if (form is null)
|
||||
return logMe;
|
||||
|
||||
LogForm = form;
|
||||
|
||||
logMe.LogInfo += LogMe_LogInfo;
|
||||
logMe.LogErrorString += LogMe_LogErrorString;
|
||||
logMe.LogError += LogMe_LogError;
|
||||
|
||||
return logMe;
|
||||
}
|
||||
|
||||
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
|
||||
}
|
||||
|
||||
private static async void LogMe_LogErrorString(object sender, string text)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(text));
|
||||
|
||||
private static async void LogMe_LogInfo(object sender, string text)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(text));
|
||||
|
||||
public void Info(string text) => LogInfo?.Invoke(this, text);
|
||||
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
||||
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -130,8 +131,17 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
// state validation
|
||||
if (e is null ||
|
||||
e.Definition.PictureId is null ||
|
||||
Book?.PictureId is null ||
|
||||
e.Picture is null ||
|
||||
e.Picture.Length == 0)
|
||||
return;
|
||||
|
||||
// logic validation
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(e.Picture);
|
||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||
|
||||
@@ -12,6 +12,7 @@ using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
@@ -394,7 +395,7 @@ $@" Title: {libraryBook.Book.Title}
|
||||
return ProcessBookResult.FailedRetry;
|
||||
}
|
||||
|
||||
private string SkipDialogText => @"
|
||||
private static string SkipDialogText => @"
|
||||
An error occurred while trying to process this book.
|
||||
{0}
|
||||
|
||||
@@ -404,9 +405,9 @@ An error occurred while trying to process this book.
|
||||
|
||||
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
|
||||
".Trim();
|
||||
private MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
|
||||
private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
|
||||
private DialogResult SkipResult => DialogResult.Ignore;
|
||||
private static MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
|
||||
private static MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
|
||||
private static DialogResult SkipResult => DialogResult.Ignore;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace LibationAvalonia.Views
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Where to export Library",
|
||||
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books),
|
||||
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}.xlsx",
|
||||
DefaultExtension = "xlsx",
|
||||
ShowOverwritePrompt = true,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
Title = $"Locate the audio file for '{entry.Book.Title}'",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books),
|
||||
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } },
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace LibationFileManager
|
||||
bool IsFilePath { get; }
|
||||
LongPath BaseDirectory { get; }
|
||||
string DefaultTemplate { get; }
|
||||
string TemplateName { get; }
|
||||
string TemplateDescription { get; }
|
||||
Templates Folder { get; }
|
||||
Templates File { get; }
|
||||
Templates Name { get; }
|
||||
@@ -28,6 +30,8 @@ namespace LibationFileManager
|
||||
public bool IsFilePath => EditingTemplate is not Templates.ChapterTitleTemplate;
|
||||
public LongPath BaseDirectory { get; private init; }
|
||||
public string DefaultTemplate { get; private init; }
|
||||
public string TemplateName { get; private init; }
|
||||
public string TemplateDescription { get; private init; }
|
||||
public Templates Folder { get; private set; }
|
||||
public Templates File { get; private set; }
|
||||
public Templates Name { get; private set; }
|
||||
@@ -99,7 +103,10 @@ namespace LibationFileManager
|
||||
{
|
||||
_editingTemplate = template,
|
||||
BaseDirectory = baseDir,
|
||||
DefaultTemplate = T.DefaultTemplate
|
||||
DefaultTemplate = T.DefaultTemplate,
|
||||
TemplateName = T.Name,
|
||||
TemplateDescription = T.Description
|
||||
|
||||
};
|
||||
|
||||
if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
|
||||
@@ -118,7 +125,9 @@ namespace LibationFileManager
|
||||
var templateEditor = new TemplateEditor<T>
|
||||
{
|
||||
_editingTemplate = nameTemplate,
|
||||
DefaultTemplate = T.DefaultTemplate
|
||||
DefaultTemplate = T.DefaultTemplate,
|
||||
TemplateName = T.Name,
|
||||
TemplateDescription = T.Description
|
||||
};
|
||||
|
||||
if (templateEditor.IsFolder || templateEditor.IsFilePath)
|
||||
|
||||
@@ -6,14 +6,15 @@ using AaxDecrypter;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using FileManager.NamingTemplate;
|
||||
using Serilog.Formatting;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface ITemplate
|
||||
{
|
||||
static abstract string Name { get; }
|
||||
static abstract string Description { get; }
|
||||
static abstract string DefaultTemplate { get; }
|
||||
static abstract IEnumerable<TagClass> TagClass { get; }
|
||||
static abstract IEnumerable<TagCollection> TagCollections { get; }
|
||||
}
|
||||
|
||||
public abstract class Templates
|
||||
@@ -21,7 +22,7 @@ namespace LibationFileManager
|
||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||
|
||||
//Assign the properties in the static constructor will require all
|
||||
//Assigning the properties in the static constructor will require all
|
||||
//Templates users to have a valid configuration file. To allow tests
|
||||
//to work without access to Configuration, only load templates on demand.
|
||||
private static FolderTemplate _folder;
|
||||
@@ -35,57 +36,57 @@ namespace LibationFileManager
|
||||
public static ChapterTitleTemplate ChapterTitle => _chapterTitle ??= GetTemplate<ChapterTitleTemplate>(Configuration.Instance.ChapterTitleTemplate);
|
||||
|
||||
#region Template Parsing
|
||||
|
||||
public static T GetTemplate<T>(string templateText) where T : Templates, ITemplate, new()
|
||||
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>();
|
||||
|
||||
public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new()
|
||||
{
|
||||
var namingTemplate = NamingTemplate.Parse(templateText, T.TagClass);
|
||||
var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections);
|
||||
|
||||
template = new() { Template = namingTemplate };
|
||||
template = new() { NamingTemplate = namingTemplate };
|
||||
return !namingTemplate.Errors.Any();
|
||||
}
|
||||
|
||||
private static T GetDefaultTemplate<T>() where T : Templates, ITemplate, new()
|
||||
=> new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagClass) };
|
||||
=> new() { NamingTemplate = NamingTemplate.Parse(T.DefaultTemplate, T.TagCollections) };
|
||||
|
||||
static Templates()
|
||||
{
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
|
||||
(_,e) => _folder = GetTemplate<FolderTemplate>((string)e.NewValue);
|
||||
(_,e) => _folder = GetTemplate<FolderTemplate>((string)e.NewValue);
|
||||
|
||||
Configuration.Instance.PropertyChanged
|
||||
+= [PropertyChangeFilter(nameof(Configuration.FileTemplate))]
|
||||
(_, e) => _file = GetTemplate<FileTemplate>((string)e.NewValue);
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.FileTemplate))]
|
||||
(_, e) => _file = GetTemplate<FileTemplate>((string)e.NewValue);
|
||||
|
||||
Configuration.Instance.PropertyChanged
|
||||
+= [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
|
||||
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue);
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
|
||||
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue);
|
||||
|
||||
Configuration.Instance.PropertyChanged
|
||||
+= [PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||
Configuration.Instance.PropertyChanged +=
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Template Properties
|
||||
public IEnumerable<TemplateTags> TagsRegistered => Template.TagsRegistered.Cast<TemplateTags>();
|
||||
public IEnumerable<TemplateTags> TagsInUse => Template.TagsInUse.Cast<TemplateTags>();
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public string TemplateText => Template.TemplateText;
|
||||
protected NamingTemplate Template { get; private set; }
|
||||
|
||||
public IEnumerable<TemplateTags> TagsRegistered => NamingTemplate.TagsRegistered.Cast<TemplateTags>();
|
||||
public IEnumerable<TemplateTags> TagsInUse => NamingTemplate.TagsInUse.Cast<TemplateTags>();
|
||||
public string TemplateText => NamingTemplate.TemplateText;
|
||||
protected NamingTemplate NamingTemplate { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region validation
|
||||
|
||||
public virtual IEnumerable<string> Errors => Template.Errors;
|
||||
public virtual IEnumerable<string> Errors => NamingTemplate.Errors;
|
||||
public bool IsValid => !Errors.Any();
|
||||
|
||||
public virtual IEnumerable<string> Warnings => Template.Warnings;
|
||||
public virtual IEnumerable<string> Warnings => NamingTemplate.Warnings;
|
||||
public bool HasWarnings => Warnings.Any();
|
||||
|
||||
#endregion
|
||||
@@ -96,7 +97,7 @@ namespace LibationFileManager
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
return string.Join("", Template.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
|
||||
return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false)
|
||||
@@ -127,7 +128,7 @@ namespace LibationFileManager
|
||||
{
|
||||
fileExtension = FileUtility.GetStandardizedExtension(fileExtension);
|
||||
|
||||
var parts = Template.Evaluate(dtos).ToList();
|
||||
var parts = NamingTemplate.Evaluate(dtos).ToList();
|
||||
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
|
||||
|
||||
//Remove 1 character from the end of the longest filename part until
|
||||
@@ -152,8 +153,8 @@ namespace LibationFileManager
|
||||
part.Insert(maxIndex, maxEntry.Remove(maxLength - 1, 1));
|
||||
}
|
||||
}
|
||||
//Any
|
||||
var fullPath = Path.Combine(pathParts.Select(fileParts => string.Join("", fileParts)).Prepend(baseDir).ToArray());
|
||||
|
||||
var fullPath = Path.Combine(pathParts.Select(fileParts => string.Concat(fileParts)).Prepend(baseDir).ToArray());
|
||||
|
||||
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
|
||||
}
|
||||
@@ -163,7 +164,7 @@ namespace LibationFileManager
|
||||
/// returned as empty directories and are taken care of by Path.Combine()
|
||||
/// </summary>
|
||||
/// <returns>A List of template directories. Each directory is a list of template part strings</returns>
|
||||
private List<List<string>> GetPathParts(IEnumerable<string> templateParts)
|
||||
private static List<List<string>> GetPathParts(IEnumerable<string> templateParts)
|
||||
{
|
||||
List<List<string>> directories = new();
|
||||
List<string> dir = new();
|
||||
@@ -190,69 +191,65 @@ namespace LibationFileManager
|
||||
|
||||
#region Registered Template Properties
|
||||
|
||||
private static readonly PropertyTagClass<LibraryBookDto> filePropertyTags = GetFilePropertyTags();
|
||||
private static readonly ConditionalTagClass<LibraryBookDto> conditionalTags = GetConditionalTags();
|
||||
private static readonly List<TagClass> chapterPropertyTags = GetChapterPropertyTags();
|
||||
|
||||
private static ConditionalTagClass<LibraryBookDto> GetConditionalTags()
|
||||
private static readonly PropertyTagCollection<LibraryBookDto> filePropertyTags =
|
||||
new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter)
|
||||
{
|
||||
ConditionalTagClass<LibraryBookDto> lbConditions = new();
|
||||
//Don't allow formatting of Id
|
||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||
{ TemplateTags.Title, lb => lb.Title },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.Author, lb => lb.AuthorNames },
|
||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
||||
{ TemplateTags.Narrator, lb => lb.NarratorNames },
|
||||
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
||||
{ TemplateTags.Language, lb => lb.Language },
|
||||
//Don't allow formatting of LanguageShort
|
||||
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
|
||||
{ TemplateTags.Bitrate, lb => lb.BitRate },
|
||||
{ TemplateTags.SampleRate, lb => lb.SampleRate },
|
||||
{ TemplateTags.Channels, lb => lb.Channels },
|
||||
{ TemplateTags.Account, lb => lb.Account },
|
||||
{ TemplateTags.Locale, lb => lb.Locale },
|
||||
{ TemplateTags.YearPublished, lb => lb.YearPublished },
|
||||
{ TemplateTags.DatePublished, lb => lb.DatePublished },
|
||||
{ TemplateTags.DateAdded, lb => lb.DateAdded },
|
||||
{ TemplateTags.FileDate, lb => lb.FileDate },
|
||||
};
|
||||
|
||||
lbConditions.RegisterCondition(TemplateTags.IfSeries, lb => lb.IsSeries);
|
||||
lbConditions.RegisterCondition(TemplateTags.IfPodcast, lb => lb.IsPodcast);
|
||||
lbConditions.RegisterCondition(TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast);
|
||||
|
||||
return lbConditions;
|
||||
}
|
||||
|
||||
private static PropertyTagClass<LibraryBookDto> GetFilePropertyTags()
|
||||
private static readonly List<TagCollection> chapterPropertyTags = new()
|
||||
{
|
||||
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||
lbProperties.RegisterProperty(TemplateTags.Id, lb => lb.AudibleProductId);
|
||||
lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')), StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Author, lb => lb.AuthorNames, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FirstAuthor, lb => lb.FirstAuthor, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Narrator, lb => lb.NarratorNames, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FirstNarrator, lb => lb.FirstNarrator, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.SeriesNumber, lb => lb.SeriesNumber, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Language, lb => lb.Language, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language), StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Bitrate, lb => lb.BitRate, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Channels, lb => lb.Channels, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Account, lb => lb.Account, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Locale, lb => lb.Locale, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.YearPublished, lb => lb.YearPublished, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.DatePublished, lb => lb.DatePublished, DateTimeFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.DateAdded, lb => lb.DateAdded, DateTimeFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter);
|
||||
return lbProperties;
|
||||
}
|
||||
new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter)
|
||||
{
|
||||
{ TemplateTags.Title, lb => lb.Title },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||
},
|
||||
new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
|
||||
{
|
||||
{ TemplateTags.ChCount, m => m.PartsTotal },
|
||||
{ TemplateTags.ChNumber, m => m.PartsPosition },
|
||||
{ TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)) },
|
||||
{ TemplateTags.ChTitle, m => m.Title },
|
||||
{ TemplateTags.FileDate, m => m.FileDate }
|
||||
}
|
||||
};
|
||||
|
||||
private static List<TagClass> GetChapterPropertyTags()
|
||||
private static readonly ConditionalTagCollection<LibraryBookDto> conditionalTags = new()
|
||||
{
|
||||
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||
PropertyTagClass<MultiConvertFileProperties> multiConvertProperties = new();
|
||||
|
||||
lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb?.Title?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName, StringFormatter);
|
||||
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChCount, lb => lb.PartsTotal, IntegerFormatter);
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChNumber, lb => lb.PartsPosition, IntegerFormatter);
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)));
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChTitle, m => m.Title, StringFormatter);
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter);
|
||||
|
||||
return new List<TagClass> { lbProperties, multiConvertProperties };
|
||||
}
|
||||
{ TemplateTags.IfSeries, lb => lb.IsSeries },
|
||||
{ TemplateTags.IfPodcast, lb => lb.IsPodcast },
|
||||
{ TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast },
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tag Formatters
|
||||
|
||||
private static string getTitleShort(string title)
|
||||
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
||||
|
||||
private static string getLanguageShort(string language)
|
||||
{
|
||||
if (language is null)
|
||||
@@ -289,57 +286,50 @@ namespace LibationFileManager
|
||||
|
||||
public class FolderTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "Folder Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public static string Name { get; }= "Folder Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
public static IEnumerable<TagClass> TagClass => new TagClass[] { filePropertyTags, conditionalTags };
|
||||
public static IEnumerable<TagCollection> TagCollections => new TagCollection[] { filePropertyTags, conditionalTags };
|
||||
|
||||
public override IEnumerable<string> Errors
|
||||
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
|
||||
|
||||
protected override List<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||
{
|
||||
foreach (var tp in parts)
|
||||
{
|
||||
=> parts
|
||||
.Select(tp => tp.TemplateTag is null
|
||||
//FolderTemplate literals can have directory separator characters
|
||||
if (tp.TemplateTag is null)
|
||||
tp.Value = replacements.ReplacePathChars(tp.Value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
|
||||
else
|
||||
tp.Value = replacements.ReplaceFilenameChars(tp.Value);
|
||||
}
|
||||
return parts.Select(p => p.Value).ToList();
|
||||
}
|
||||
? replacements.ReplacePathChars(tp.Value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))
|
||||
: replacements.ReplaceFilenameChars(tp.Value)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
public class FileTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public static string Name { get; } = "File Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
public static IEnumerable<TagClass> TagClass { get; } = new TagClass[] { filePropertyTags, conditionalTags };
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags };
|
||||
}
|
||||
|
||||
public class ChapterFileTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "Chapter File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public static string Name { get; } = "Chapter File Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
public static IEnumerable<TagClass> TagClass { get; }
|
||||
= chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
|
||||
|
||||
public override IEnumerable<string> Warnings
|
||||
=> Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
|
||||
=> NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
|
||||
? base.Warnings
|
||||
: base.Warnings.Append(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
}
|
||||
|
||||
public class ChapterTitleTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "Chapter Title Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public static string Name { get; } = "Chapter Title Template";
|
||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||
public static IEnumerable<TagClass> TagClass { get; }
|
||||
= chapterPropertyTags.Append(conditionalTags);
|
||||
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags);
|
||||
|
||||
protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||
=> parts.Select(p => p.Value);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public interface ILogForm
|
||||
{
|
||||
24
Source/LibationUiBase/LibationUiBase.csproj
Normal file
24
Source/LibationUiBase/LibationUiBase.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,10 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
namespace LibationUiBase
|
||||
{
|
||||
// decouple serilog and form. include convenience factory method
|
||||
public class LogMe
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
namespace LibationAvalonia
|
||||
namespace LibationUiBase
|
||||
{
|
||||
internal class ObjectComparer<T> : IComparer where T : IComparable
|
||||
public class ObjectComparer<T> : IComparer where T : IComparable
|
||||
{
|
||||
public int Compare(object x, object y) => ((T)x).CompareTo(y);
|
||||
}
|
||||
@@ -37,9 +37,9 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
warningsLbl.Text = "";
|
||||
|
||||
this.Text = $"Edit {templateEditor.EditingTemplate.Name}";
|
||||
this.Text = $"Edit {templateEditor.TemplateName}";
|
||||
|
||||
this.templateLbl.Text = templateEditor.EditingTemplate.Description;
|
||||
this.templateLbl.Text = templateEditor.TemplateDescription;
|
||||
resetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
|
||||
// populate list view
|
||||
|
||||
@@ -5,6 +5,7 @@ using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.WindowsDesktop.Drawing;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -140,7 +141,16 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
// state validation
|
||||
if (e is null ||
|
||||
e.Definition.PictureId is null ||
|
||||
Book?.PictureId is null ||
|
||||
e.Picture is null ||
|
||||
e.Picture.Length == 0)
|
||||
return;
|
||||
|
||||
// logic validation
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
Cover = ImageReader.ToImage(e.Picture);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
|
||||
@@ -49,9 +49,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\LibationUiBase\LibationUiBase.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Form1.*.cs">
|
||||
@@ -64,16 +62,6 @@
|
||||
<DependentUpon>SettingsDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Form1.*.cs">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Form1.*.cs">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
internal class ObjectComparer<T> : IComparer where T : IComparable
|
||||
{
|
||||
public int Compare(object x, object y) => ((T)x).CompareTo(y);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.WindowsDesktop.Drawing;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
|
||||
@@ -33,20 +33,56 @@ namespace NamingTemplateTests
|
||||
public string Item2 { get; set; }
|
||||
public string Item3 { get; set; }
|
||||
public string Item4 { get; set; }
|
||||
public ReferenceType RefType { get; set; }
|
||||
public int? Int2 { get; set; }
|
||||
public bool Condition { get; set; }
|
||||
}
|
||||
class ReferenceType
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(ReferenceType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestClass]
|
||||
public class GetPortionFilename
|
||||
{
|
||||
PropertyTagClass<PropertyClass1> props1 = new();
|
||||
PropertyTagClass<PropertyClass2> props2 = new();
|
||||
PropertyTagClass<PropertyClass3> props3 = new();
|
||||
ConditionalTagClass<PropertyClass1> conditional1 = new();
|
||||
ConditionalTagClass<PropertyClass2> conditional2 = new();
|
||||
ConditionalTagClass<PropertyClass3> conditional3 = new();
|
||||
PropertyTagCollection<PropertyClass1> props1 = new()
|
||||
{
|
||||
{ new TemplateTag { TagName = "item1" }, i => i.Item1 },
|
||||
{ new TemplateTag { TagName = "item2" }, i => i.Item2 },
|
||||
{ new TemplateTag { TagName = "item3" }, i => i.Item3 }
|
||||
};
|
||||
|
||||
PropertyTagCollection<PropertyClass2> props2 = new()
|
||||
{
|
||||
{ new TemplateTag { TagName = "item1" }, i => i.Item1 },
|
||||
{ new TemplateTag { TagName = "item2" }, i => i.Item2 },
|
||||
{ new TemplateTag { TagName = "item3" }, i => i.Item3 },
|
||||
{ new TemplateTag { TagName = "item4" }, i => i.Item4 },
|
||||
};
|
||||
PropertyTagCollection<PropertyClass3> props3 = new(true, GetVal)
|
||||
{
|
||||
{ new TemplateTag { TagName = "item3_1" }, i => i.Item1 },
|
||||
{ new TemplateTag { TagName = "item3_2" }, i => i.Item2 },
|
||||
{ new TemplateTag { TagName = "item3_3" }, i => i.Item3 },
|
||||
{ new TemplateTag { TagName = "item3_4" }, i => i.Item4 },
|
||||
{ new TemplateTag { TagName = "reftype" }, i => i.RefType },
|
||||
};
|
||||
ConditionalTagCollection<PropertyClass1> conditional1 = new()
|
||||
{
|
||||
{ new TemplateTag { TagName = "ifc1" }, i => i.Condition },
|
||||
};
|
||||
ConditionalTagCollection<PropertyClass2> conditional2 = new()
|
||||
{
|
||||
{ new TemplateTag { TagName = "ifc2" }, i => i.Condition },
|
||||
};
|
||||
ConditionalTagCollection<PropertyClass3> conditional3 = new()
|
||||
{
|
||||
{ new TemplateTag { TagName = "ifc3" }, i => i.Condition },
|
||||
};
|
||||
|
||||
PropertyClass1 propertyClass1 = new()
|
||||
{
|
||||
@@ -74,27 +110,6 @@ namespace NamingTemplateTests
|
||||
Condition = true
|
||||
};
|
||||
|
||||
public GetPortionFilename()
|
||||
{
|
||||
props1.RegisterProperty(new TemplateTag { TagName = "item1" }, i => i.Item1);
|
||||
props1.RegisterProperty(new TemplateTag { TagName = "item2" }, i => i.Item2);
|
||||
props1.RegisterProperty(new TemplateTag { TagName = "item3" }, i => i.Item3);
|
||||
|
||||
props2.RegisterProperty(new TemplateTag { TagName = "item1" }, i => i.Item1);
|
||||
props2.RegisterProperty(new TemplateTag { TagName = "item2" }, i => i.Item2);
|
||||
props2.RegisterProperty(new TemplateTag { TagName = "item3" }, i => i.Item3);
|
||||
props2.RegisterProperty(new TemplateTag { TagName = "item4" }, i => i.Item4);
|
||||
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "item3_1" }, i => i.Item1);
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "item3_2" }, i => i.Item2);
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "item3_3" }, i => i.Item3);
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "item3_4" }, i => i.Item4);
|
||||
|
||||
conditional1.RegisterCondition(new TemplateTag { TagName = "ifc1" }, i => i.Condition);
|
||||
conditional2.RegisterCondition(new TemplateTag { TagName = "ifc2" }, i => i.Condition);
|
||||
conditional3.RegisterCondition(new TemplateTag { TagName = "ifc3" }, i => i.Condition);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<item1>", "prop1_item1", 1)]
|
||||
@@ -110,13 +125,13 @@ namespace NamingTemplateTests
|
||||
[DataRow("<!ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "prop1_item1prop2_item4prop3_item2", 3)]
|
||||
public void test(string inStr, string outStr, int numTags)
|
||||
{
|
||||
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
|
||||
template.TagsInUse.Should().HaveCount(numTags);
|
||||
template.Warnings.Should().HaveCount(numTags > 0 ? 0 : 1);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
}
|
||||
@@ -132,11 +147,17 @@ namespace NamingTemplateTests
|
||||
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc1><-ifc2>", new string[] { "Missing <-ifc3> closing conditional.", "Missing <-ifc3> closing conditional.", "Missing <-ifc1> closing conditional.", "Missing <-ifc2> closing conditional." })]
|
||||
public void condition_error(string inStr, string[] warnings)
|
||||
{
|
||||
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
|
||||
template.Errors.Should().HaveCount(0);
|
||||
template.Warnings.Should().BeEquivalentTo(warnings);
|
||||
}
|
||||
|
||||
static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<int1>", "55")]
|
||||
[DataRow("<int1[]>", "55")]
|
||||
@@ -152,19 +173,20 @@ namespace NamingTemplateTests
|
||||
[DataRow("<item2_2_null>", "")]
|
||||
[DataRow("<item2_2_null[]>", "")]
|
||||
[DataRow("<item2_2_null[l]>", "")]
|
||||
[DataRow("<reftype[l]>", "")]
|
||||
public void formatting(string inStr, string outStr)
|
||||
{
|
||||
props1.RegisterProperty(new TemplateTag { TagName = "int1" }, i => i.Int1, formatInt);
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "int2" }, i => i.Int2, formatInt);
|
||||
props3.RegisterProperty(new TemplateTag { TagName = "item3_format" }, i => i.Item3, formatString);
|
||||
props2.RegisterProperty(new TemplateTag { TagName = "item2_2_null" }, i => i.Item2, formatString);
|
||||
props1.Add(new TemplateTag { TagName = "int1" }, i => i.Int1, formatInt);
|
||||
props3.Add(new TemplateTag { TagName = "int2" }, i => i.Int2, formatInt);
|
||||
props3.Add(new TemplateTag { TagName = "item3_format" }, i => i.Item3, formatString);
|
||||
props2.Add(new TemplateTag { TagName = "item2_2_null" }, i => i.Item2, formatString);
|
||||
|
||||
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||
|
||||
template.Warnings.Should().HaveCount(0);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user