mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eaac6acc2 | ||
|
|
03b458765c | ||
|
|
c8b4bc6361 | ||
|
|
d9b5725ff1 | ||
|
|
0a0f60192b | ||
|
|
424d939c15 | ||
|
|
87f13ff8ed | ||
|
|
1e24df626a | ||
|
|
0312786721 | ||
|
|
1f8a5b256e | ||
|
|
426391f01c | ||
|
|
c296bff47f | ||
|
|
6b649cf4ca | ||
|
|
5103240a76 | ||
|
|
c2418b10f6 | ||
|
|
d705c23472 | ||
|
|
de45d008c7 | ||
|
|
c267332027 | ||
|
|
4829e85faf | ||
|
|
2acb9ca7e5 | ||
|
|
b260554a2a | ||
|
|
41a4055cd9 | ||
|
|
c6e9ba9bf9 | ||
|
|
5059333b38 | ||
|
|
b4015030cf | ||
|
|
7f5cf8f018 | ||
|
|
2c9ccd9c78 | ||
|
|
cebf218db4 | ||
|
|
530b44a0e6 | ||
|
|
b3dc5a7054 | ||
|
|
2567ccb44c | ||
|
|
e67eac92fd | ||
|
|
6e84fd97f1 | ||
|
|
9a458bf3dc | ||
|
|
283a46e1e2 | ||
|
|
6ff2859c39 | ||
|
|
e8df4952fc | ||
|
|
b19e1e8a30 | ||
|
|
a3cf6ac40d | ||
|
|
ab450c37c4 | ||
|
|
c837fefbdd | ||
|
|
46b120ee41 | ||
|
|
cae8ca7ef3 | ||
|
|
904665da7f | ||
|
|
2478c61df6 | ||
|
|
288ed75b5d | ||
|
|
ad5efbd9a9 | ||
|
|
7eb7b2a0f9 | ||
|
|
d0051c0f02 | ||
|
|
d20517063e | ||
|
|
bcca69a102 | ||
|
|
35f8c05106 | ||
|
|
a3d38e082d | ||
|
|
b2e956e70b | ||
|
|
e5119357b2 | ||
|
|
b42ff827d5 | ||
|
|
68da9779da | ||
|
|
8e358d8f04 | ||
|
|
0a986238bc | ||
|
|
d636ceed8e | ||
|
|
e4fc104afe | ||
|
|
87e3075fb3 |
@@ -1,12 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean" Version="0.1.9" />
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
|
||||
<PackageReference Include="AAXClean" Version="0.1.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,10 +12,13 @@ namespace AaxDecrypter
|
||||
{
|
||||
protected override StepSequence Steps { get; }
|
||||
|
||||
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
||||
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
|
||||
|
||||
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
|
||||
private List<string> multiPartFilePaths { get; } = new List<string>();
|
||||
|
||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat,
|
||||
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
|
||||
: base(outFileName, cacheDirectory, dlLic, outputFormat)
|
||||
{
|
||||
Steps = new StepSequence
|
||||
@@ -26,6 +29,7 @@ namespace AaxDecrypter
|
||||
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
|
||||
["Step 3: Cleanup"] = Step_Cleanup,
|
||||
};
|
||||
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -119,7 +123,15 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
|
||||
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
||||
{
|
||||
var fileName = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title);
|
||||
var fileName = multipartFileNameCallback(new()
|
||||
{
|
||||
OutputFileName = OutputFileName,
|
||||
PartsPosition = currentChapter,
|
||||
PartsTotal = splitChapters.Count,
|
||||
Title = newSplitCallback?.Chapter?.Title
|
||||
});
|
||||
fileName = FileUtility.GetValidFilename(fileName);
|
||||
|
||||
multiPartFilePaths.Add(fileName);
|
||||
|
||||
FileUtility.SaferDelete(fileName);
|
||||
|
||||
@@ -30,8 +30,7 @@ namespace AaxDecrypter
|
||||
protected abstract StepSequence Steps { get; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
|
||||
private string cacheDir { get; }
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.ChangeExtension(OutputFileName, ".json"));
|
||||
private string jsonDownloadState { get; }
|
||||
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
|
||||
|
||||
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
@@ -44,11 +43,12 @@ namespace AaxDecrypter
|
||||
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
|
||||
cacheDir = cacheDirectory;
|
||||
jsonDownloadState = Path.Combine(cacheDirectory, Path.ChangeExtension(OutputFileName, ".json"));
|
||||
|
||||
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
|
||||
// delete file after validation is complete
|
||||
FileUtility.SaferDelete(OutputFileName);
|
||||
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
}
|
||||
|
||||
public abstract void Cancel();
|
||||
|
||||
25
AaxDecrypter/MultiConvertFileProperties.cs
Normal file
25
AaxDecrypter/MultiConvertFileProperties.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FileManager;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class MultiConvertFileProperties
|
||||
{
|
||||
public string OutputFileName { get; set; }
|
||||
public int PartsPosition { get; set; }
|
||||
public int PartsTotal { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
||||
public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties)
|
||||
{
|
||||
var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " };
|
||||
fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,9 +193,6 @@ namespace AaxDecrypter
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
|
||||
if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null)
|
||||
throw new WebException($"Server at {Uri.Host} does not support Http ranges");
|
||||
|
||||
//Content length is the length of the range request, and it is only equal
|
||||
//to the complete file length if requesting Range: bytes=0-
|
||||
if (WritePosition == 0)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>6.2.7.1</Version>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>6.7.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,10 +3,10 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
@@ -50,15 +50,48 @@ namespace AppScaffolding
|
||||
public static void RunPostConfigMigrations(Configuration config)
|
||||
{
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
PopulateMissingConfigValues(config);
|
||||
|
||||
//
|
||||
// migrations go below here
|
||||
//
|
||||
|
||||
Migrations.migrate_to_v5_2_0__post_config(config);
|
||||
Migrations.migrate_to_v5_7_1(config);
|
||||
Migrations.migrate_to_v6_1_2(config);
|
||||
Migrations.migrate_to_v6_2_0(config);
|
||||
Migrations.migrate_to_v6_6_9(config);
|
||||
}
|
||||
|
||||
public static void PopulateMissingConfigValues(Configuration config)
|
||||
{
|
||||
config.InProgress ??= Configuration.WinTemp;
|
||||
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!config.Exists(nameof(config.DecryptToLossy)))
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||
config.ImportEpisodes = true;
|
||||
|
||||
if (!config.Exists(nameof(config.SplitFilesByChapter)))
|
||||
config.SplitFilesByChapter = false;
|
||||
|
||||
if (!config.Exists(nameof(config.FolderTemplate)))
|
||||
config.FolderTemplate = Templates.Folder.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.FileTemplate)))
|
||||
config.FileTemplate = Templates.File.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.ChapterFileTemplate)))
|
||||
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
|
||||
|
||||
if (!config.Exists(nameof(config.ShowImportedStats)))
|
||||
config.ShowImportedStats = true;
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Run after migration</summary>
|
||||
@@ -74,29 +107,12 @@ namespace AppScaffolding
|
||||
if (config.GetObject("Serilog") != null)
|
||||
return;
|
||||
|
||||
// "Serilog": {
|
||||
// "MinimumLevel": "Information"
|
||||
// "WriteTo": [
|
||||
// {
|
||||
// "Name": "Console"
|
||||
// },
|
||||
// {
|
||||
// "Name": "File",
|
||||
// "Args": {
|
||||
// "rollingInterval": "Day",
|
||||
// "outputTemplate": ...
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// "Using": [ "Dinah.Core" ],
|
||||
// "Enrich": [ "WithCaller" ]
|
||||
// }
|
||||
var serilogObj = new JObject
|
||||
{
|
||||
{ "MinimumLevel", "Information" },
|
||||
{ "WriteTo", new JArray
|
||||
{
|
||||
new JObject { {"Name", "Console" } },
|
||||
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
||||
new JObject
|
||||
{
|
||||
{ "Name", "File" },
|
||||
@@ -111,14 +127,16 @@ namespace AppScaffolding
|
||||
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] Begin Libation
|
||||
// - with class and method info: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
|
||||
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation
|
||||
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}" }
|
||||
// {Properties:j} needed for expanded exception logging
|
||||
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller" } },
|
||||
// better exception logging with: Serilog.Exceptions library -- WithExceptionDetails
|
||||
{ "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } },
|
||||
};
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
}
|
||||
@@ -133,9 +151,9 @@ namespace AppScaffolding
|
||||
// capture most Console.WriteLine() and write to serilog. See below tests for details.
|
||||
// Some dependencies print helpful info via Console.WriteLine. We'd like to log it.
|
||||
//
|
||||
// Serilog also writes to Console so this might be asking for trouble. ie: infinite loops.
|
||||
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
|
||||
// Empirical testing so far has shown no issues.
|
||||
// If Serilog also writes to Console, this might be asking for trouble. ie: infinite loops.
|
||||
// To use that way, SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
|
||||
// However, empirical testing so far has shown no issues.
|
||||
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
|
||||
|
||||
#region Console => Serilog tests
|
||||
@@ -211,10 +229,10 @@ namespace AppScaffolding
|
||||
|
||||
config.InProgress,
|
||||
|
||||
DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
|
||||
AudibleFileStorage.DownloadsInProgressDirectory,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
|
||||
|
||||
DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
|
||||
AudibleFileStorage.DecryptInProgressDirectory,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
|
||||
});
|
||||
}
|
||||
@@ -311,39 +329,53 @@ namespace AppScaffolding
|
||||
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
|
||||
_ => path
|
||||
};
|
||||
|
||||
public static void migrate_to_v5_2_0__post_config(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!config.Exists(nameof(config.DecryptToLossy)))
|
||||
config.DecryptToLossy = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
// add config.BadBook
|
||||
public static void migrate_to_v5_7_1(Configuration config)
|
||||
public static void migrate_to_v6_6_9(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
}
|
||||
var writeToPath = $"Serilog.WriteTo";
|
||||
|
||||
// add config.DownloadEpisodes , config.ImportEpisodes
|
||||
public static void migrate_to_v6_1_2(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||
config.DownloadEpisodes = true;
|
||||
// remove WriteTo[].Name == Console
|
||||
{
|
||||
if (UNSAFE_MigrationHelper.Settings_TryGetArrayLength(writeToPath, out var length1))
|
||||
{
|
||||
for (var i = length1 - 1; i >= 0; i--)
|
||||
{
|
||||
var exists = UNSAFE_MigrationHelper.Settings_TryGetFromJsonPath($"{writeToPath}[{i}].Name", out var value);
|
||||
|
||||
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||
config.ImportEpisodes = true;
|
||||
}
|
||||
if (exists && value == "Console")
|
||||
UNSAFE_MigrationHelper.Settings_RemoveFromArray(writeToPath, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add config.SplitFilesByChapter
|
||||
public static void migrate_to_v6_2_0(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.SplitFilesByChapter)))
|
||||
config.SplitFilesByChapter = false;
|
||||
// add Serilog.Exceptions -- WithExceptionDetails
|
||||
{
|
||||
// outputTemplate should contain "{Properties:j}"
|
||||
{
|
||||
// re-calculate. previous loop may have changed the length
|
||||
if (UNSAFE_MigrationHelper.Settings_TryGetArrayLength(writeToPath, out var length2))
|
||||
{
|
||||
var propertyName = "outputTemplate";
|
||||
for (var i = 0; i < length2; i++)
|
||||
{
|
||||
var jsonPath = $"{writeToPath}[{i}].Args";
|
||||
var exists = UNSAFE_MigrationHelper.Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out var value);
|
||||
|
||||
var newValue = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}";
|
||||
|
||||
if (exists && value != newValue)
|
||||
UNSAFE_MigrationHelper.Settings_SetWithJsonPath(jsonPath, propertyName, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serilog.Using must include "Serilog.Exceptions"
|
||||
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Using", "Serilog.Exceptions");
|
||||
|
||||
// Serilog.Enrich must include "WithExceptionDetails"
|
||||
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,108 @@ namespace AppScaffolding
|
||||
return success;
|
||||
}
|
||||
|
||||
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
|
||||
{
|
||||
JToken val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
return val?.Type == jTokenType;
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetFromJsonPath(string jsonPath, out string value)
|
||||
{
|
||||
JToken val = null;
|
||||
|
||||
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
|
||||
|
||||
if (val?.Type == JTokenType.String)
|
||||
{
|
||||
value = val.Value<string>();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Settings_SetWithJsonPath(string jsonPath, string propertyName, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetFromJsonPath($"{jsonPath}.{propertyName}", out _))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
var token = jObj.SelectToken(jsonPath);
|
||||
if (token is null
|
||||
|| token is not JObject o
|
||||
|| o[propertyName] is null)
|
||||
return;
|
||||
|
||||
var oldValue = token.Value<string>(propertyName);
|
||||
if (oldValue != newValue)
|
||||
token[propertyName] = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
public static bool Settings_TryGetArrayLength(string jsonPath, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return false;
|
||||
|
||||
JArray array = null;
|
||||
process_SettingsJson(jObj => array = (JArray)jObj.SelectToken(jsonPath));
|
||||
|
||||
length = array.Count;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Settings_AddToArray(string jsonPath, string newValue)
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
var array = (JArray)jObj.SelectToken(jsonPath);
|
||||
array.Add(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Do not add if already exists</summary>
|
||||
public static void Settings_AddUniqueToArray(string arrayPath, string newValue)
|
||||
{
|
||||
if (!Settings_TryGetArrayLength(arrayPath, out var qty))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < qty; i++)
|
||||
{
|
||||
var exists = Settings_TryGetFromJsonPath($"{arrayPath}[{i}]", out var value);
|
||||
if (exists && value == newValue)
|
||||
return;
|
||||
}
|
||||
|
||||
Settings_AddToArray(arrayPath, newValue);
|
||||
}
|
||||
|
||||
/// <summary>only remove if not exists</summary>
|
||||
public static void Settings_RemoveFromArray(string jsonPath, int position)
|
||||
{
|
||||
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
|
||||
return;
|
||||
|
||||
process_SettingsJson(jObj =>
|
||||
{
|
||||
var array = (JArray)jObj.SelectToken(jsonPath);
|
||||
if (position < array.Count)
|
||||
array.RemoveAt(position);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>only insert if not exists</summary>
|
||||
public static void Settings_Insert(string key, string value)
|
||||
=> process_SettingsJson(jObj => jObj.TryAdd(key, value));
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="27.1.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.4" />
|
||||
<PackageReference Include="CsvHelper" Version="27.2.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,10 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
@@ -167,8 +167,8 @@ namespace ApplicationServices
|
||||
{
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
|
||||
var libraryBookImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
var qtyChanges = context.SaveChanges();
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
|
||||
@@ -44,6 +44,9 @@ namespace ApplicationServices
|
||||
[Name("Length In Minutes")]
|
||||
public int LengthInMinutes { get; set; }
|
||||
|
||||
[Name("Description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Name("Publisher")]
|
||||
public string Publisher { get; set; }
|
||||
|
||||
@@ -111,6 +114,7 @@ namespace ApplicationServices
|
||||
AuthorNames = a.Book.AuthorNames,
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Description = a.Book.Description,
|
||||
Publisher = a.Book.Publisher,
|
||||
HasPdf = a.Book.HasPdf,
|
||||
SeriesNames = a.Book.SeriesNames,
|
||||
@@ -180,6 +184,7 @@ namespace ApplicationServices
|
||||
nameof (ExportDto.AuthorNames),
|
||||
nameof (ExportDto.NarratorNames),
|
||||
nameof (ExportDto.LengthInMinutes),
|
||||
nameof (ExportDto.Description),
|
||||
nameof (ExportDto.Publisher),
|
||||
nameof (ExportDto.HasPdf),
|
||||
nameof (ExportDto.SeriesNames),
|
||||
@@ -233,6 +238,7 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
|
||||
row.CreateCell(col++).SetCellValue(dto.Description);
|
||||
row.CreateCell(col++).SetCellValue(dto.Publisher);
|
||||
row.CreateCell(col++).SetCellValue(dto.HasPdf);
|
||||
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
|
||||
|
||||
@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class Account : IUpdatable
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
|
||||
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):
|
||||
@@ -3,7 +3,7 @@ using AudibleApi.Authorization;
|
||||
using Dinah.Core.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
|
||||
{
|
||||
@@ -8,7 +8,7 @@ using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||
public class ApiExtended
|
||||
@@ -127,11 +127,18 @@ namespace InternalUtilities
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//}
|
||||
#endif
|
||||
|
||||
Serilog.Log.Logger.Debug("Begin initial library scan");
|
||||
|
||||
if (!items.Any())
|
||||
items = await Api.GetAllLibraryItemsAsync(responseGroups);
|
||||
|
||||
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
|
||||
|
||||
await manageEpisodesAsync(items, importEpisodes);
|
||||
|
||||
Serilog.Log.Logger.Debug("Episode scan complete");
|
||||
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
@@ -166,8 +173,7 @@ namespace InternalUtilities
|
||||
|
||||
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
|
||||
|
||||
// remove episode parents. even if the following stuff fails, these will still be removed from the collection.
|
||||
// also must happen before processing children because children abuses this flag
|
||||
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
if (importEpisodes)
|
||||
@@ -192,6 +198,14 @@ namespace InternalUtilities
|
||||
{
|
||||
var children = await getEpisodeChildrenAsync(parent);
|
||||
|
||||
// actual individual episode, not the parent of a series.
|
||||
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
|
||||
if (!children.Any())
|
||||
{
|
||||
results.Add(parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
@@ -202,7 +216,8 @@ namespace InternalUtilities
|
||||
new Series
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
Sequence = parent.Relationships.Single(r => r.Asin == child.Asin).Sort.ToString(),
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public static class AudibleApiStorage
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace InternalUtilities
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public interface IValidator
|
||||
{
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.3.1.1" />
|
||||
<PackageReference Include="AudibleApi" Version="2.7.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
1
AudibleUtilities/_InternalsVisible.cs
Normal file
1
AudibleUtilities/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(AudibleUtilities) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -12,13 +12,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.7.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.0.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.11">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -185,6 +185,9 @@ namespace DataLayer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_seriesLink is null)
|
||||
return "";
|
||||
|
||||
// first: alphabetical by name
|
||||
var withNames = _seriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer.Utilities
|
||||
{
|
||||
public static class LocalDatabaseInfo
|
||||
{
|
||||
public static List<string> GetLocalDBInstances()
|
||||
{
|
||||
// Start the child process.
|
||||
using var p = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/C sqllocaldb info",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
|
||||
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
|
||||
? new List<string>()
|
||||
: output
|
||||
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -68,7 +68,7 @@ namespace DtoImporterService
|
||||
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
|
||||
var book = DbContext.Books.Local.FirstOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
|
||||
if (book is null)
|
||||
{
|
||||
book = createNewBook(item);
|
||||
@@ -94,7 +94,8 @@ namespace DtoImporterService
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var authors = item
|
||||
.Authors
|
||||
.Select(a => DbContext.Contributors.Local.Single(c => a.Name == c.Name))
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
.Select(a => DbContext.Contributors.Local.FirstOrDefault(c => a.Name == c.Name))
|
||||
.ToList();
|
||||
|
||||
var narrators
|
||||
@@ -104,7 +105,8 @@ namespace DtoImporterService
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
: item
|
||||
.Narrators
|
||||
.Select(n => DbContext.Contributors.Local.Single(c => n.Name == c.Name))
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
.Select(n => DbContext.Contributors.Local.FirstOrDefault(c => n.Name == c.Name))
|
||||
.ToList();
|
||||
|
||||
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
|
||||
@@ -118,24 +120,45 @@ namespace DtoImporterService
|
||||
// 2+
|
||||
: item.Categories[1].CategoryId;
|
||||
|
||||
var category = DbContext.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory);
|
||||
// This should properly be SingleOrDefault() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
var category = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == lastCategory);
|
||||
|
||||
var book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
category,
|
||||
importItem.LocaleName)
|
||||
).Entity;
|
||||
Book book;
|
||||
try
|
||||
{
|
||||
book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
category,
|
||||
importItem.LocaleName)
|
||||
).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding book. {@DebugInfo}", new {
|
||||
item.ProductId,
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
QtyAuthors = authors?.Count,
|
||||
QtyNarrators = narrators?.Count,
|
||||
Category = category?.Name,
|
||||
importItem.LocaleName
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = DbContext.Contributors.Local.Single(c => publisherName == c.Name);
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
var publisher = DbContext.Contributors.Local.FirstOrDefault(c => publisherName == c.Name);
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
@@ -152,7 +175,9 @@ namespace DtoImporterService
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
// set/update book-specific info which may have changed
|
||||
book.PictureId = item.PictureId;
|
||||
if (item.PictureId is not null)
|
||||
book.PictureId = item.PictureId;
|
||||
|
||||
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
|
||||
|
||||
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -73,7 +73,15 @@ namespace DtoImporterService
|
||||
var category = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == id);
|
||||
if (category is null)
|
||||
{
|
||||
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
|
||||
try
|
||||
{
|
||||
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
|
||||
throw;
|
||||
}
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -82,8 +82,18 @@ namespace DtoImporterService
|
||||
);
|
||||
foreach (var name in newPeople)
|
||||
{
|
||||
var p = groupby.Single(g => g.Name == name).People.First();
|
||||
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
var p = groupby.FirstOrDefault(g => g.Name == name).People.First();
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding person. {@DebugInfo}", new { p?.Name, p?.Asin });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return newPeople.Count;
|
||||
@@ -99,7 +109,17 @@ namespace DtoImporterService
|
||||
.ToList();
|
||||
|
||||
foreach (var pub in newPublishers)
|
||||
DbContext.Contributors.Add(new Contributor(pub));
|
||||
{
|
||||
try
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(pub));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding publisher. {@DebugInfo}", new { pub });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return newPublishers.Count;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -35,18 +35,39 @@ namespace DtoImporterService
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
|
||||
var newItems = importItems
|
||||
.Where(dto => !currentLibraryProductIds
|
||||
.Contains(dto.DtoItem.ProductId))
|
||||
.ToList();
|
||||
|
||||
foreach (var newItem in newItems)
|
||||
// if 2 accounts try to import the same book in the same transaction: error since we're only tracking and pulling by asin.
|
||||
// just use the first
|
||||
var groupby = newItems.GroupBy(
|
||||
i => i.DtoItem.ProductId,
|
||||
i => i,
|
||||
(key, g) => new { ProductId = key, ImportItems = g.ToList() }
|
||||
)
|
||||
.ToList();
|
||||
foreach (var gb in groupby)
|
||||
{
|
||||
var newItem = gb.ImportItems.First();
|
||||
|
||||
var libraryBook = new LibraryBook(
|
||||
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
|
||||
DbContext.Books.Local.FirstOrDefault(b => b.AudibleProductId == newItem.DtoItem.ProductId),
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId);
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
}
|
||||
}
|
||||
|
||||
var qtyNew = newItems.Count;
|
||||
var qtyNew = groupby.Count;
|
||||
return qtyNew;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApi.Common;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
@@ -51,7 +51,15 @@ namespace DtoImporterService
|
||||
var series = DbContext.Series.Local.FirstOrDefault(c => c.AudibleSeriesId == s.SeriesId);
|
||||
if (series is null)
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
try
|
||||
{
|
||||
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { s?.SeriesId });
|
||||
throw;
|
||||
}
|
||||
qtyNew++;
|
||||
}
|
||||
series.UpdateName(s.SeriesName);
|
||||
|
||||
56
FileLiberator/AudioFileStorageExt.cs
Normal file
56
FileLiberator/AudioFileStorageExt.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public static class AudioFileStorageExt
|
||||
{
|
||||
private class MultipartRenamer
|
||||
{
|
||||
private LibraryBook libraryBook { get; }
|
||||
|
||||
internal MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
|
||||
|
||||
internal string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(libraryBook.ToDto(), props);
|
||||
}
|
||||
|
||||
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
=> new MultipartRenamer(libraryBook).MultipartFilename;
|
||||
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// File path for where to move files into.
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
=> Templates.Folder.GetFilename(libraryBook.ToDto());
|
||||
|
||||
/// <summary>
|
||||
/// DownloadDecryptBook:
|
||||
/// Path: in progress directory.
|
||||
/// File name: final file name.
|
||||
/// </summary>
|
||||
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension);
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file does not exist
|
||||
/// </summary>
|
||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
|
||||
|
||||
/// <summary>
|
||||
/// PDF: audio file already exists
|
||||
/// </summary>
|
||||
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
|
||||
=> Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension);
|
||||
}
|
||||
}
|
||||
@@ -59,13 +59,12 @@ namespace FileLiberator
|
||||
FilePathCache.Removed -= FilePathCache_Removed;
|
||||
}
|
||||
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
// moves new files from temp dir to final dest
|
||||
var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries);
|
||||
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
|
||||
|
||||
// decrypt failed
|
||||
if (!movedAudioFile)
|
||||
@@ -115,14 +114,17 @@ namespace FileLiberator
|
||||
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
||||
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
||||
}
|
||||
|
||||
var outFileName = FileUtility.GetValidFilename(AudibleFileStorage.DecryptInProgressDirectory, libraryBook.Book.Title, outputFormat.ToString().ToLower(), libraryBook.Book.AudibleProductId);
|
||||
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, outputFormat.ToString().ToLower());
|
||||
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
|
||||
abDownloader
|
||||
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
|
||||
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(outFileName, cacheDir, audiobookDlLic, outputFormat)
|
||||
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
|
||||
outFileName, cacheDir, audiobookDlLic, outputFormat,
|
||||
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)
|
||||
)
|
||||
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
|
||||
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
|
||||
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
|
||||
@@ -134,7 +136,6 @@ namespace FileLiberator
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(abDownloader.Run);
|
||||
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
@@ -175,43 +176,35 @@ namespace FileLiberator
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
|
||||
private static bool moveFilesToBooksDir(Book book, List<FilePathCache.CacheEntry> entries)
|
||||
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var title = book.Title;
|
||||
var asin = book.AudibleProductId;
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? title
|
||||
: title.Substring(0, underscoreIndex);
|
||||
var destinationDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, asin);
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
if (getFirstAudio() == default)
|
||||
return false;
|
||||
if (getFirstAudio() == default)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
|
||||
FilePathCache.Insert(book.AudibleProductId, realDest);
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
||||
|
||||
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ namespace FileLiberator
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
var extension = Path.GetExtension(getdownloadUrl(libraryBook));
|
||||
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
if (existingPath is not null)
|
||||
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, existingPath, extension);
|
||||
|
||||
if (existingPath != null)
|
||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||
|
||||
return FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, libraryBook.Book.Title, Path.GetExtension(file), libraryBook.Book.AudibleProductId);
|
||||
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
|
||||
}
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
@@ -19,8 +20,24 @@ namespace FileLiberator
|
||||
|
||||
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||
{
|
||||
var apiExtended = await InternalUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
return apiExtended.Api;
|
||||
}
|
||||
|
||||
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
|
||||
{
|
||||
Account = libraryBook.Account,
|
||||
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title ?? "",
|
||||
Locale = libraryBook.Book.Locale,
|
||||
|
||||
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
|
||||
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
|
||||
|
||||
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1
FileLiberator/_InternalsVisible.cs
Normal file
1
FileLiberator/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileLiberator) + ".Tests")]
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
<PackageReference Include="Dinah.Core" Version="4.0.4.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
||||
public class FileTemplate
|
||||
public class FileNamingTemplate
|
||||
{
|
||||
/// <summary>Proposed full file path. May contain optional html-styled template tags. Eg: <name></summary>
|
||||
public string Template { get; }
|
||||
|
||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
public FileNamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
|
||||
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
||||
public Dictionary<string, string> ParameterReplacements { get; } = new Dictionary<string, string>();
|
||||
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>Convenience method</summary>
|
||||
public void AddParameterReplacement(string key ,string value) => ParameterReplacements.Add(key, value);
|
||||
public void AddParameterReplacement(string key, object value)
|
||||
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
|
||||
=> ParameterReplacements.Add(key, value);
|
||||
|
||||
/// <summary>If set, truncate each parameter replacement to this many characters. Default 50</summary>
|
||||
public int? ParameterMaxSize { get; set; } = 50;
|
||||
@@ -28,7 +29,7 @@ namespace FileManager
|
||||
public string IllegalCharacterReplacements { get; set; }
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
public string GetFilename()
|
||||
public string GetFilePath()
|
||||
{
|
||||
var filename = Template;
|
||||
|
||||
@@ -38,14 +39,26 @@ namespace FileManager
|
||||
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
|
||||
}
|
||||
|
||||
private string formatKey(string key)
|
||||
private static string formatKey(string key)
|
||||
=> key
|
||||
.Replace("<", "")
|
||||
.Replace(">", "");
|
||||
|
||||
private string formatValue(string value)
|
||||
=> ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
|
||||
? value?.Truncate(ParameterMaxSize.Value)
|
||||
: value;
|
||||
private string formatValue(object value)
|
||||
{
|
||||
if (value is null)
|
||||
return "";
|
||||
|
||||
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
||||
// Esp important for file templates.
|
||||
var val = value
|
||||
.ToString()
|
||||
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
|
||||
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
|
||||
return
|
||||
ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
|
||||
? val.Truncate(ParameterMaxSize.Value)
|
||||
: val;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
@@ -18,43 +19,24 @@ namespace FileManager
|
||||
public static string GetStandardizedExtension(string extension)
|
||||
=> string.IsNullOrWhiteSpace(extension)
|
||||
? (extension ?? "")?.Trim()
|
||||
: '.' + extension.Trim('.');
|
||||
: '.' + extension.Trim().Trim('.');
|
||||
|
||||
public static string GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
/// <summary>
|
||||
/// Return position with correct number of leading zeros.
|
||||
/// <br />- 2 of 9 => "2"
|
||||
/// <br />- 2 of 90 => "02"
|
||||
/// <br />- 2 of 900 => "002"
|
||||
/// </summary>
|
||||
/// <param name="position">position in sequence. The 'x' in 'x of y'</param>
|
||||
/// <param name="total">total qty in sequence. The 'y' in 'x of y'</param>
|
||||
public static string GetSequenceFormatted(int position, int total)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(filename, nameof(filename));
|
||||
ArgumentValidator.EnsureGreaterThan(position, nameof(position), 0);
|
||||
ArgumentValidator.EnsureGreaterThan(total, nameof(total), 0);
|
||||
if (position > total)
|
||||
throw new ArgumentException($"{position} may not be greater than {total}");
|
||||
|
||||
var template = $"<title> [<id>]";
|
||||
|
||||
var fullfilename = Path.Combine(dirFullPath, template + GetStandardizedExtension(extension));
|
||||
|
||||
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
fileTemplate.AddParameterReplacement("title", filename);
|
||||
fileTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||
return fileTemplate.GetFilename();
|
||||
}
|
||||
|
||||
public static string GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(originalPath, nameof(originalPath));
|
||||
ArgumentValidator.EnsureGreaterThan(partsPosition, nameof(partsPosition), 0);
|
||||
ArgumentValidator.EnsureGreaterThan(partsTotal, nameof(partsTotal), 0);
|
||||
if (partsPosition > partsTotal)
|
||||
throw new ArgumentException($"{partsPosition} may not be greater than {partsTotal}");
|
||||
|
||||
// 1-9 => 1-9
|
||||
// 10-99 => 01-99
|
||||
// 100-999 => 001-999
|
||||
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
|
||||
|
||||
var template = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
|
||||
|
||||
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
|
||||
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileTemplate.AddParameterReplacement("title", suffix);
|
||||
|
||||
return fileTemplate.GetFilename();
|
||||
return position.ToString().PadLeft(total.ToString().Length, '0');
|
||||
}
|
||||
|
||||
private const int MAX_FILENAME_LENGTH = 255;
|
||||
@@ -84,6 +66,8 @@ namespace FileManager
|
||||
|
||||
var fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - extension.Length) + extension;
|
||||
|
||||
fullfilename = removeInvalidWhitespace(fullfilename);
|
||||
|
||||
var i = 0;
|
||||
while (File.Exists(fullfilename))
|
||||
{
|
||||
@@ -105,24 +89,72 @@ namespace FileManager
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||
|
||||
var fixedPath = string
|
||||
.Join(illegalCharacterReplacements ?? "", path.Split(Path.GetInvalidPathChars()))
|
||||
.Replace("*", illegalCharacterReplacements)
|
||||
.Replace("?", illegalCharacterReplacements)
|
||||
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
path = replaceInvalidChars(path, illegalCharacterReplacements);
|
||||
path = standardizeSlashes(path);
|
||||
path = replaceColons(path, illegalCharacterReplacements);
|
||||
path = removeDoubleSlashes(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
|
||||
'*', '?',
|
||||
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
|
||||
// In live code, Path.GetInvalidPathChars() does not include them
|
||||
'"', '<', '>'
|
||||
}).ToArray();
|
||||
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
|
||||
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
|
||||
|
||||
private static string standardizeSlashes(string path)
|
||||
=> path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
private static string replaceColons(string path, string illegalCharacterReplacements)
|
||||
{
|
||||
// replace all colons except within the first 2 chars
|
||||
var builder = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < fixedPath.Length; i++)
|
||||
for (var i = 0; i < path.Length; i++)
|
||||
{
|
||||
var c = fixedPath[i];
|
||||
var c = path[i];
|
||||
if (i >= 2 && c == ':')
|
||||
builder.Append(illegalCharacterReplacements);
|
||||
else
|
||||
builder.Append(c);
|
||||
}
|
||||
fixedPath = builder.ToString();
|
||||
return fixedPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string removeDoubleSlashes(string path)
|
||||
{
|
||||
if (path.Length < 2)
|
||||
return path;
|
||||
|
||||
// exception: don't try to condense the initial dbl bk slashes in a path. eg: \\192.168.0.1
|
||||
|
||||
var remainder = path[1..];
|
||||
var dblSeparator = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
||||
while (remainder.Contains(dblSeparator))
|
||||
remainder = remainder.Replace(dblSeparator, $"{Path.DirectorySeparatorChar}");
|
||||
|
||||
return path[0] + remainder;
|
||||
}
|
||||
|
||||
private static string removeInvalidWhitespace_pattern { get; } = $@"\s*\{Path.DirectorySeparatorChar}\s*";
|
||||
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
/// <summary>no part of the path may begin or end in whitespace</summary>
|
||||
private static string removeInvalidWhitespace(string fullfilename)
|
||||
{
|
||||
// no whitespace at beginning or end
|
||||
// replace whitespace around path slashes
|
||||
// regex (with space added for clarity)
|
||||
// \s* \\ \s* => \
|
||||
fullfilename = fullfilename.Trim();
|
||||
|
||||
fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, @"\");
|
||||
|
||||
fullfilename = removeDoubleSlashes(fullfilename);
|
||||
return fullfilename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,8 +30,7 @@ namespace FileManager
|
||||
if (IsReadOnly)
|
||||
return;
|
||||
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
createNewFile();
|
||||
}
|
||||
|
||||
public string GetString(string propertyName)
|
||||
@@ -205,9 +204,39 @@ namespace FileManager
|
||||
|
||||
private JObject readFile()
|
||||
{
|
||||
if (!File.Exists(Filepath))
|
||||
{
|
||||
var msg = "Unrecoverable error. Settings file cannot be found";
|
||||
var ex = new FileNotFoundException(msg, Filepath);
|
||||
Serilog.Log.Logger.Error(ex, msg);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
var settingsJsonContents = File.ReadAllText(Filepath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settingsJsonContents))
|
||||
{
|
||||
createNewFile();
|
||||
settingsJsonContents = File.ReadAllText(Filepath);
|
||||
}
|
||||
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(settingsJsonContents);
|
||||
|
||||
if (jObject is null)
|
||||
{
|
||||
var msg = "Unrecoverable error. Unable to read settings from Settings file";
|
||||
var ex = new NullReferenceException(msg);
|
||||
Serilog.Log.Logger.Error(ex, msg);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return jObject;
|
||||
}
|
||||
|
||||
private void createNewFile()
|
||||
{
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
FileManager/_InternalsVisible.cs
Normal file
1
FileManager/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileManager) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
48
Libation.sln
48
Libation.sln
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28803.156
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\Data
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities", "AudibleUtilities\AudibleUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
|
||||
EndProject
|
||||
@@ -46,8 +46,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "Appl
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
||||
@@ -56,9 +54,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileManager.Tests", "FileManager.Tests\FileManager.Tests.csproj", "{3B58450C-FBDA-4D48-8418-A3C750596D7D}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities.Tests", "_Tests\AudibleUtilities.Tests\AudibleUtilities.Tests.csproj", "{788294BE-0D8E-40D4-9CEE-67896FBB52CE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tests\FileLiberator.Tests\FileLiberator.Tests.csproj", "{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests\FileManager.Tests\FileManager.Tests.csproj", "{F2E04270-4551-41C4-99FF-E7125BED708C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -102,10 +106,6 @@ Global
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -126,10 +126,22 @@ Global
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -144,13 +156,15 @@ Global
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{3B58450C-FBDA-4D48-8418-A3C750596D7D} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace LibationFileManager
|
||||
{
|
||||
// primary lookup
|
||||
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
if (cachedFile is not null && File.Exists(cachedFile))
|
||||
return cachedFile;
|
||||
|
||||
// secondary lookup attempt
|
||||
@@ -66,8 +66,26 @@ namespace LibationFileManager
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
|
||||
protected override string GetFilePathCustom(string productId)
|
||||
{
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return Directory
|
||||
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
internal AudioFileStorage() : base(FileType.Audio)
|
||||
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
private static object bookDirectoryFilesLocker { get; } = new();
|
||||
protected override string GetFilePathCustom(string productId)
|
||||
@@ -81,26 +99,8 @@ namespace LibationFileManager
|
||||
return BookDirectoryFiles.FindFile(regex);
|
||||
}
|
||||
|
||||
internal AudioFileStorage() : base(FileType.Audio)
|
||||
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
public string GetPath(string productId) => GetFilePath(productId);
|
||||
}
|
||||
|
||||
internal class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
protected override string GetFilePathCustom(string productId)
|
||||
{
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return Directory
|
||||
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => regex.IsMatch(s));
|
||||
}
|
||||
|
||||
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
|
||||
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,13 @@ namespace LibationFileManager
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
@@ -145,8 +152,40 @@ namespace LibationFileManager
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
persistentDictionary.SetString(settingName, template);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region known directories
|
||||
@@ -231,7 +270,7 @@ namespace LibationFileManager
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
Log.Logger.Information("LogLevel.set attempt. No change");
|
||||
Log.Logger.Debug("LogLevel.set attempt. No change");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,18 @@ namespace LibationFileManager
|
||||
if (File.Exists(jsonFile))
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
|
||||
|
||||
// file exists but deser is null. this will never happen when file is healthy
|
||||
if (list is null)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
|
||||
File.Delete(jsonFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
29
LibationFileManager/LibraryBookDto.cs
Normal file
29
LibationFileManager/LibraryBookDto.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class BookDto
|
||||
{
|
||||
public string AudibleProductId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Locale { get; set; }
|
||||
|
||||
public IEnumerable<string> Authors { get; set; }
|
||||
public string AuthorNames => string.Join(", ", Authors);
|
||||
public string FirstAuthor => Authors.FirstOrDefault();
|
||||
|
||||
public IEnumerable<string> Narrators { get; set; }
|
||||
public string NarratorNames => string.Join(", ", Narrators);
|
||||
public string FirstNarrator => Narrators.FirstOrDefault();
|
||||
|
||||
public string SeriesName { get; set; }
|
||||
public string SeriesNumber { get; set; }
|
||||
}
|
||||
|
||||
public class LibraryBookDto : BookDto
|
||||
{
|
||||
public string Account { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -73,16 +73,10 @@ namespace LibationFileManager
|
||||
if (!cache.ContainsKey(def) || cache[def] == null)
|
||||
{
|
||||
var path = getPath(def);
|
||||
byte[] bytes;
|
||||
|
||||
if (File.Exists(path))
|
||||
bytes = File.ReadAllBytes(path);
|
||||
else
|
||||
{
|
||||
bytes = downloadBytes(def);
|
||||
saveFile(def, bytes);
|
||||
}
|
||||
|
||||
var bytes
|
||||
= File.Exists(path)
|
||||
? File.ReadAllBytes(path)
|
||||
: downloadBytes(def);
|
||||
cache[def] = bytes;
|
||||
}
|
||||
return cache[def];
|
||||
@@ -104,7 +98,6 @@ namespace LibationFileManager
|
||||
continue;
|
||||
|
||||
var bytes = downloadBytes(def);
|
||||
saveFile(def, bytes);
|
||||
lock (cacheLocker)
|
||||
cache[def] = bytes;
|
||||
|
||||
@@ -115,14 +108,24 @@ namespace LibationFileManager
|
||||
private static HttpClient imageDownloadClient { get; } = new HttpClient();
|
||||
private static byte[] downloadBytes(PictureDefinition def)
|
||||
{
|
||||
var sz = (int)def.Size;
|
||||
return imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
|
||||
}
|
||||
if (def.PictureId is null)
|
||||
return getDefaultImage(def.Size);
|
||||
|
||||
private static void saveFile(PictureDefinition def, byte[] bytes)
|
||||
{
|
||||
var path = getPath(def);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
try
|
||||
{
|
||||
var sz = (int)def.Size;
|
||||
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}._SL{sz}_.jpg").Result;
|
||||
|
||||
// save image file. make sure to not save default image
|
||||
var path = getPath(def);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return getDefaultImage(def.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,6 @@ namespace LibationFileManager
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;";
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
|
||||
}
|
||||
}
|
||||
|
||||
44
LibationFileManager/TemplateTags.cs
Normal file
44
LibationFileManager/TemplateTags.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : Enumeration<TemplateTags>
|
||||
{
|
||||
public string TagName => DisplayName;
|
||||
public string Description { get; }
|
||||
public bool IsChapterOnly { get; }
|
||||
|
||||
private static int value = 0;
|
||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName)
|
||||
{
|
||||
Description = description;
|
||||
IsChapterOnly = isChapterOnly;
|
||||
}
|
||||
|
||||
// putting these first is the incredibly lazy way to make them show up first in the EditTemplateDialog
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title", true);
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #", true);
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros", true);
|
||||
|
||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
||||
public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
|
||||
public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
|
||||
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
|
||||
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
|
||||
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
|
||||
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
|
||||
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
|
||||
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
|
||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||
public static TemplateTags Locale { get; } = new TemplateTags("locale", "Region/country");
|
||||
|
||||
// Special case. Isn't mapped to a replacement in Templates.cs
|
||||
// Included here for display by EditTemplateDialog
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series");
|
||||
}
|
||||
}
|
||||
284
LibationFileManager/Templates.cs
Normal file
284
LibationFileManager/Templates.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public abstract class Templates
|
||||
{
|
||||
protected static string[] Valid => Array.Empty<string>();
|
||||
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||
public const string ERROR_INVALID_FILE_NAME_CHAR = @"Only file name friendly characters allowed. Eg: no colons or slashes";
|
||||
|
||||
public const string WARNING_EMPTY = "Template is empty.";
|
||||
public const string WARNING_WHITE_SPACE = "Template is white space.";
|
||||
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
||||
public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>";
|
||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||
|
||||
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
||||
public static FileTemplate File { get; } = new FileTemplate();
|
||||
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
||||
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract string DefaultTemplate { get; }
|
||||
protected abstract bool IsChapterized { get; }
|
||||
|
||||
protected Templates() { }
|
||||
|
||||
#region validation
|
||||
internal string GetValid(string configValue)
|
||||
{
|
||||
var value = configValue?.Trim();
|
||||
return IsValid(value) ? value : DefaultTemplate;
|
||||
}
|
||||
|
||||
public abstract IEnumerable<string> GetErrors(string template);
|
||||
public bool IsValid(string template) => !GetErrors(template).Any();
|
||||
|
||||
public abstract IEnumerable<string> GetWarnings(string template);
|
||||
public bool HasWarnings(string template) => GetWarnings(template).Any();
|
||||
|
||||
protected static string[] GetFileErrors(string template)
|
||||
{
|
||||
// File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
if (template.Contains(':')
|
||||
|| template.Contains(Path.DirectorySeparatorChar)
|
||||
|| template.Contains(Path.AltDirectorySeparatorChar)
|
||||
)
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
protected IEnumerable<string> GetStandardWarnings(string template)
|
||||
{
|
||||
var warnings = GetErrors(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
if (string.IsNullOrEmpty(template))
|
||||
warnings.Add(WARNING_EMPTY);
|
||||
else if (string.IsNullOrWhiteSpace(template))
|
||||
warnings.Add(WARNING_WHITE_SPACE);
|
||||
|
||||
if (TagCount(template) == 0)
|
||||
warnings.Add(WARNING_NO_TAGS);
|
||||
|
||||
if (!IsChapterized && ContainsChapterOnlyTags(template))
|
||||
warnings.Add(WARNING_HAS_CHAPTER_TAGS);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
internal int TagCount(string template)
|
||||
=> GetTemplateTags()
|
||||
// for <id><id> == 1, use:
|
||||
// .Count(t => template.Contains($"<{t.TagName}>"))
|
||||
// .Sum() impl: <id><id> == 2
|
||||
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
|
||||
|
||||
internal static bool ContainsChapterOnlyTags(string template)
|
||||
=> TemplateTags.GetAll()
|
||||
.Where(t => t.IsChapterOnly)
|
||||
.Any(t => ContainsTag(template, t.TagName));
|
||||
|
||||
internal static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>
|
||||
/// EditTemplateDialog: Get template generated filename for portion of path
|
||||
/// </summary>
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template)
|
||||
=> string.IsNullOrWhiteSpace(template)
|
||||
? ""
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
||||
.GetFilePath();
|
||||
|
||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
dirFullPath = dirFullPath?.Trim() ?? "";
|
||||
|
||||
// for non-series, remove <if series-> and <-if series> tags and everything in between
|
||||
// for series, remove <if series-> and <-if series> tags, what's in between will remain
|
||||
template = ifSeriesRegex.Replace(
|
||||
template,
|
||||
string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1");
|
||||
|
||||
var t = template + FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Id, libraryBookDto.AudibleProductId);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Author, libraryBookDto.AuthorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBookDto.FirstAuthor);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBookDto.NarratorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBookDto.FirstNarrator);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, libraryBookDto.SeriesNumber);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Account, libraryBookDto.Account);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
|
||||
|
||||
return fileNamingTemplate;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public IEnumerable<TemplateTags> GetTemplateTags()
|
||||
=> TemplateTags.GetAll()
|
||||
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
||||
.Where(t => IsChapterized || !t.IsChapterOnly);
|
||||
|
||||
public string Sanitize(string template)
|
||||
{
|
||||
var value = template ?? "";
|
||||
|
||||
// don't use alt slash
|
||||
value = value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
// don't allow double slashes
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
||||
while (value.Contains(dbl))
|
||||
value = value.Replace(dbl, sing);
|
||||
|
||||
// trim. don't start or end with slash
|
||||
while (true)
|
||||
{
|
||||
var start = value.Length;
|
||||
value = value
|
||||
.Trim()
|
||||
.Trim(Path.DirectorySeparatorChar);
|
||||
var end = value.Length;
|
||||
if (start == end)
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public class FolderTemplate : Templates
|
||||
{
|
||||
public override string Name => "Folder Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
|
||||
internal FolderTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template)
|
||||
{
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
if (template.Contains(':'))
|
||||
return new[] { ERROR_FULL_PATH_IS_INVALID };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, AudibleFileStorage.BooksDirectory, null)
|
||||
.GetFilePath();
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class FileTemplate : Templates
|
||||
{
|
||||
public override string Name => "File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
|
||||
internal FileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
|
||||
.GetFilePath();
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class ChapterFileTemplate : Templates
|
||||
{
|
||||
public override string Name => "Chapter File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
protected override bool IsChapterized { get; } = true;
|
||||
|
||||
internal ChapterFileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template)
|
||||
{
|
||||
var warnings = GetStandardWarnings(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
// recommended to incl. <ch#> or <ch# 0>
|
||||
if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName))
|
||||
warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
|
||||
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath)
|
||||
{
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
13
LibationFileManager/UtilityExtensions.cs
Normal file
13
LibationFileManager/UtilityExtensions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static void AddParameterReplacement(this FileNamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
||||
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
||||
}
|
||||
}
|
||||
1
LibationFileManager/_InternalsVisible.cs
Normal file
1
LibationFileManager/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationFileManager) + ".Tests")]
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
1
LibationSearchEngine/_InternalsVisible.cs
Normal file
1
LibationSearchEngine/_InternalsVisible.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationSearchEngine) + ".Tests")]
|
||||
@@ -103,6 +103,15 @@ namespace LibationWinForms.BookLiberation
|
||||
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
|
||||
private void updateRemainingTime(int remaining)
|
||||
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
|
||||
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
|
||||
|
||||
private string formatTime(int seconds)
|
||||
{
|
||||
var timeSpan = new TimeSpan(0, 0, seconds);
|
||||
return
|
||||
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
|
||||
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
|
||||
: $"{seconds} sec";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -112,6 +112,9 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!inputIsValid())
|
||||
return;
|
||||
|
||||
// without transaction, accounts persister will write ANY EDIT immediately to file
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
@@ -129,6 +132,28 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private bool inputIsValid()
|
||||
{
|
||||
var dtos = getRowDtos();
|
||||
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
{
|
||||
MessageBox.Show("Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
{
|
||||
MessageBox.Show("Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void persist(AccountsSettings accountsSettings)
|
||||
{
|
||||
var existingAccounts = accountsSettings.Accounts;
|
||||
@@ -152,11 +177,6 @@ namespace LibationWinForms.Dialogs
|
||||
// upsert each. validation occurs through Account and AccountsSettings
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
throw new Exception("Please enter an account id for all accounts");
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
throw new Exception("Please select a locale (i.e.: country or region) for all accounts");
|
||||
|
||||
var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName);
|
||||
acct.LibraryScan = dto.LibraryScan;
|
||||
acct.AccountName
|
||||
|
||||
197
LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs
generated
Normal file
197
LibationWinForms/Dialogs/EditTemplateDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,197 @@
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class EditTemplateDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.templateTb = new System.Windows.Forms.TextBox();
|
||||
this.templateLbl = new System.Windows.Forms.Label();
|
||||
this.resetToDefaultBtn = new System.Windows.Forms.Button();
|
||||
this.listView1 = new System.Windows.Forms.ListView();
|
||||
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
|
||||
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
|
||||
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
|
||||
this.warningsLbl = new System.Windows.Forms.Label();
|
||||
this.exampleLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 345);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 345);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// templateTb
|
||||
//
|
||||
this.templateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.templateTb.Location = new System.Drawing.Point(12, 27);
|
||||
this.templateTb.Name = "templateTb";
|
||||
this.templateTb.Size = new System.Drawing.Size(779, 23);
|
||||
this.templateTb.TabIndex = 1;
|
||||
this.templateTb.TextChanged += new System.EventHandler(this.templateTb_TextChanged);
|
||||
//
|
||||
// templateLbl
|
||||
//
|
||||
this.templateLbl.AutoSize = true;
|
||||
this.templateLbl.Location = new System.Drawing.Point(12, 9);
|
||||
this.templateLbl.Name = "templateLbl";
|
||||
this.templateLbl.Size = new System.Drawing.Size(89, 15);
|
||||
this.templateLbl.TabIndex = 0;
|
||||
this.templateLbl.Text = "[template desc]";
|
||||
//
|
||||
// resetToDefaultBtn
|
||||
//
|
||||
this.resetToDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
|
||||
this.resetToDefaultBtn.Name = "resetToDefaultBtn";
|
||||
this.resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
|
||||
this.resetToDefaultBtn.TabIndex = 2;
|
||||
this.resetToDefaultBtn.Text = "Reset to default";
|
||||
this.resetToDefaultBtn.UseVisualStyleBackColor = true;
|
||||
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click);
|
||||
//
|
||||
// listView1
|
||||
//
|
||||
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
this.columnHeader2});
|
||||
this.listView1.HideSelection = false;
|
||||
this.listView1.Location = new System.Drawing.Point(12, 56);
|
||||
this.listView1.Name = "listView1";
|
||||
this.listView1.Size = new System.Drawing.Size(328, 283);
|
||||
this.listView1.TabIndex = 3;
|
||||
this.listView1.UseCompatibleStateImageBehavior = false;
|
||||
this.listView1.View = System.Windows.Forms.View.Details;
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
this.columnHeader1.Text = "Tag";
|
||||
this.columnHeader1.Width = 137;
|
||||
//
|
||||
// columnHeader2
|
||||
//
|
||||
this.columnHeader2.Text = "Description";
|
||||
this.columnHeader2.Width = 170;
|
||||
//
|
||||
// richTextBox1
|
||||
//
|
||||
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.richTextBox1.Location = new System.Drawing.Point(346, 74);
|
||||
this.richTextBox1.Name = "richTextBox1";
|
||||
this.richTextBox1.ReadOnly = true;
|
||||
this.richTextBox1.Size = new System.Drawing.Size(574, 185);
|
||||
this.richTextBox1.TabIndex = 5;
|
||||
this.richTextBox1.Text = "";
|
||||
//
|
||||
// warningsLbl
|
||||
//
|
||||
this.warningsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
|
||||
this.warningsLbl.Location = new System.Drawing.Point(346, 262);
|
||||
this.warningsLbl.Name = "warningsLbl";
|
||||
this.warningsLbl.Size = new System.Drawing.Size(574, 77);
|
||||
this.warningsLbl.TabIndex = 6;
|
||||
this.warningsLbl.Text = "[warnings]";
|
||||
//
|
||||
// exampleLbl
|
||||
//
|
||||
this.exampleLbl.AutoSize = true;
|
||||
this.exampleLbl.Location = new System.Drawing.Point(346, 56);
|
||||
this.exampleLbl.Name = "exampleLbl";
|
||||
this.exampleLbl.Size = new System.Drawing.Size(55, 15);
|
||||
this.exampleLbl.TabIndex = 4;
|
||||
this.exampleLbl.Text = "Example:";
|
||||
//
|
||||
// EditTemplateDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 388);
|
||||
this.Controls.Add(this.exampleLbl);
|
||||
this.Controls.Add(this.warningsLbl);
|
||||
this.Controls.Add(this.richTextBox1);
|
||||
this.Controls.Add(this.listView1);
|
||||
this.Controls.Add(this.resetToDefaultBtn);
|
||||
this.Controls.Add(this.templateLbl);
|
||||
this.Controls.Add(this.templateTb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "EditTemplateDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Template";
|
||||
this.Load += new System.EventHandler(this.EditTemplateDialog_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.TextBox templateTb;
|
||||
private System.Windows.Forms.Label templateLbl;
|
||||
private System.Windows.Forms.Button resetToDefaultBtn;
|
||||
private System.Windows.Forms.ListView listView1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader2;
|
||||
private System.Windows.Forms.RichTextBox richTextBox1;
|
||||
private System.Windows.Forms.Label warningsLbl;
|
||||
private System.Windows.Forms.Label exampleLbl;
|
||||
}
|
||||
}
|
||||
173
LibationWinForms/Dialogs/EditTemplateDialog.cs
Normal file
173
LibationWinForms/Dialogs/EditTemplateDialog.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class EditTemplateDialog : Form
|
||||
{
|
||||
// final value. post-validity check
|
||||
public string TemplateText { get; private set; }
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _workingTemplateText;
|
||||
private string workingTemplateText
|
||||
{
|
||||
get => _workingTemplateText;
|
||||
set => _workingTemplateText = template.Sanitize(value);
|
||||
}
|
||||
|
||||
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
|
||||
|
||||
private Configuration config { get; } = Configuration.Instance;
|
||||
|
||||
private Templates template { get; }
|
||||
private string inputTemplateText { get; }
|
||||
|
||||
public EditTemplateDialog() => InitializeComponent();
|
||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
||||
{
|
||||
this.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||
this.inputTemplateText = inputTemplateText ?? "";
|
||||
}
|
||||
|
||||
private void EditTemplateDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
if (template is null)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show($"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
|
||||
return;
|
||||
}
|
||||
|
||||
warningsLbl.Text = "";
|
||||
|
||||
this.Text = $"Edit {template.Name}";
|
||||
|
||||
this.templateLbl.Text = template.Description;
|
||||
resetTextBox(inputTemplateText);
|
||||
|
||||
// populate list view
|
||||
foreach (var tag in template.GetTemplateTags())
|
||||
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }));
|
||||
}
|
||||
|
||||
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate);
|
||||
|
||||
private void templateTb_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
workingTemplateText = templateTb.Text;
|
||||
|
||||
var isFolder = template == Templates.Folder;
|
||||
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
Account = "my account",
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = "Sherlock Holmes",
|
||||
SeriesNumber = "1"
|
||||
};
|
||||
var chapterName = "A Flight for Life";
|
||||
var chapterNumber = 4;
|
||||
var chaptersTotal = 10;
|
||||
|
||||
var books = config.Books;
|
||||
var folder = Templates.Folder.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? workingTemplateText : config.FolderTemplate);
|
||||
var file
|
||||
= template == Templates.ChapterFile
|
||||
? Templates.ChapterFile.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
workingTemplateText,
|
||||
new() { OutputFileName = "", PartsPosition = chapterNumber, PartsTotal = chaptersTotal, Title = chapterName },
|
||||
"")
|
||||
: Templates.File.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? config.FileTemplate : workingTemplateText);
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
warningsLbl.Text
|
||||
= !template.HasWarnings(workingTemplateText)
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
template
|
||||
.GetWarnings(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
var bold = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Bold);
|
||||
var reg = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Regular);
|
||||
|
||||
richTextBox1.Clear();
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText(slashWrap(books));
|
||||
richTextBox1.AppendText(sing);
|
||||
|
||||
if (isFolder)
|
||||
richTextBox1.SelectionFont = bold;
|
||||
|
||||
richTextBox1.AppendText(slashWrap(folder));
|
||||
|
||||
if (isFolder)
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText(sing);
|
||||
|
||||
if (!isFolder)
|
||||
richTextBox1.SelectionFont = bold;
|
||||
|
||||
richTextBox1.AppendText(file);
|
||||
|
||||
if (!isFolder)
|
||||
richTextBox1.SelectionFont = reg;
|
||||
|
||||
richTextBox1.AppendText($".{ext}");
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!template.IsValid(workingTemplateText))
|
||||
{
|
||||
var errors = template
|
||||
.GetErrors(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
TemplateText = workingTemplateText;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
LibationWinForms/Dialogs/EditTemplateDialog.resx
Normal file
60
LibationWinForms/Dialogs/EditTemplateDialog.resx
Normal file
@@ -0,0 +1,60 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,9 +1,8 @@
|
||||
using ApplicationServices;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Login;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Login;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -28,140 +28,152 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginExternalDialog));
|
||||
this.submitBtn = new System.Windows.Forms.Button();
|
||||
this.localeLbl = new System.Windows.Forms.Label();
|
||||
this.usernameLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.copyBtn = new System.Windows.Forms.Button();
|
||||
this.launchBrowserBtn = new System.Windows.Forms.Button();
|
||||
this.instructionsLbl = new System.Windows.Forms.Label();
|
||||
this.responseUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// submitBtn
|
||||
//
|
||||
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.submitBtn.Location = new System.Drawing.Point(665, 400);
|
||||
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.submitBtn.Name = "submitBtn";
|
||||
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.submitBtn.TabIndex = 8;
|
||||
this.submitBtn.Text = "Submit";
|
||||
this.submitBtn.UseVisualStyleBackColor = true;
|
||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||
//
|
||||
// localeLbl
|
||||
//
|
||||
this.localeLbl.AutoSize = true;
|
||||
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.localeLbl.Name = "localeLbl";
|
||||
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
this.localeLbl.TabIndex = 0;
|
||||
this.localeLbl.Text = "Locale: {0}";
|
||||
//
|
||||
// usernameLbl
|
||||
//
|
||||
this.usernameLbl.AutoSize = true;
|
||||
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.usernameLbl.Name = "usernameLbl";
|
||||
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.usernameLbl.TabIndex = 1;
|
||||
this.usernameLbl.Text = "Username: {0}";
|
||||
//
|
||||
// loginUrlLbl
|
||||
//
|
||||
this.loginUrlLbl.AutoSize = true;
|
||||
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
|
||||
this.loginUrlLbl.Name = "loginUrlLbl";
|
||||
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
|
||||
this.loginUrlLbl.TabIndex = 2;
|
||||
this.loginUrlLbl.Text = "Paste this URL into your browser:";
|
||||
//
|
||||
// loginUrlTb
|
||||
//
|
||||
this.loginUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginExternalDialog));
|
||||
this.submitBtn = new System.Windows.Forms.Button();
|
||||
this.localeLbl = new System.Windows.Forms.Label();
|
||||
this.usernameLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlLbl = new System.Windows.Forms.Label();
|
||||
this.loginUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.copyBtn = new System.Windows.Forms.Button();
|
||||
this.launchBrowserBtn = new System.Windows.Forms.Button();
|
||||
this.instructionsLbl = new System.Windows.Forms.Label();
|
||||
this.responseUrlTb = new System.Windows.Forms.TextBox();
|
||||
this.tldrLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// submitBtn
|
||||
//
|
||||
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.submitBtn.Location = new System.Drawing.Point(665, 458);
|
||||
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.submitBtn.Name = "submitBtn";
|
||||
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.submitBtn.TabIndex = 8;
|
||||
this.submitBtn.Text = "Submit";
|
||||
this.submitBtn.UseVisualStyleBackColor = true;
|
||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||
//
|
||||
// localeLbl
|
||||
//
|
||||
this.localeLbl.AutoSize = true;
|
||||
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.localeLbl.Name = "localeLbl";
|
||||
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
this.localeLbl.TabIndex = 0;
|
||||
this.localeLbl.Text = "Locale: {0}";
|
||||
//
|
||||
// usernameLbl
|
||||
//
|
||||
this.usernameLbl.AutoSize = true;
|
||||
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.usernameLbl.Name = "usernameLbl";
|
||||
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.usernameLbl.TabIndex = 1;
|
||||
this.usernameLbl.Text = "Username: {0}";
|
||||
//
|
||||
// loginUrlLbl
|
||||
//
|
||||
this.loginUrlLbl.AutoSize = true;
|
||||
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
|
||||
this.loginUrlLbl.Name = "loginUrlLbl";
|
||||
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
|
||||
this.loginUrlLbl.TabIndex = 2;
|
||||
this.loginUrlLbl.Text = "Paste this URL into your browser:";
|
||||
//
|
||||
// loginUrlTb
|
||||
//
|
||||
this.loginUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.loginUrlTb.Location = new System.Drawing.Point(14, 79);
|
||||
this.loginUrlTb.Multiline = true;
|
||||
this.loginUrlTb.Name = "loginUrlTb";
|
||||
this.loginUrlTb.ReadOnly = true;
|
||||
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.loginUrlTb.Size = new System.Drawing.Size(739, 92);
|
||||
this.loginUrlTb.TabIndex = 3;
|
||||
//
|
||||
// copyBtn
|
||||
//
|
||||
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.copyBtn.Location = new System.Drawing.Point(14, 177);
|
||||
this.copyBtn.Name = "copyBtn";
|
||||
this.copyBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.copyBtn.TabIndex = 4;
|
||||
this.copyBtn.Text = "Copy URL to clipboard";
|
||||
this.copyBtn.UseVisualStyleBackColor = true;
|
||||
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
|
||||
//
|
||||
// launchBrowserBtn
|
||||
//
|
||||
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.launchBrowserBtn.Location = new System.Drawing.Point(588, 177);
|
||||
this.launchBrowserBtn.Name = "launchBrowserBtn";
|
||||
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.launchBrowserBtn.TabIndex = 5;
|
||||
this.launchBrowserBtn.Text = "Launch in browser";
|
||||
this.launchBrowserBtn.UseVisualStyleBackColor = true;
|
||||
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
|
||||
//
|
||||
// instructionsLbl
|
||||
//
|
||||
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.instructionsLbl.AutoSize = true;
|
||||
this.instructionsLbl.Location = new System.Drawing.Point(14, 203);
|
||||
this.instructionsLbl.Name = "instructionsLbl";
|
||||
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
|
||||
this.instructionsLbl.TabIndex = 6;
|
||||
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
|
||||
//
|
||||
// responseUrlTb
|
||||
//
|
||||
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.loginUrlTb.Location = new System.Drawing.Point(14, 79);
|
||||
this.loginUrlTb.Multiline = true;
|
||||
this.loginUrlTb.Name = "loginUrlTb";
|
||||
this.loginUrlTb.ReadOnly = true;
|
||||
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.loginUrlTb.Size = new System.Drawing.Size(739, 117);
|
||||
this.loginUrlTb.TabIndex = 3;
|
||||
//
|
||||
// copyBtn
|
||||
//
|
||||
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.copyBtn.Location = new System.Drawing.Point(14, 202);
|
||||
this.copyBtn.Name = "copyBtn";
|
||||
this.copyBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.copyBtn.TabIndex = 4;
|
||||
this.copyBtn.Text = "Copy URL to clipboard";
|
||||
this.copyBtn.UseVisualStyleBackColor = true;
|
||||
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
|
||||
//
|
||||
// launchBrowserBtn
|
||||
//
|
||||
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.launchBrowserBtn.Location = new System.Drawing.Point(589, 202);
|
||||
this.launchBrowserBtn.Name = "launchBrowserBtn";
|
||||
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
|
||||
this.launchBrowserBtn.TabIndex = 5;
|
||||
this.launchBrowserBtn.Text = "Launch in browser";
|
||||
this.launchBrowserBtn.UseVisualStyleBackColor = true;
|
||||
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
|
||||
//
|
||||
// instructionsLbl
|
||||
//
|
||||
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.instructionsLbl.AutoSize = true;
|
||||
this.instructionsLbl.Location = new System.Drawing.Point(14, 261);
|
||||
this.instructionsLbl.Name = "instructionsLbl";
|
||||
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
|
||||
this.instructionsLbl.TabIndex = 6;
|
||||
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
|
||||
//
|
||||
// responseUrlTb
|
||||
//
|
||||
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.responseUrlTb.Location = new System.Drawing.Point(14, 296);
|
||||
this.responseUrlTb.Multiline = true;
|
||||
this.responseUrlTb.Name = "responseUrlTb";
|
||||
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
|
||||
this.responseUrlTb.TabIndex = 7;
|
||||
//
|
||||
// LoginExternalDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(766, 440);
|
||||
this.Controls.Add(this.responseUrlTb);
|
||||
this.Controls.Add(this.instructionsLbl);
|
||||
this.Controls.Add(this.launchBrowserBtn);
|
||||
this.Controls.Add(this.copyBtn);
|
||||
this.Controls.Add(this.loginUrlTb);
|
||||
this.Controls.Add(this.loginUrlLbl);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
this.Controls.Add(this.submitBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "LoginExternalDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Audible External Login";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.responseUrlTb.Location = new System.Drawing.Point(14, 354);
|
||||
this.responseUrlTb.Multiline = true;
|
||||
this.responseUrlTb.Name = "responseUrlTb";
|
||||
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
|
||||
this.responseUrlTb.TabIndex = 7;
|
||||
//
|
||||
// tldrLbl
|
||||
//
|
||||
this.tldrLbl.AutoSize = true;
|
||||
this.tldrLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.tldrLbl.Location = new System.Drawing.Point(14, 237);
|
||||
this.tldrLbl.Name = "tldrLbl";
|
||||
this.tldrLbl.Size = new System.Drawing.Size(421, 15);
|
||||
this.tldrLbl.TabIndex = 9;
|
||||
this.tldrLbl.Text = "tl;dr : an ERROR on Amazon is GOOD. Sorry, I can\'t control their weird login";
|
||||
//
|
||||
// LoginExternalDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(766, 498);
|
||||
this.Controls.Add(this.tldrLbl);
|
||||
this.Controls.Add(this.responseUrlTb);
|
||||
this.Controls.Add(this.instructionsLbl);
|
||||
this.Controls.Add(this.launchBrowserBtn);
|
||||
this.Controls.Add(this.copyBtn);
|
||||
this.Controls.Add(this.loginUrlTb);
|
||||
this.Controls.Add(this.loginUrlLbl);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
this.Controls.Add(this.submitBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "LoginExternalDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Audible External Login";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -175,5 +187,6 @@
|
||||
private System.Windows.Forms.Button launchBrowserBtn;
|
||||
private System.Windows.Forms.Label instructionsLbl;
|
||||
private System.Windows.Forms.TextBox responseUrlTb;
|
||||
}
|
||||
private System.Windows.Forms.Label tldrLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
|
||||
namespace LibationWinForms.Login
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using AudibleApi;
|
||||
using InternalUtilities;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
|
||||
namespace LibationWinForms.Login
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Login;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Login;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using InternalUtilities;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
using AudibleUtilities;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
840
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
840
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -28,326 +28,517 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.inProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
||||
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAskRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
|
||||
this.splitFilesByChapterCbox = new System.Windows.Forms.CheckBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.logsBtn = new System.Windows.Forms.Button();
|
||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
this.advancedSettingsGb.SuspendLayout();
|
||||
this.badBookGb.SuspendLayout();
|
||||
this.decryptAndConvertGb.SuspendLayout();
|
||||
this.booksGb.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
||||
this.booksLocationDescLbl.TabIndex = 1;
|
||||
this.booksLocationDescLbl.Text = "[book desc]";
|
||||
//
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
|
||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
|
||||
this.inProgressDescLbl.TabIndex = 18;
|
||||
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// advancedSettingsGb
|
||||
//
|
||||
this.advancedSettingsGb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.inProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
|
||||
this.badBookAskRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
|
||||
this.splitFilesByChapterCbox = new System.Windows.Forms.CheckBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||
this.logsBtn = new System.Windows.Forms.Button();
|
||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
this.tabControl = new System.Windows.Forms.TabControl();
|
||||
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
||||
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
|
||||
this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
|
||||
this.customFileNamingGb = new System.Windows.Forms.GroupBox();
|
||||
this.chapterFileTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.chapterFileTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.chapterFileTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.fileTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.fileTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.fileTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.folderTemplateBtn = new System.Windows.Forms.Button();
|
||||
this.folderTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.folderTemplateLbl = new System.Windows.Forms.Label();
|
||||
this.showImportedStatsCb = new System.Windows.Forms.CheckBox();
|
||||
this.badBookGb.SuspendLayout();
|
||||
this.decryptAndConvertGb.SuspendLayout();
|
||||
this.tabControl.SuspendLayout();
|
||||
this.tab1ImportantSettings.SuspendLayout();
|
||||
this.booksGb.SuspendLayout();
|
||||
this.tab2ImportLibrary.SuspendLayout();
|
||||
this.tab3DownloadDecrypt.SuspendLayout();
|
||||
this.inProgressFilesGb.SuspendLayout();
|
||||
this.customFileNamingGb.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
||||
this.booksLocationDescLbl.TabIndex = 1;
|
||||
this.booksLocationDescLbl.Text = "[book desc]";
|
||||
//
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
this.inProgressDescLbl.Size = new System.Drawing.Size(100, 45);
|
||||
this.inProgressDescLbl.TabIndex = 18;
|
||||
this.inProgressDescLbl.Text = "[in progress desc]\r\n[line 2]\r\n[line 3]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// importEpisodesCb
|
||||
//
|
||||
this.importEpisodesCb.AutoSize = true;
|
||||
this.importEpisodesCb.Location = new System.Drawing.Point(6, 31);
|
||||
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||
this.importEpisodesCb.TabIndex = 2;
|
||||
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
this.downloadEpisodesCb.AutoSize = true;
|
||||
this.downloadEpisodesCb.Location = new System.Drawing.Point(6, 56);
|
||||
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
this.downloadEpisodesCb.TabIndex = 3;
|
||||
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookGb
|
||||
//
|
||||
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||
this.badBookGb.Location = new System.Drawing.Point(371, 6);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(524, 124);
|
||||
this.badBookGb.TabIndex = 13;
|
||||
this.badBookGb.TabStop = false;
|
||||
this.badBookGb.Text = "[bad book desc]";
|
||||
//
|
||||
// badBookIgnoreRb
|
||||
//
|
||||
this.badBookIgnoreRb.AutoSize = true;
|
||||
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||
this.badBookIgnoreRb.TabIndex = 17;
|
||||
this.badBookIgnoreRb.TabStop = true;
|
||||
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookRetryRb
|
||||
//
|
||||
this.badBookRetryRb.AutoSize = true;
|
||||
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||
this.badBookRetryRb.TabIndex = 16;
|
||||
this.badBookRetryRb.TabStop = true;
|
||||
this.badBookRetryRb.Text = "[retry desc]";
|
||||
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAbortRb
|
||||
//
|
||||
this.badBookAbortRb.AutoSize = true;
|
||||
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||
this.badBookAbortRb.TabIndex = 15;
|
||||
this.badBookAbortRb.TabStop = true;
|
||||
this.badBookAbortRb.Text = "[abort desc]";
|
||||
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAskRb
|
||||
//
|
||||
this.badBookAskRb.AutoSize = true;
|
||||
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.badBookAskRb.Name = "badBookAskRb";
|
||||
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||
this.badBookAskRb.TabIndex = 14;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptAndConvertGb
|
||||
//
|
||||
this.decryptAndConvertGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(6, 6);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 9;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
// splitFilesByChapterCbox
|
||||
//
|
||||
this.splitFilesByChapterCbox.AutoSize = true;
|
||||
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(6, 46);
|
||||
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
|
||||
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
|
||||
this.splitFilesByChapterCbox.TabIndex = 13;
|
||||
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
||||
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
//
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
this.allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// convertLossyRb
|
||||
//
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 101);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// convertLosslessRb
|
||||
//
|
||||
this.convertLosslessRb.AutoSize = true;
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 76);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 11;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68);
|
||||
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(875, 52);
|
||||
this.inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
this.logsBtn.Location = new System.Drawing.Point(256, 169);
|
||||
this.logsBtn.Name = "logsBtn";
|
||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
this.logsBtn.TabIndex = 5;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(876, 87);
|
||||
this.booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
this.loggingLevelLbl.AutoSize = true;
|
||||
this.loggingLevelLbl.Location = new System.Drawing.Point(6, 172);
|
||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
this.loggingLevelLbl.TabIndex = 3;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
//
|
||||
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.loggingLevelCb.FormattingEnabled = true;
|
||||
this.loggingLevelCb.Location = new System.Drawing.Point(90, 169);
|
||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
this.loggingLevelCb.TabIndex = 4;
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
|
||||
this.advancedSettingsGb.Controls.Add(this.badBookGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
|
||||
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Name = "advancedSettingsGb";
|
||||
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
|
||||
this.advancedSettingsGb.TabIndex = 6;
|
||||
this.advancedSettingsGb.TabStop = false;
|
||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// importEpisodesCb
|
||||
//
|
||||
this.importEpisodesCb.AutoSize = true;
|
||||
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
|
||||
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||
this.importEpisodesCb.TabIndex = 7;
|
||||
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
this.downloadEpisodesCb.AutoSize = true;
|
||||
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
|
||||
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
this.downloadEpisodesCb.TabIndex = 8;
|
||||
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookGb
|
||||
//
|
||||
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||
this.badBookGb.Location = new System.Drawing.Point(372, 72);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(529, 124);
|
||||
this.badBookGb.TabIndex = 13;
|
||||
this.badBookGb.TabStop = false;
|
||||
this.badBookGb.Text = "[bad book desc]";
|
||||
//
|
||||
// badBookIgnoreRb
|
||||
//
|
||||
this.badBookIgnoreRb.AutoSize = true;
|
||||
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||
this.badBookIgnoreRb.TabIndex = 17;
|
||||
this.badBookIgnoreRb.TabStop = true;
|
||||
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookRetryRb
|
||||
//
|
||||
this.badBookRetryRb.AutoSize = true;
|
||||
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||
this.badBookRetryRb.TabIndex = 16;
|
||||
this.badBookRetryRb.TabStop = true;
|
||||
this.badBookRetryRb.Text = "[retry desc]";
|
||||
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAbortRb
|
||||
//
|
||||
this.badBookAbortRb.AutoSize = true;
|
||||
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||
this.badBookAbortRb.TabIndex = 15;
|
||||
this.badBookAbortRb.TabStop = true;
|
||||
this.badBookAbortRb.Text = "[abort desc]";
|
||||
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// badBookAskRb
|
||||
//
|
||||
this.badBookAskRb.AutoSize = true;
|
||||
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.badBookAskRb.Name = "badBookAskRb";
|
||||
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||
this.badBookAskRb.TabIndex = 14;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptAndConvertGb
|
||||
//
|
||||
this.decryptAndConvertGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 9;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
// splitFilesByChapterCbox
|
||||
//
|
||||
this.splitFilesByChapterCbox.AutoSize = true;
|
||||
this.splitFilesByChapterCbox.Location = new System.Drawing.Point(6, 46);
|
||||
this.splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
|
||||
this.splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
|
||||
this.splitFilesByChapterCbox.TabIndex = 13;
|
||||
this.splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
|
||||
this.splitFilesByChapterCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
//
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
this.allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// convertLossyRb
|
||||
//
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 101);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// convertLosslessRb
|
||||
//
|
||||
this.convertLosslessRb.AutoSize = true;
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 76);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 11;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.tabControl.Controls.Add(this.tab1ImportantSettings);
|
||||
this.tabControl.Controls.Add(this.tab2ImportLibrary);
|
||||
this.tabControl.Controls.Add(this.tab3DownloadDecrypt);
|
||||
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
this.tabControl.Name = "tabControl";
|
||||
this.tabControl.SelectedIndex = 0;
|
||||
this.tabControl.Size = new System.Drawing.Size(909, 478);
|
||||
this.tabControl.TabIndex = 100;
|
||||
//
|
||||
// tab1ImportantSettings
|
||||
//
|
||||
this.tab1ImportantSettings.Controls.Add(this.booksGb);
|
||||
this.tab1ImportantSettings.Controls.Add(this.logsBtn);
|
||||
this.tab1ImportantSettings.Controls.Add(this.loggingLevelCb);
|
||||
this.tab1ImportantSettings.Controls.Add(this.loggingLevelLbl);
|
||||
this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab1ImportantSettings.Name = "tab1ImportantSettings";
|
||||
this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab1ImportantSettings.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab1ImportantSettings.TabIndex = 0;
|
||||
this.tab1ImportantSettings.Text = "Important settings";
|
||||
this.tab1ImportantSettings.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
|
||||
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(894, 52);
|
||||
this.inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
this.logsBtn.Location = new System.Drawing.Point(262, 147);
|
||||
this.logsBtn.Name = "logsBtn";
|
||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
this.logsBtn.TabIndex = 5;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
this.booksSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.booksGb.Controls.Add(this.booksSelectControl);
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
this.booksGb.Location = new System.Drawing.Point(6, 6);
|
||||
this.booksGb.Name = "booksGb";
|
||||
this.booksGb.Size = new System.Drawing.Size(889, 129);
|
||||
this.booksGb.TabIndex = 0;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
// tab2ImportLibrary
|
||||
//
|
||||
this.tab2ImportLibrary.Controls.Add(this.showImportedStatsCb);
|
||||
this.tab2ImportLibrary.Controls.Add(this.importEpisodesCb);
|
||||
this.tab2ImportLibrary.Controls.Add(this.downloadEpisodesCb);
|
||||
this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab2ImportLibrary.Name = "tab2ImportLibrary";
|
||||
this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab2ImportLibrary.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab2ImportLibrary.TabIndex = 1;
|
||||
this.tab2ImportLibrary.Text = "Import library";
|
||||
this.tab2ImportLibrary.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// tab3DownloadDecrypt
|
||||
//
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.inProgressFilesGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.customFileNamingGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.decryptAndConvertGb);
|
||||
this.tab3DownloadDecrypt.Controls.Add(this.badBookGb);
|
||||
this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
|
||||
this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
|
||||
this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tab3DownloadDecrypt.Size = new System.Drawing.Size(901, 450);
|
||||
this.tab3DownloadDecrypt.TabIndex = 2;
|
||||
this.tab3DownloadDecrypt.Text = "Download/Decrypt";
|
||||
this.tab3DownloadDecrypt.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressFilesGb
|
||||
//
|
||||
this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
||||
this.booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl);
|
||||
this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.inProgressFilesGb.Location = new System.Drawing.Point(7, 299);
|
||||
this.inProgressFilesGb.Name = "inProgressFilesGb";
|
||||
this.inProgressFilesGb.Size = new System.Drawing.Size(888, 128);
|
||||
this.inProgressFilesGb.TabIndex = 21;
|
||||
this.inProgressFilesGb.TabStop = false;
|
||||
this.inProgressFilesGb.Text = "In progress files";
|
||||
//
|
||||
// customFileNamingGb
|
||||
//
|
||||
this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksGb.Controls.Add(this.booksSelectControl);
|
||||
this.booksGb.Controls.Add(this.booksLocationDescLbl);
|
||||
this.booksGb.Location = new System.Drawing.Point(12, 12);
|
||||
this.booksGb.Name = "booksGb";
|
||||
this.booksGb.Size = new System.Drawing.Size(908, 129);
|
||||
this.booksGb.TabIndex = 0;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
this.loggingLevelLbl.AutoSize = true;
|
||||
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
|
||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
this.loggingLevelLbl.TabIndex = 3;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
//
|
||||
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.loggingLevelCb.FormattingEnabled = true;
|
||||
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
|
||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
this.loggingLevelCb.TabIndex = 4;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||
this.Controls.Add(this.logsBtn);
|
||||
this.Controls.Add(this.loggingLevelCb);
|
||||
this.Controls.Add(this.loggingLevelLbl);
|
||||
this.Controls.Add(this.booksGb);
|
||||
this.Controls.Add(this.advancedSettingsGb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.advancedSettingsGb.ResumeLayout(false);
|
||||
this.advancedSettingsGb.PerformLayout();
|
||||
this.badBookGb.ResumeLayout(false);
|
||||
this.badBookGb.PerformLayout();
|
||||
this.decryptAndConvertGb.ResumeLayout(false);
|
||||
this.decryptAndConvertGb.PerformLayout();
|
||||
this.booksGb.ResumeLayout(false);
|
||||
this.booksGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.fileTemplateLbl);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateBtn);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateTb);
|
||||
this.customFileNamingGb.Controls.Add(this.folderTemplateLbl);
|
||||
this.customFileNamingGb.Location = new System.Drawing.Point(7, 136);
|
||||
this.customFileNamingGb.Name = "customFileNamingGb";
|
||||
this.customFileNamingGb.Size = new System.Drawing.Size(888, 157);
|
||||
this.customFileNamingGb.TabIndex = 20;
|
||||
this.customFileNamingGb.TabStop = false;
|
||||
this.customFileNamingGb.Text = "Custom file naming";
|
||||
//
|
||||
// chapterFileTemplateBtn
|
||||
//
|
||||
this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterFileTemplateBtn.Location = new System.Drawing.Point(808, 124);
|
||||
this.chapterFileTemplateBtn.Name = "chapterFileTemplateBtn";
|
||||
this.chapterFileTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.chapterFileTemplateBtn.TabIndex = 8;
|
||||
this.chapterFileTemplateBtn.Text = "Edit...";
|
||||
this.chapterFileTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.chapterFileTemplateBtn.Click += new System.EventHandler(this.chapterFileTemplateBtn_Click);
|
||||
//
|
||||
// chapterFileTemplateTb
|
||||
//
|
||||
this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125);
|
||||
this.chapterFileTemplateTb.Name = "chapterFileTemplateTb";
|
||||
this.chapterFileTemplateTb.ReadOnly = true;
|
||||
this.chapterFileTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.chapterFileTemplateTb.TabIndex = 7;
|
||||
//
|
||||
// chapterFileTemplateLbl
|
||||
//
|
||||
this.chapterFileTemplateLbl.AutoSize = true;
|
||||
this.chapterFileTemplateLbl.Location = new System.Drawing.Point(6, 107);
|
||||
this.chapterFileTemplateLbl.Name = "chapterFileTemplateLbl";
|
||||
this.chapterFileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.chapterFileTemplateLbl.TabIndex = 6;
|
||||
this.chapterFileTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// fileTemplateBtn
|
||||
//
|
||||
this.fileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.fileTemplateBtn.Location = new System.Drawing.Point(808, 80);
|
||||
this.fileTemplateBtn.Name = "fileTemplateBtn";
|
||||
this.fileTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.fileTemplateBtn.TabIndex = 5;
|
||||
this.fileTemplateBtn.Text = "Edit...";
|
||||
this.fileTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.fileTemplateBtn.Click += new System.EventHandler(this.fileTemplateBtn_Click);
|
||||
//
|
||||
// fileTemplateTb
|
||||
//
|
||||
this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.fileTemplateTb.Location = new System.Drawing.Point(6, 81);
|
||||
this.fileTemplateTb.Name = "fileTemplateTb";
|
||||
this.fileTemplateTb.ReadOnly = true;
|
||||
this.fileTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.fileTemplateTb.TabIndex = 4;
|
||||
//
|
||||
// fileTemplateLbl
|
||||
//
|
||||
this.fileTemplateLbl.AutoSize = true;
|
||||
this.fileTemplateLbl.Location = new System.Drawing.Point(6, 63);
|
||||
this.fileTemplateLbl.Name = "fileTemplateLbl";
|
||||
this.fileTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.fileTemplateLbl.TabIndex = 3;
|
||||
this.fileTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// folderTemplateBtn
|
||||
//
|
||||
this.folderTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.folderTemplateBtn.Location = new System.Drawing.Point(807, 36);
|
||||
this.folderTemplateBtn.Name = "folderTemplateBtn";
|
||||
this.folderTemplateBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.folderTemplateBtn.TabIndex = 2;
|
||||
this.folderTemplateBtn.Text = "Edit...";
|
||||
this.folderTemplateBtn.UseVisualStyleBackColor = true;
|
||||
this.folderTemplateBtn.Click += new System.EventHandler(this.folderTemplateBtn_Click);
|
||||
//
|
||||
// folderTemplateTb
|
||||
//
|
||||
this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.folderTemplateTb.Location = new System.Drawing.Point(5, 37);
|
||||
this.folderTemplateTb.Name = "folderTemplateTb";
|
||||
this.folderTemplateTb.ReadOnly = true;
|
||||
this.folderTemplateTb.Size = new System.Drawing.Size(796, 23);
|
||||
this.folderTemplateTb.TabIndex = 1;
|
||||
//
|
||||
// folderTemplateLbl
|
||||
//
|
||||
this.folderTemplateLbl.AutoSize = true;
|
||||
this.folderTemplateLbl.Location = new System.Drawing.Point(5, 19);
|
||||
this.folderTemplateLbl.Name = "folderTemplateLbl";
|
||||
this.folderTemplateLbl.Size = new System.Drawing.Size(123, 15);
|
||||
this.folderTemplateLbl.TabIndex = 0;
|
||||
this.folderTemplateLbl.Text = "[folder template desc]";
|
||||
//
|
||||
// showImportedStatsCb
|
||||
//
|
||||
this.showImportedStatsCb.AutoSize = true;
|
||||
this.showImportedStatsCb.Location = new System.Drawing.Point(6, 6);
|
||||
this.showImportedStatsCb.Name = "showImportedStatsCb";
|
||||
this.showImportedStatsCb.Size = new System.Drawing.Size(168, 19);
|
||||
this.showImportedStatsCb.TabIndex = 1;
|
||||
this.showImportedStatsCb.Text = "[show imported stats desc]";
|
||||
this.showImportedStatsCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.badBookGb.ResumeLayout(false);
|
||||
this.badBookGb.PerformLayout();
|
||||
this.decryptAndConvertGb.ResumeLayout(false);
|
||||
this.decryptAndConvertGb.PerformLayout();
|
||||
this.tabControl.ResumeLayout(false);
|
||||
this.tab1ImportantSettings.ResumeLayout(false);
|
||||
this.tab1ImportantSettings.PerformLayout();
|
||||
this.booksGb.ResumeLayout(false);
|
||||
this.booksGb.PerformLayout();
|
||||
this.tab2ImportLibrary.ResumeLayout(false);
|
||||
this.tab2ImportLibrary.PerformLayout();
|
||||
this.tab3DownloadDecrypt.ResumeLayout(false);
|
||||
this.inProgressFilesGb.ResumeLayout(false);
|
||||
this.inProgressFilesGb.PerformLayout();
|
||||
this.customFileNamingGb.ResumeLayout(false);
|
||||
this.customFileNamingGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
@@ -356,13 +547,11 @@
|
||||
private System.Windows.Forms.Label inProgressDescLbl;
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.GroupBox advancedSettingsGb;
|
||||
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private DirectorySelectControl inProgressSelectControl;
|
||||
private System.Windows.Forms.RadioButton convertLossyRb;
|
||||
private System.Windows.Forms.RadioButton convertLosslessRb;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
private System.Windows.Forms.Label loggingLevelLbl;
|
||||
private System.Windows.Forms.ComboBox loggingLevelCb;
|
||||
@@ -375,5 +564,22 @@
|
||||
private System.Windows.Forms.CheckBox downloadEpisodesCb;
|
||||
private System.Windows.Forms.CheckBox importEpisodesCb;
|
||||
private System.Windows.Forms.CheckBox splitFilesByChapterCbox;
|
||||
private System.Windows.Forms.TabControl tabControl;
|
||||
private System.Windows.Forms.TabPage tab1ImportantSettings;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.TabPage tab2ImportLibrary;
|
||||
private System.Windows.Forms.TabPage tab3DownloadDecrypt;
|
||||
private System.Windows.Forms.GroupBox inProgressFilesGb;
|
||||
private System.Windows.Forms.GroupBox customFileNamingGb;
|
||||
private System.Windows.Forms.Button chapterFileTemplateBtn;
|
||||
private System.Windows.Forms.TextBox chapterFileTemplateTb;
|
||||
private System.Windows.Forms.Label chapterFileTemplateLbl;
|
||||
private System.Windows.Forms.Button fileTemplateBtn;
|
||||
private System.Windows.Forms.TextBox fileTemplateTb;
|
||||
private System.Windows.Forms.Label fileTemplateLbl;
|
||||
private System.Windows.Forms.Button folderTemplateBtn;
|
||||
private System.Windows.Forms.TextBox folderTemplateTb;
|
||||
private System.Windows.Forms.Label folderTemplateLbl;
|
||||
private System.Windows.Forms.CheckBox showImportedStatsCb;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
@@ -26,6 +27,7 @@ namespace LibationWinForms.Dialogs
|
||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||
}
|
||||
|
||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
@@ -45,6 +47,7 @@ namespace LibationWinForms.Dialogs
|
||||
"Books");
|
||||
booksSelectControl.SelectDirectory(config.Books);
|
||||
|
||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
@@ -78,6 +81,13 @@ namespace LibationWinForms.Dialogs
|
||||
_ => this.badBookAskRb
|
||||
};
|
||||
rb.Checked = true;
|
||||
|
||||
folderTemplateLbl.Text = desc(nameof(config.FolderTemplate));
|
||||
fileTemplateLbl.Text = desc(nameof(config.FileTemplate));
|
||||
chapterFileTemplateLbl.Text = desc(nameof(config.ChapterFileTemplate));
|
||||
folderTemplateTb.Text = config.FolderTemplate;
|
||||
fileTemplateTb.Text = config.FileTemplate;
|
||||
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
||||
}
|
||||
|
||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||
@@ -95,28 +105,56 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
|
||||
|
||||
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
|
||||
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
|
||||
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
|
||||
private static void editTemplate(Templates template, TextBox textBox)
|
||||
{
|
||||
var form = new EditTemplateDialog(template, textBox.Text);
|
||||
if (form.ShowDialog() == DialogResult.OK)
|
||||
textBox.Text = form.TemplateText;
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
#region validation
|
||||
static void validationError(string text, string caption)
|
||||
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
if (string.IsNullOrWhiteSpace(newBooks))
|
||||
{
|
||||
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
validationError("Cannot set Books Location to blank", "Location is blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(newBooks))
|
||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
if (booksSelectControl.SelectedDirectoryIsCustom)
|
||||
{
|
||||
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (booksSelectControl.SelectedDirectoryIsKnown)
|
||||
Directory.CreateDirectory(newBooks);
|
||||
validationError($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
||||
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
||||
return;
|
||||
}
|
||||
if (!Templates.File.IsValid(fileTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
||||
return;
|
||||
}
|
||||
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
|
||||
{
|
||||
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
||||
return;
|
||||
}
|
||||
#endregion
|
||||
|
||||
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsKnown)
|
||||
Directory.CreateDirectory(newBooks);
|
||||
|
||||
config.Books = newBooks;
|
||||
|
||||
{
|
||||
@@ -130,6 +168,7 @@ namespace LibationWinForms.Dialogs
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
@@ -145,6 +184,10 @@ namespace LibationWinForms.Dialogs
|
||||
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
||||
: Configuration.BadBookAction.Ask;
|
||||
|
||||
config.FolderTemplate = folderTemplateTb.Text;
|
||||
config.FileTemplate = fileTemplateTb.Text;
|
||||
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Drawing;
|
||||
using Dinah.Core.Threading;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
@@ -334,7 +334,8 @@ namespace LibationWinForms
|
||||
var totalProcessed = dialog.TotalBooksProcessed;
|
||||
var newAdded = dialog.NewBooksAdded;
|
||||
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
if (Configuration.Instance.ShowImportedStats)
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace LibationWinForms
|
||||
/// </summary>
|
||||
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||
{
|
||||
#region implementation properties
|
||||
#region implementation properties NOT exposed to the view
|
||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
||||
|
||||
[Browsable(false)]
|
||||
@@ -28,10 +28,52 @@ namespace LibationWinForms
|
||||
|
||||
#endregion
|
||||
|
||||
#region Model properties exposed to the view
|
||||
private Image _cover;
|
||||
public Image Cover
|
||||
{
|
||||
get => _cover;
|
||||
private set
|
||||
{
|
||||
_cover = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ProductRating { get; }
|
||||
public string PurchaseDate { get; }
|
||||
public string MyRating { get; }
|
||||
public string Series { get; }
|
||||
public string Title { get; }
|
||||
public string Length { get; }
|
||||
public string Authors { get; }
|
||||
public string Narrators { get; }
|
||||
public string Category { get; }
|
||||
public string Misc { get; }
|
||||
public string Description { get; }
|
||||
public string DisplayTags
|
||||
{
|
||||
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
set => Book.UserDefinedItem.Tags = value;
|
||||
}
|
||||
|
||||
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
|
||||
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
|
||||
{
|
||||
get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book));
|
||||
|
||||
set
|
||||
{
|
||||
LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
|
||||
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public event EventHandler Committed;
|
||||
|
||||
private Book Book => LibraryBook.Book;
|
||||
private Image _cover;
|
||||
|
||||
public GridEntry(LibraryBook libraryBook)
|
||||
{
|
||||
@@ -145,53 +187,13 @@ namespace LibationWinForms
|
||||
Committed?.Invoke(this, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Model properties exposed to the view
|
||||
public Image Cover
|
||||
{
|
||||
get
|
||||
{
|
||||
return _cover;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_cover = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ProductRating { get; }
|
||||
public string PurchaseDate { get; }
|
||||
public string MyRating { get; }
|
||||
public string Series { get; }
|
||||
public string Title { get; }
|
||||
public string Length { get; }
|
||||
public string Authors { get; }
|
||||
public string Narrators { get; }
|
||||
public string Category { get; }
|
||||
public string Misc { get; }
|
||||
public string Description { get; }
|
||||
public string DisplayTags
|
||||
{
|
||||
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
set => Book.UserDefinedItem.Tags = value;
|
||||
}
|
||||
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
|
||||
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
|
||||
{
|
||||
get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book));
|
||||
|
||||
set
|
||||
{
|
||||
LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
|
||||
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Sorting
|
||||
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
||||
// Used by Dinah.Core.DataBinding.SortableBindingList<T> for all sorting
|
||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||
|
||||
private Dictionary<string, Func<object>> _memberValues { get; }
|
||||
|
||||
@@ -225,9 +227,6 @@ namespace LibationWinForms
|
||||
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
|
||||
};
|
||||
|
||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||
|
||||
private static readonly string[] _sortPrefixIgnores = { "the", "a", "an" };
|
||||
private static string GetSortName(string unformattedName)
|
||||
{
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -29,7 +28,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="2.0.1.2" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.0.4.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Liberate_Click(GridEntry liveGridEntry)
|
||||
private static async Task Liberate_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
var libraryBook = liveGridEntry.LibraryBook;
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace LibationWinForms
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
|
||||
}
|
||||
|
||||
private void Details_Click(GridEntry liveGridEntry)
|
||||
private static void Details_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
|
||||
|
||||
@@ -4,9 +4,9 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using InternalUtilities;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -30,9 +30,7 @@ namespace LibationWinForms
|
||||
//// Only use while debugging. Acts erratically in the wild
|
||||
//AllocConsole();
|
||||
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
@@ -140,9 +138,10 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
// INIT DEFAULT SETTINGS
|
||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
|
||||
config.InProgress ??= Configuration.WinTemp;
|
||||
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
|
||||
|
||||
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
|
||||
29
README.md
29
README.md
@@ -12,6 +12,8 @@
|
||||
- [The bad](#the-bad)
|
||||
- [The ugly](#the-ugly)
|
||||
2. [Getting started](#getting-started)
|
||||
- [Download Libation](#download-libation-1)
|
||||
- [Installation](#installation)
|
||||
- [Create Accounts](#create-accounts)
|
||||
- [Import your library](#import-your-library)
|
||||
- [Download your books -- DRM-free!](#download-your-books----drm-free)
|
||||
@@ -27,6 +29,7 @@
|
||||
- [Files and folders](#files-and-folders)
|
||||
- [Linux and Mac (unofficial)](#linux-and-mac)
|
||||
- [Settings](#settings)
|
||||
- [Custom File Naming](#custom-file-naming)
|
||||
- [Command Line Interface](#command-line-interface)
|
||||
|
||||
## Audible audiobook manager
|
||||
@@ -63,7 +66,11 @@ I made this for myself and I want to share it with the great programming and aud
|
||||
|
||||
## Getting started
|
||||
|
||||
#### [Download Libation](https://github.com/rmcrackan/Libation/releases)
|
||||
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
|
||||
|
||||
### Installation
|
||||
|
||||
To install Libation, extract the zip file to a folder, for example `C:\Libation`, and then run Libation.exe from that folder to begin the configuration process and configure your account(s).
|
||||
|
||||
### Create Accounts
|
||||
|
||||
@@ -149,7 +156,6 @@ When you set up Libation, you'll specify a Books directory. Libation looks insid
|
||||
|
||||
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
|
||||
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
|
||||
* .nfo: This is just some general info about the book and includes some technical stats about the audiofile.
|
||||
|
||||
### Export your library
|
||||
|
||||
@@ -237,12 +243,18 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
|
||||
|
||||
### Linux and Mac
|
||||
|
||||
Although Libation only currently officially supports Windows, [some users](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158) have had success with WINE.
|
||||
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592))
|
||||
|
||||
### Settings
|
||||
|
||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
||||
|
||||
### Custom File Naming
|
||||
|
||||
In Settings, on the Download/Decrypt tab, you can specify the format in which you want your files to be named. As you edit these templates, a live example will be shown. Parameters are listed for folders, files, and files split by chapter including an explanation of what each naming option means. For instance: you can use template `<title short> - <ch# 0> of <ch count> - <ch title>` to create the file `A Study in Scarlet - 04 of 10 - A Flight for Life.m4b`.
|
||||
|
||||
These templates apply to GUI and CLI.
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
|
||||
@@ -281,14 +293,3 @@ export library to file
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" -x
|
||||
```
|
||||
|
||||
Currently logs are written to Console and to file. This means they'll be printed in the CLI. To disable, find this in Settings.json and delete the 3 lines after `"WriteTo": [`
|
||||
|
||||
```
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information",
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console"
|
||||
},
|
||||
```
|
||||
|
||||
@@ -8,10 +8,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Common;
|
||||
using InternalUtilities;
|
||||
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
@@ -1,25 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
24
_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
Normal file
24
_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,24 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
<ProjectReference Include="..\..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,17 +7,17 @@ using FileManager;
|
||||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FileTemplateTests
|
||||
namespace FileNamingTemplateTests
|
||||
{
|
||||
[TestClass]
|
||||
public class GetFilename
|
||||
public class GetFilePath
|
||||
{
|
||||
[TestMethod]
|
||||
public void equiv_GetValidFilename()
|
||||
{
|
||||
var expected = @"C:\foo\bar\my_ book LONG_1234567890_1234567890_1234567890_123 [ID123456].txt";
|
||||
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
|
||||
var f2 = NEW_GetValidFilename_FileTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
|
||||
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
|
||||
|
||||
f1.Should().Be(expected);
|
||||
f1.Should().Be(f2);
|
||||
@@ -49,16 +49,16 @@ namespace FileTemplateTests
|
||||
|
||||
return fullfilename;
|
||||
}
|
||||
private static string NEW_GetValidFilename_FileTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
{
|
||||
var template = $"<title> [<id>]";
|
||||
|
||||
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
|
||||
|
||||
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
fileTemplate.AddParameterReplacement("title", filename);
|
||||
fileTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||
return fileTemplate.GetFilename();
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
|
||||
fileNamingTemplate.AddParameterReplacement("title", filename);
|
||||
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -66,7 +66,7 @@ namespace FileTemplateTests
|
||||
{
|
||||
var expected = @"C:\foo\bar\my file - 002 - title.txt";
|
||||
var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title");
|
||||
var f2 = NEW_GetMultipartFileName_FileTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
|
||||
var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
|
||||
|
||||
f1.Should().Be(expected);
|
||||
f1.Should().Be(f2);
|
||||
@@ -89,7 +89,7 @@ namespace FileTemplateTests
|
||||
var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
|
||||
return path;
|
||||
}
|
||||
private static string NEW_GetMultipartFileName_FileTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||
{
|
||||
// 1-9 => 1-9
|
||||
// 10-99 => 01-99
|
||||
@@ -98,11 +98,18 @@ namespace FileTemplateTests
|
||||
|
||||
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
|
||||
|
||||
var fileTemplate = new FileTemplate(t) { IllegalCharacterReplacements = " " };
|
||||
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileTemplate.AddParameterReplacement("title", suffix);
|
||||
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
|
||||
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
||||
return fileNamingTemplate.GetFilePath();
|
||||
}
|
||||
|
||||
return fileTemplate.GetFilename();
|
||||
[TestMethod]
|
||||
public void remove_slashes()
|
||||
{
|
||||
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
|
||||
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
||||
fileNamingTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace FileUtilityTests
|
||||
|
||||
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
|
||||
[TestMethod]
|
||||
[DataRow("http://test.com/a/b/c", @"http\\test.com\a\b\c")]
|
||||
[DataRow("http://test.com/a/b/c", @"http\test.com\a\b\c")]
|
||||
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
|
||||
|
||||
[TestMethod]
|
||||
@@ -31,8 +31,11 @@ namespace FileUtilityTests
|
||||
[DataRow("a*?:z.txt", "Z", "aZZZz.txt")]
|
||||
// retain drive letter path colon
|
||||
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
|
||||
// replace all other colongs
|
||||
// replace all other colons
|
||||
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
|
||||
// remove empty directories
|
||||
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
|
||||
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\ZZZfoo\ZZZidZZZ")]
|
||||
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
|
||||
}
|
||||
|
||||
@@ -55,76 +58,59 @@ namespace FileUtilityTests
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetValidFilename
|
||||
public class GetSequenceFormatted
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow(null, "name", "ext", "suffix")]
|
||||
[DataRow(@"C:\", null, "ext", "suffix")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void arg_null_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("", "name", "ext", "suffix")]
|
||||
[DataRow(" ", "name", "ext", "suffix")]
|
||||
[DataRow(@"C:\", "", "ext", "suffix")]
|
||||
[DataRow(@"C:\", " ", "ext", "suffix")]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void arg_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix);
|
||||
|
||||
[TestMethod]
|
||||
public void null_extension() => Tests(@"C:\foo\bar", "my file", null, "meta", @"C:\foo\bar\my file [meta]");
|
||||
[TestMethod]
|
||||
public void null_metadataSuffix() => Tests(@"C:\foo\bar", "my file", "txt", null, @"C:\foo\bar\my file [].txt");
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\foo\bar", "my file", "txt", "my id", @"C:\foo\bar\my file [my id].txt")]
|
||||
[DataRow(@"C:\foo\bar", "my file", "txt", "", @"C:\foo\bar\my file [].txt")]
|
||||
public void Tests(string dirFullPath, string filename, string extension, string metadataSuffix, string expected)
|
||||
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetMultipartFileName
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_path() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetMultipartFileName(null, 1, 1, ""));
|
||||
|
||||
[TestMethod]
|
||||
public void null_suffix() => Tests(@"C:\foo\bar\my file.txt", 2, 100, null, @"C:\foo\bar\my file - 002 - .txt");
|
||||
|
||||
[TestMethod]
|
||||
public void negative_partsPosition() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", -1, 2, "")
|
||||
=> FileUtility.GetSequenceFormatted(-1, 2)
|
||||
);
|
||||
[TestMethod]
|
||||
public void zero_partsPosition() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 0, 2, "")
|
||||
=> FileUtility.GetSequenceFormatted(0, 2)
|
||||
);
|
||||
|
||||
[TestMethod]
|
||||
public void negative_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 2, -1, "")
|
||||
=> FileUtility.GetSequenceFormatted(2, -1)
|
||||
);
|
||||
[TestMethod]
|
||||
public void zero_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 2, 0, "")
|
||||
=> FileUtility.GetSequenceFormatted(2, 0)
|
||||
);
|
||||
|
||||
[TestMethod]
|
||||
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 2, 1, "")
|
||||
=> FileUtility.GetSequenceFormatted(2, 1)
|
||||
);
|
||||
|
||||
[TestMethod]
|
||||
// only part
|
||||
[DataRow(@"C:\foo\bar\my file.txt", 1, 1, "title", @"C:\foo\bar\my file - 1 - title.txt")]
|
||||
[DataRow(1, 1, "1")]
|
||||
// 2 digits
|
||||
[DataRow(2, 90, "02")]
|
||||
// 3 digits
|
||||
[DataRow(@"C:\foo\bar\my file.txt", 2, 100, "title", @"C:\foo\bar\my file - 002 - title.txt")]
|
||||
// no suffix
|
||||
[DataRow(@"C:\foo\bar\my file.txt", 2, 100, "", @"C:\foo\bar\my file - 002 - .txt")]
|
||||
public void Tests(string originalPath, int partsPosition, int partsTotal, string suffix, string expected)
|
||||
=> FileUtility.GetMultipartFileName(originalPath, partsPosition, partsTotal, suffix).Should().Be(expected);
|
||||
[DataRow(2, 900, "002")]
|
||||
public void Tests(int partsPosition, int partsTotal, string expected)
|
||||
=> FileUtility.GetSequenceFormatted(partsPosition, partsTotal).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetStandardizedExtension
|
||||
{
|
||||
[TestMethod]
|
||||
public void is_null() => Tests(null, "");
|
||||
|
||||
[TestMethod]
|
||||
public void is_empty() => Tests("", "");
|
||||
|
||||
[TestMethod]
|
||||
public void is_whitespace() => Tests(" ", "");
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("txt", ".txt")]
|
||||
[DataRow(".txt", ".txt")]
|
||||
[DataRow(" .txt ", ".txt")]
|
||||
public void Tests(string input, string expected)
|
||||
=> FileUtility.GetStandardizedExtension(input).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LibationFileManager\LibationFileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
393
_Tests/LibationFileManager.Tests/TemplatesTests.cs
Normal file
393
_Tests/LibationFileManager.Tests/TemplatesTests.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using FluentAssertions;
|
||||
using LibationFileManager;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using static TemplatesTests.Shared;
|
||||
|
||||
namespace TemplatesTests
|
||||
{
|
||||
public static class Shared
|
||||
{
|
||||
public static LibraryBookDto GetLibraryBook(string asin, string seriesName = "Sherlock Holmes")
|
||||
=> new()
|
||||
{
|
||||
Account = "my account",
|
||||
AudibleProductId = asin,
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = seriesName ?? "",
|
||||
SeriesNumber = "1"
|
||||
};
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ContainsChapterOnlyTags
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow("<ch>", false)]
|
||||
[DataRow("<ch#>", true)]
|
||||
[DataRow("<id>", false)]
|
||||
[DataRow("<id><ch#>", true)]
|
||||
public void Tests(string template, bool expected) => Templates.ContainsChapterOnlyTags(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ContainsTag
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow("<ch#>", "ch#", true)]
|
||||
[DataRow("<id>", "ch#", false)]
|
||||
[DataRow("<id><ch#>", "ch#", true)]
|
||||
public void Tests(string template, string tag, bool expected) => Templates.ContainsTag(template, tag).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class getFileNamingTemplate
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow(null, "asin", @"C:\", "ext")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void arg_null_exception(string template, string asin, string dirFullPath, string extension)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("", "asin", @"C:\foo\bar", "ext")]
|
||||
[DataRow(" ", "asin", @"C:\foo\bar", "ext")]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void arg_exception(string template, string asin, string dirFullPath, string extension)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
|
||||
|
||||
[TestMethod]
|
||||
public void null_extension() => Tests("f.txt", "asin", @"C:\foo\bar", null, @"C:\foo\bar\f.txt");
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("f.txt", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext")]
|
||||
[DataRow("f", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext")]
|
||||
[DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")]
|
||||
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
|
||||
.GetFilePath()
|
||||
.Should().Be(expected);
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_empty()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.Should().Be(@"C:\a\b\foobar.ext");
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_no_series()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.Should().Be(@"C:\a\b\foobar.ext");
|
||||
|
||||
[TestMethod]
|
||||
public void IfSeries_with_series()
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
|
||||
.GetFilePath()
|
||||
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
|
||||
}
|
||||
}
|
||||
|
||||
namespace Templates_Folder_Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class GetErrors
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
||||
|
||||
[TestMethod]
|
||||
public void empty_is_valid() => valid_tests("");
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_is_valid() => valid_tests(" ");
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"foo")]
|
||||
[DataRow(@"\foo")]
|
||||
[DataRow(@"foo\")]
|
||||
[DataRow(@"\foo\")]
|
||||
[DataRow(@"foo\bar")]
|
||||
[DataRow(@"<id>")]
|
||||
[DataRow(@"<id>\<title>")]
|
||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\", Templates.ERROR_FULL_PATH_IS_INVALID)]
|
||||
public void Tests(string template, params string[] expected)
|
||||
{
|
||||
var result = Templates.Folder.GetErrors(template);
|
||||
result.Count().Should().Be(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class IsValid
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, false);
|
||||
|
||||
[TestMethod]
|
||||
public void empty_is_valid() => Tests("", true);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_is_valid() => Tests(" ", true);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\", false)]
|
||||
[DataRow(@"foo", true)]
|
||||
[DataRow(@"\foo", true)]
|
||||
[DataRow(@"foo\", true)]
|
||||
[DataRow(@"\foo\", true)]
|
||||
[DataRow(@"foo\bar", true)]
|
||||
[DataRow(@"<id>", true)]
|
||||
[DataRow(@"<id>\<title>", true)]
|
||||
public void Tests(string template, bool expected) => Templates.Folder.IsValid(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetWarnings
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
||||
|
||||
[TestMethod]
|
||||
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"<id>\foo\bar")]
|
||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"no tags", Templates.WARNING_NO_TAGS)]
|
||||
[DataRow("<ch#> <id>", Templates.WARNING_HAS_CHAPTER_TAGS)]
|
||||
[DataRow("<ch#> chapter tag", Templates.WARNING_NO_TAGS, Templates.WARNING_HAS_CHAPTER_TAGS)]
|
||||
public void Tests(string template, params string[] expected)
|
||||
{
|
||||
var result = Templates.Folder.GetWarnings(template);
|
||||
result.Count().Should().Be(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class HasWarnings
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_has_warnings() => Tests(null, true);
|
||||
|
||||
[TestMethod]
|
||||
public void empty_has_warnings() => Tests("", true);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_has_warnings() => Tests(" ", true);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"no tags", true)]
|
||||
[DataRow(@"<id>\foo\bar", false)]
|
||||
[DataRow("<ch#> <id>", true)]
|
||||
[DataRow("<ch#> chapter tag", true)]
|
||||
public void Tests(string template, bool expected) => Templates.Folder.HasWarnings(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class TagCount
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_throws() => Assert.ThrowsException<NullReferenceException>(() => Templates.Folder.TagCount(null));
|
||||
|
||||
[TestMethod]
|
||||
public void empty() => Tests("", 0);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace() => Tests(" ", 0);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("no tags", 0)]
|
||||
[DataRow(@"<id>\foo\bar", 1)]
|
||||
[DataRow("<id> <id>", 2)]
|
||||
[DataRow("<id <id> >", 1)]
|
||||
[DataRow("<id> <title>", 2)]
|
||||
[DataRow("id> <title incomplete tags", 0)]
|
||||
[DataRow("<not a real tag>", 0)]
|
||||
[DataRow("<ch#> non-folder tag", 0)]
|
||||
[DataRow("<ID> case specific", 0)]
|
||||
public void Tests(string template, int expected) => Templates.Folder.TagCount(template).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Templates_File_Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class GetErrors
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
||||
|
||||
[TestMethod]
|
||||
public void empty_is_valid() => valid_tests("");
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_is_valid() => valid_tests(" ");
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"foo")]
|
||||
[DataRow(@"<id>")]
|
||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
||||
[DataRow(@"\foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
||||
[DataRow(@"/foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
||||
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
||||
public void Tests(string template, params string[] expected)
|
||||
{
|
||||
var result = Templates.File.GetErrors(template);
|
||||
result.Count().Should().Be(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class IsValid
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, false);
|
||||
|
||||
[TestMethod]
|
||||
public void empty_is_valid() => Tests("", true);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_is_valid() => Tests(" ", true);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\", false)]
|
||||
[DataRow(@"foo", true)]
|
||||
[DataRow(@"\foo", false)]
|
||||
[DataRow(@"/foo", false)]
|
||||
[DataRow(@"<id>", true)]
|
||||
public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
// same as Templates.Folder.GetWarnings
|
||||
//[TestClass]
|
||||
//public class GetWarnings { }
|
||||
|
||||
// same as Templates.Folder.HasWarnings
|
||||
//[TestClass]
|
||||
//public class HasWarnings { }
|
||||
|
||||
// same as Templates.Folder.TagCount
|
||||
//[TestClass]
|
||||
//public class TagCount { }
|
||||
}
|
||||
|
||||
namespace Templates_ChapterFile_Tests
|
||||
{
|
||||
// same as Templates.File.GetErrors
|
||||
//[TestClass]
|
||||
//public class GetErrors { }
|
||||
|
||||
// same as Templates.File.IsValid
|
||||
//[TestClass]
|
||||
//public class IsValid { }
|
||||
|
||||
[TestClass]
|
||||
public class GetWarnings
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
||||
|
||||
[TestMethod]
|
||||
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<ch#>")]
|
||||
[DataRow("<ch#> <id>")]
|
||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"no tags", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||
[DataRow(@"<id>\foo\bar", Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||
public void Tests(string template, params string[] expected)
|
||||
{
|
||||
var result = Templates.ChapterFile.GetWarnings(template);
|
||||
result.Count().Should().Be(expected.Length);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class HasWarnings
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_has_warnings() => Tests(null, true);
|
||||
|
||||
[TestMethod]
|
||||
public void empty_has_warnings() => Tests("", true);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_has_warnings() => Tests(" ", true);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"no tags", true)]
|
||||
[DataRow(@"<id>\foo\bar", true)]
|
||||
[DataRow("<ch#> <id>", false)]
|
||||
[DataRow("<ch#> -- chapter tag", false)]
|
||||
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)]
|
||||
public void Tests(string template, bool expected) => Templates.ChapterFile.HasWarnings(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class TagCount
|
||||
{
|
||||
[TestMethod]
|
||||
public void null_is_not_recommended() => Assert.ThrowsException<NullReferenceException>(() => Tests(null, -1));
|
||||
|
||||
[TestMethod]
|
||||
public void empty_is_not_recommended() => Tests("", 0);
|
||||
|
||||
[TestMethod]
|
||||
public void whitespace_is_not_recommended() => Tests(" ", 0);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("no tags", 0)]
|
||||
[DataRow(@"<id>\foo\bar", 1)]
|
||||
[DataRow("<id> <id>", 2)]
|
||||
[DataRow("<id <id> >", 1)]
|
||||
[DataRow("<id> <title>", 2)]
|
||||
[DataRow("id> <title incomplete tags", 0)]
|
||||
[DataRow("<not a real tag>", 0)]
|
||||
[DataRow("<ch#> non-folder tag", 1)]
|
||||
[DataRow("<ID> case specific", 0)]
|
||||
public void Tests(string template, int expected) => Templates.ChapterFile.TagCount(template).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class GetPortionFilename
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow("asin", "[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")]
|
||||
[DataRow("asin", "<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")]
|
||||
public void Tests(string asin, string template, string dir, string ext, int pos, int total, string chapter, string expected)
|
||||
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -24,7 +24,7 @@ STRUCTURE
|
||||
* 2 Utilities (domain ignorant)
|
||||
Stand-alone libraries with no knowledge of anything having to do with Libation or other programs. In theory any of these should be able to one day be converted to a nuget pkg
|
||||
* 3 Domain Internal Utilities (db ignorant)
|
||||
Can have knowledge of Libation concepts. Cannot access the database.
|
||||
Cannot access the database. Can have knowledge of Libation concepts. Can even have knowledge of some db concepts, but no actual db access.
|
||||
* 4 Domain (db)
|
||||
All database access
|
||||
* 5 Domain Utilities (db aware)
|
||||
|
||||
Reference in New Issue
Block a user