mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 10:58:43 -05:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e2d78b9b | ||
|
|
6b711190c3 | ||
|
|
b4a6342513 | ||
|
|
988b137d67 | ||
|
|
dae9c9c9b6 | ||
|
|
420b7529c6 | ||
|
|
4cf999c84d | ||
|
|
8fe3896d76 | ||
|
|
adcba34560 | ||
|
|
8e09d7e617 | ||
|
|
197b50e3ac | ||
|
|
ac2114e270 | ||
|
|
29461701cd | ||
|
|
0f130c70f5 | ||
|
|
995637e843 | ||
|
|
9501687f86 | ||
|
|
248dea3402 | ||
|
|
1d420f5430 | ||
|
|
5f0a6b8526 | ||
|
|
c337c0b44e | ||
|
|
89207866f3 | ||
|
|
9e11086d49 | ||
|
|
58b172f816 | ||
|
|
0b8084bc03 | ||
|
|
37970222f3 | ||
|
|
bcab2dd440 | ||
|
|
d402128d1d | ||
|
|
3ae0f2daa2 | ||
|
|
126919d578 | ||
|
|
437e85fd12 | ||
|
|
de34e5c795 | ||
|
|
8ffcefd6ae | ||
|
|
e59ab9b483 | ||
|
|
57fa1bd763 | ||
|
|
dccb2d73d6 | ||
|
|
77fc865636 | ||
|
|
1040a347c6 | ||
|
|
6ed1307443 | ||
|
|
c2c732b2b1 | ||
|
|
9e0caf34d6 | ||
|
|
802763a4fb | ||
|
|
b4803c42a5 | ||
|
|
62c98c66a3 | ||
|
|
6b289445e2 | ||
|
|
52bf91f8aa | ||
|
|
6d2dff1a98 | ||
|
|
7c9970c0cb | ||
|
|
d2892f9076 | ||
|
|
89f60a7ca3 | ||
|
|
ea37c09081 | ||
|
|
76cb280933 | ||
|
|
0a54a8104c | ||
|
|
7464336535 | ||
|
|
dc0dd3474b | ||
|
|
7b9c5c0f4f | ||
|
|
ad87f1851e | ||
|
|
e8423341ef | ||
|
|
a9d3494af1 | ||
|
|
90731a8948 | ||
|
|
e723467ca6 | ||
|
|
722c33bf61 | ||
|
|
f080215cbb | ||
|
|
d5c74d629f | ||
|
|
d12c246f6d | ||
|
|
8969c216af | ||
|
|
9a4903f0dd | ||
|
|
3eda498a5e | ||
|
|
8af7f28f04 | ||
|
|
d9d7dfe1f7 | ||
|
|
b9c4d11946 | ||
|
|
68a5d7a58d | ||
|
|
4d69b222c5 | ||
|
|
42f94e7f6c | ||
|
|
381d52be72 | ||
|
|
f16ad30891 | ||
|
|
ef53a6a8cb | ||
|
|
9a37d434f1 | ||
|
|
d7eb190f69 | ||
|
|
f19c46ee45 | ||
|
|
343c3b62d6 | ||
|
|
b1de10a71a | ||
|
|
6beb5cc74a | ||
|
|
3767c3574a | ||
|
|
4ceb4f9c03 | ||
|
|
0f5149f7b4 | ||
|
|
673451dc11 | ||
|
|
e4257afc14 | ||
|
|
2a7e185dc3 | ||
|
|
9e06c343c1 | ||
|
|
40b3a9990d | ||
|
|
d66c112a1e | ||
|
|
d826885728 | ||
|
|
263222d8cc | ||
|
|
f25734334d | ||
|
|
ede8397f13 | ||
|
|
1369ee575a | ||
|
|
c8e2418af7 | ||
|
|
2da25edafd | ||
|
|
f60964f4c7 | ||
|
|
3183f99153 | ||
|
|
2a22cff67c | ||
|
|
7fbe8ae769 | ||
|
|
f9df466ad8 | ||
|
|
0b129fcf7c | ||
|
|
2be5fd5af3 | ||
|
|
c9727f84ab | ||
|
|
aa56bb74a1 | ||
|
|
85a6e21dcf | ||
|
|
8c620c25ab | ||
|
|
813d91dfa4 | ||
|
|
d0d66c6135 | ||
|
|
a8d609676e | ||
|
|
8386da5ec6 | ||
|
|
f5089e7e29 | ||
|
|
a639857ec6 | ||
|
|
35b5d7370c | ||
|
|
c9f988acf8 | ||
|
|
6dfef09ea3 | ||
|
|
7e288c0c08 | ||
|
|
dbcf6f25db | ||
|
|
0cc55fd1e8 | ||
|
|
e36ea70cd1 | ||
|
|
a86185e644 | ||
|
|
64a8f007a5 | ||
|
|
215a626c92 | ||
|
|
de93047192 |
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
<PackageReference Include="AAXClean" Version="0.1.9" />
|
||||
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -83,8 +83,7 @@ namespace AaxDecrypter
|
||||
}
|
||||
|
||||
var speedup = (int)(aaxFile.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
||||
Console.WriteLine("Done");
|
||||
Serilog.Log.Logger.Information($"Speedup is {speedup}x realtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -145,7 +144,7 @@ namespace AaxDecrypter
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
|
||||
FileStream outFile = File.OpenWrite(outputFileName);
|
||||
FileStream outFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
|
||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
||||
|
||||
@@ -224,6 +223,8 @@ namespace AaxDecrypter
|
||||
isCanceled = true;
|
||||
aaxFile?.Cancel();
|
||||
aaxFile?.Dispose();
|
||||
nfsPersister?.NetworkFileStream?.Close();
|
||||
nfsPersister?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,9 +241,12 @@ namespace AaxDecrypter
|
||||
} while (downloadPosition < ContentLength && !isCancelled);
|
||||
|
||||
_writeFile.Close();
|
||||
_networkStream.Close();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
_networkStream.Close();
|
||||
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
|
||||
if (!isCancelled && WritePosition < ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
@@ -251,7 +254,6 @@ namespace AaxDecrypter
|
||||
if (WritePosition > ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
|
||||
downloadEnded.Set();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
21
AppScaffolding/AppScaffolding.csproj
Normal file
21
AppScaffolding/AppScaffolding.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>5.7.5.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSBump" Version="2.3.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
333
AppScaffolding/LibationScaffolding.cs
Normal file
333
AppScaffolding/LibationScaffolding.cs
Normal file
@@ -0,0 +1,333 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
private static Assembly ExecutingAssembly
|
||||
=> _executingAssembly ??= Assembly.GetExecutingAssembly();
|
||||
|
||||
// LibationWinForms or LibationCli
|
||||
private static Assembly _entryAssembly;
|
||||
private static Assembly EntryAssembly
|
||||
=> _entryAssembly ??= Assembly.GetEntryAssembly();
|
||||
|
||||
// previously: System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
private static Version _buildVersion;
|
||||
public static Version BuildVersion
|
||||
=> _buildVersion
|
||||
??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() }
|
||||
.Max(a => a.Version);
|
||||
|
||||
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
|
||||
public static Configuration RunPreConfigMigrations()
|
||||
{
|
||||
// must occur before access to Configuration instance
|
||||
Migrations.migrate_to_v5_2_0__pre_config();
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
return Configuration.Instance;
|
||||
}
|
||||
|
||||
public static void RunPostConfigMigrations()
|
||||
{
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
//
|
||||
// migrations go below here
|
||||
//
|
||||
|
||||
Migrations.migrate_to_v5_2_0__post_config(config);
|
||||
Migrations.migrate_to_v5_7_1(config);
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
}
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
{
|
||||
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", "File" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
// for this sink to work, a path must be provided. we override this below
|
||||
{ "path", Path.Combine(config.LibationFiles, "_Log.log") },
|
||||
{ "rollingInterval", "Month" },
|
||||
// Serilog template formatting examples
|
||||
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||
// 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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller" } },
|
||||
};
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
}
|
||||
|
||||
// to restore original: Console.SetOut(origOut);
|
||||
private static TextWriter origOut { get; } = Console.Out;
|
||||
|
||||
private static void configureLogging(Configuration config)
|
||||
{
|
||||
config.ConfigureLogging();
|
||||
|
||||
// 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.
|
||||
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
|
||||
|
||||
#region Console => Serilog tests
|
||||
/*
|
||||
// all below apply to "Console." and "Console.Out."
|
||||
|
||||
// captured
|
||||
Console.WriteLine("str");
|
||||
Console.WriteLine(new { a = "anon" });
|
||||
Console.WriteLine("{0}", "format");
|
||||
Console.WriteLine("{0}{1}", "zero|", "one");
|
||||
Console.WriteLine("{0}{1}{2}", "zero|", "one|", "two");
|
||||
Console.WriteLine("{0}", new object[] { "arr" });
|
||||
|
||||
// not captured
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(true);
|
||||
Console.WriteLine('0');
|
||||
Console.WriteLine(1);
|
||||
Console.WriteLine(2m);
|
||||
Console.WriteLine(3f);
|
||||
Console.WriteLine(4d);
|
||||
Console.WriteLine(5L);
|
||||
Console.WriteLine((uint)6);
|
||||
Console.WriteLine((ulong)7);
|
||||
|
||||
Console.Write("str");
|
||||
Console.Write(true);
|
||||
Console.Write('0');
|
||||
Console.Write(1);
|
||||
Console.Write(2m);
|
||||
Console.Write(3f);
|
||||
Console.Write(4d);
|
||||
Console.Write(5L);
|
||||
Console.Write((uint)6);
|
||||
Console.Write((ulong)7);
|
||||
Console.Write(new { a = "anon" });
|
||||
Console.Write("{0}", "format");
|
||||
Console.Write("{0}{1}", "zero|", "one");
|
||||
Console.Write("{0}{1}{2}", "zero|", "one|", "two");
|
||||
Console.Write("{0}", new object[] { "arr" });
|
||||
*/
|
||||
#endregion
|
||||
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||
{
|
||||
AppName = EntryAssembly.GetName().Name,
|
||||
Version = BuildVersion.ToString(),
|
||||
#if DEBUG
|
||||
Mode = "Debug",
|
||||
#else
|
||||
Mode = "Release",
|
||||
#endif
|
||||
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
|
||||
config.InProgress,
|
||||
|
||||
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
|
||||
|
||||
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
||||
});
|
||||
}
|
||||
|
||||
public static (bool hasUpgrade, string zipUrl, string htmlUrl, string zipName) GetLatestRelease()
|
||||
{
|
||||
(bool, string, string, string) isFalse = (false, null, null, null);
|
||||
|
||||
// timed out
|
||||
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
if (latest is null)
|
||||
return isFalse;
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
if (!Version.TryParse(latestVersionString, out var latestRelease))
|
||||
return isFalse;
|
||||
|
||||
// we're up to date
|
||||
if (latestRelease <= BuildVersion)
|
||||
return isFalse;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
var zipUrl = zip?.BrowserDownloadUrl;
|
||||
|
||||
Log.Logger.Information("Update available: {@DebugInfo}", new
|
||||
{
|
||||
latestRelease = latestRelease.ToString(),
|
||||
latest.HtmlUrl,
|
||||
zipUrl
|
||||
});
|
||||
|
||||
return (true, zipUrl, latest.HtmlUrl, zip.Name);
|
||||
}
|
||||
private static Octokit.Release getLatestRelease(TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
|
||||
if (task.Wait(timeout))
|
||||
return task.Result;
|
||||
|
||||
Log.Logger.Information("Timed out");
|
||||
}
|
||||
catch (AggregateException aggEx)
|
||||
{
|
||||
Log.Logger.Error(aggEx, "Checking for new version too often");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static Octokit.Release getLatestRelease()
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Migrations
|
||||
{
|
||||
#region migrate to v5.2.0
|
||||
// get rid of meta-directories, combine DownloadsInProgressEnum and DecryptInProgressEnum => InProgress
|
||||
public static void migrate_to_v5_2_0__pre_config()
|
||||
{
|
||||
{
|
||||
var settingsKey = "DownloadsInProgressEnum";
|
||||
if (UNSAFE_MigrationHelper.Settings_TryGet(settingsKey, out var value))
|
||||
{
|
||||
UNSAFE_MigrationHelper.Settings_Delete(settingsKey);
|
||||
UNSAFE_MigrationHelper.Settings_Insert("InProgress", translatePath(value));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
UNSAFE_MigrationHelper.Settings_Delete("DecryptInProgressEnum");
|
||||
}
|
||||
|
||||
{ // appsettings.json
|
||||
var appSettingsKey = UNSAFE_MigrationHelper.LIBATION_FILES_KEY;
|
||||
if (UNSAFE_MigrationHelper.APPSETTINGS_TryGet(appSettingsKey, out var value))
|
||||
UNSAFE_MigrationHelper.APPSETTINGS_Update(appSettingsKey, translatePath(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static string translatePath(string path)
|
||||
=> path switch
|
||||
{
|
||||
"AppDir" => @".\LibationFiles",
|
||||
"MyDocs" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LibationFiles")),
|
||||
"UserProfile" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")),
|
||||
"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)
|
||||
{
|
||||
if (!config.Exists(nameof(config.BadBook)))
|
||||
config.BadBook = Configuration.BadBookAction.Ask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,21 @@ using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace LibationLauncher
|
||||
namespace AppScaffolding
|
||||
{
|
||||
/// <summary>for migrations only. directly manipulatings settings files without going through domain logic</summary>
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// directly manipulates settings files without going through domain logic.
|
||||
///
|
||||
/// for migrations only. use with caution.
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
internal static class UNSAFE_MigrationHelper
|
||||
{
|
||||
#region appsettings.json
|
||||
private const string APPSETTINGS_JSON = "appsettings.json";
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
|
||||
|
||||
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
|
||||
|
||||
@@ -6,14 +6,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="27.1.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.3" />
|
||||
<PackageReference Include="NPOI" Version="2.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
|
||||
@@ -6,11 +7,15 @@ namespace ApplicationServices
|
||||
{
|
||||
public static class DbContexts
|
||||
{
|
||||
//// idea for future command/query separation
|
||||
// public static LibationContext GetCommandContext() { }
|
||||
// public static LibationContext GetQueryContext() { }
|
||||
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
public static LibationContext GetContext()
|
||||
=> LibationContext.Create(SqliteStorage.ConnectionString);
|
||||
|
||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibrary_Flat_NoTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,23 @@ using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
// subtly different from DataLayer.LiberatedStatus
|
||||
// - DataLayer.LiberatedStatus: has no concept of partially downloaded
|
||||
// - ApplicationServices.LiberatedState: has no concept of Error/skipped
|
||||
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
|
||||
|
||||
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
|
||||
|
||||
public static class LibraryCommands
|
||||
{
|
||||
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
||||
|
||||
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, ILoginCallback> loginCallbackFactoryFunc, List<LibraryBook> existingLibrary, params Account[] accounts)
|
||||
{
|
||||
{
|
||||
logRestart();
|
||||
|
||||
//These are the minimum response groups required for the
|
||||
//library scanner to pass all validation and filtering.
|
||||
LibraryResponseGroups =
|
||||
LibraryResponseGroups =
|
||||
LibraryOptions.ResponseGroupOptions.ProductAttrs |
|
||||
LibraryOptions.ResponseGroupOptions.ProductDesc |
|
||||
LibraryOptions.ResponseGroupOptions.ProductDesc |
|
||||
LibraryOptions.ResponseGroupOptions.Relationships;
|
||||
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
@@ -36,8 +32,12 @@ namespace ApplicationServices
|
||||
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {libraryItems.Count}");
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = libraryItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
|
||||
var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
|
||||
|
||||
@@ -63,32 +63,39 @@ namespace ApplicationServices
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error importing library");
|
||||
Log.Logger.Error(ex, "Error scanning library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
finally
|
||||
{
|
||||
LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
||||
}
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
}
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var importItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = importItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
Log.Logger.Information($"Import: New count {newCount}");
|
||||
|
||||
await Task.Run(() => SearchEngineCommands.FullReIndex());
|
||||
Log.Logger.Information("FullReIndex: success");
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
@@ -115,6 +122,11 @@ namespace ApplicationServices
|
||||
Log.Logger.Error(ex, "Error importing library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, Account[] accounts)
|
||||
@@ -146,142 +158,131 @@ namespace ApplicationServices
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api, LibraryResponseGroups);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
{
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryImporter = new LibraryImporter(context);
|
||||
var libraryImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
|
||||
context.SaveChanges();
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
var qtyChanges = context.SaveChanges();
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
|
||||
if (qtyChanges > 0)
|
||||
await Task.Run(() => finalizeLibrarySizeChange());
|
||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||
|
||||
return newCount;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region remove books
|
||||
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
private static List<LibraryBook> removeBooks(List<string> idsToRemove)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libBooks = context.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
|
||||
context.LibraryBooks.RemoveRange(removeLibraryBooks);
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
finalizeLibrarySizeChange();
|
||||
|
||||
return removeLibraryBooks;
|
||||
}
|
||||
#endregion
|
||||
|
||||
// call this whenever books are added or removed from library
|
||||
private static void finalizeLibrarySizeChange()
|
||||
{
|
||||
SearchEngineCommands.FullReIndex();
|
||||
LibrarySizeChanged?.Invoke(null, null);
|
||||
}
|
||||
|
||||
/// <summary>Occurs when books are added or removed from library</summary>
|
||||
public static event EventHandler LibrarySizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/>
|
||||
/// changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string> BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateTags(Book book, string newTags)
|
||||
public static int UpdateUserDefinedItem(Book book)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = book.UserDefinedItem;
|
||||
|
||||
if (udi.Tags == newTags)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.Tags = newTags;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineCommands.UpdateBookTags(book);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error updating tags");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus, string finalAudioPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = libraryBook.Book.UserDefinedItem;
|
||||
|
||||
if (udi.BookStatus == liberatedStatus && udi.BookLocation == finalAudioPath)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.BookStatus = liberatedStatus;
|
||||
udi.BookLocation = finalAudioPath;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
|
||||
{
|
||||
SearchEngineCommands.UpdateLiberatedStatus(book);
|
||||
BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
|
||||
}
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error updating tags");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var udi = libraryBook.Book.UserDefinedItem;
|
||||
|
||||
if (udi.PdfStatus == liberatedStatus)
|
||||
return 0;
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
udi.PdfStatus = liberatedStatus;
|
||||
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error updating tags");
|
||||
Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists ? book.UserDefinedItem.BookStatus
|
||||
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
|
||||
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public static LiberatedState Liberated_Status(Book book)
|
||||
=> TransitionalFileLocator.Audio_Exists(book) ? LiberatedState.Liberated
|
||||
: TransitionalFileLocator.AAXC_Exists(book) ? LiberatedState.PartialDownload
|
||||
: LiberatedState.NotDownloaded;
|
||||
|
||||
public static PdfState Pdf_Status(Book book)
|
||||
=> !book.Supplements.Any() ? PdfState.NoPdf
|
||||
: TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded
|
||||
: PdfState.NotDownloaded;
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
|
||||
public static LibraryStats GetCounts()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var results = libraryBooks
|
||||
.AsParallel()
|
||||
.Select(lb => Liberated_Status(lb.Book))
|
||||
.ToList();
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedState.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedState.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedState.NotDownloaded);
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
var booksError = results.Count(r => r == LiberatedStatus.Error);
|
||||
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
|
||||
|
||||
var boolResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.Supplements.Any())
|
||||
.Where(lb => lb.Book.HasPdf)
|
||||
.Select(lb => Pdf_Status(lb.Book))
|
||||
.ToList();
|
||||
var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded);
|
||||
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, pdfsDownloaded, pdfsNotDownloaded);
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace ApplicationServices
|
||||
[Name("Publisher")]
|
||||
public string Publisher { get; set; }
|
||||
|
||||
[Name("Pdf url")]
|
||||
public string PdfUrl { get; set; }
|
||||
[Name("Has PDF")]
|
||||
public bool HasPdf { get; set; }
|
||||
|
||||
[Name("Series Names")]
|
||||
public string SeriesNames { get; set; }
|
||||
@@ -89,14 +89,14 @@ namespace ApplicationServices
|
||||
[Name("My Libation Tags")]
|
||||
public string MyLibationTags { get; set; }
|
||||
|
||||
[Name("Book Liberation Status")]
|
||||
[Name("Book Liberated Status")]
|
||||
public string BookStatus { get; set; }
|
||||
|
||||
[Name("Book File Location")]
|
||||
public string BookLocation { get; set; }
|
||||
|
||||
[Name("PDF Liberation Status")]
|
||||
[Name("PDF Liberated Status")]
|
||||
public string PdfStatus { get; set; }
|
||||
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
@@ -112,7 +112,7 @@ namespace ApplicationServices
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Publisher = a.Book.Publisher,
|
||||
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
|
||||
HasPdf = a.Book.HasPdf,
|
||||
SeriesNames = a.Book.SeriesNames,
|
||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||
CommunityRatingOverall = a.Book.Rating?.OverallRating,
|
||||
@@ -127,17 +127,15 @@ namespace ApplicationServices
|
||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
BookLocation = a.Book.UserDefinedItem.BookLocation,
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString()
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
{
|
||||
public static void ToCsv(string saveFilePath)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
|
||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
if (!dtos.Any())
|
||||
return;
|
||||
|
||||
@@ -151,17 +149,14 @@ namespace ApplicationServices
|
||||
|
||||
public static void ToJson(string saveFilePath)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
|
||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
|
||||
System.IO.File.WriteAllText(saveFilePath, json);
|
||||
}
|
||||
|
||||
public static void ToXlsx(string saveFilePath)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
|
||||
var workbook = new XSSFWorkbook();
|
||||
var sheet = workbook.CreateSheet("Library");
|
||||
@@ -186,7 +181,7 @@ namespace ApplicationServices
|
||||
nameof (ExportDto.NarratorNames),
|
||||
nameof (ExportDto.LengthInMinutes),
|
||||
nameof (ExportDto.Publisher),
|
||||
nameof (ExportDto.PdfUrl),
|
||||
nameof (ExportDto.HasPdf),
|
||||
nameof (ExportDto.SeriesNames),
|
||||
nameof (ExportDto.SeriesOrder),
|
||||
nameof (ExportDto.CommunityRatingOverall),
|
||||
@@ -201,8 +196,8 @@ namespace ApplicationServices
|
||||
nameof (ExportDto.MyRatingStory),
|
||||
nameof (ExportDto.MyLibationTags),
|
||||
nameof (ExportDto.BookStatus),
|
||||
nameof (ExportDto.BookLocation),
|
||||
nameof (ExportDto.PdfStatus)
|
||||
nameof (ExportDto.PdfStatus),
|
||||
nameof (ExportDto.ContentType)
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
@@ -239,7 +234,7 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
|
||||
row.CreateCell(col++).SetCellValue(dto.Publisher);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfUrl);
|
||||
row.CreateCell(col++).SetCellValue(dto.HasPdf);
|
||||
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
|
||||
|
||||
@@ -265,8 +260,8 @@ namespace ApplicationServices
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
|
||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.BookLocation);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace ApplicationServices
|
||||
public static void FullReIndex(SearchEngine engine = null)
|
||||
{
|
||||
engine ??= new SearchEngine();
|
||||
engine.CreateNewIndex(DbContexts.GetContext());
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
engine.CreateNewIndex(library);
|
||||
}
|
||||
|
||||
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
|
||||
@@ -39,17 +40,17 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> action)
|
||||
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
return action(engine);
|
||||
return func(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex(engine);
|
||||
return action(engine);
|
||||
return func(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class TransitionalFileLocator
|
||||
{
|
||||
public static string Audio_GetPath(Book book)
|
||||
{
|
||||
var loc = book?.UserDefinedItem?.BookLocation ?? "";
|
||||
if (File.Exists(loc))
|
||||
return loc;
|
||||
|
||||
return AudibleFileStorage.Audio.GetPath(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool PDF_Exists(Book book)
|
||||
{
|
||||
var status = book?.UserDefinedItem?.PdfStatus;
|
||||
if (status.HasValue && status.Value == LiberatedStatus.Liberated)
|
||||
return true;
|
||||
|
||||
return AudibleFileStorage.PDF.Exists(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool Audio_Exists(Book book)
|
||||
{
|
||||
var status = book?.UserDefinedItem?.BookStatus;
|
||||
// true since Error == libhack
|
||||
if (status.HasValue && status.Value != LiberatedStatus.NotLiberated)
|
||||
return true;
|
||||
|
||||
return AudibleFileStorage.Audio.Exists(book.AudibleProductId);
|
||||
}
|
||||
|
||||
public static bool AAXC_Exists(Book book)
|
||||
{
|
||||
// this one will actually stay the same. centralizing helps with organization in the interim though
|
||||
return AudibleFileStorage.AAXC.Exists(book.AudibleProductId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,20 @@ namespace DataLayer.Configurations
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<LibraryBook> entity)
|
||||
{
|
||||
// to allow same book (incl region) with diff acct.s:
|
||||
//
|
||||
// this file:
|
||||
// - composite key:
|
||||
// entity.HasKey(b => new { b.BookId, b.Account });
|
||||
// entity.HasIndex(b => b.BookId);
|
||||
// entity.HasIndex(b => b.Account);
|
||||
// - change the below relationship since Book+LibraryBook would no longer be 1:1
|
||||
//
|
||||
// other files:
|
||||
// - change Book class since Book+LibraryBook would no longer be 1:1
|
||||
// - update LibraryBook import code
|
||||
// - would likely challenge assumptions throughout Libation which have been true up until now
|
||||
|
||||
entity.HasKey(b => b.BookId);
|
||||
|
||||
entity
|
||||
|
||||
@@ -12,19 +12,19 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5">
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.5.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace DataLayer
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// enum will be easier than bool to extend later
|
||||
public enum ContentType { Unknown = 0, Product = 1, Episode = 2 }
|
||||
|
||||
public class Book
|
||||
{
|
||||
// implementation detail. set by db only. only used by data layer
|
||||
@@ -25,8 +29,7 @@ namespace DataLayer
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public int LengthInMinutes { get; private set; }
|
||||
|
||||
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
|
||||
public ContentType ContentType { get; private set; }
|
||||
public string Locale { get; private set; }
|
||||
|
||||
// mutable
|
||||
@@ -51,6 +54,12 @@ namespace DataLayer
|
||||
// is owned, not optional 1:1
|
||||
public UserDefinedItem UserDefinedItem { get; private set; }
|
||||
|
||||
// UserDefinedItem convenience properties
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool Audio_Exists => UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PDF_Exists => UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
|
||||
|
||||
// is owned, not optional 1:1
|
||||
/// <summary>The product's aggregate community rating</summary>
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
@@ -64,9 +73,11 @@ namespace DataLayer
|
||||
string title,
|
||||
string description,
|
||||
int lengthInMinutes,
|
||||
ContentType contentType,
|
||||
IEnumerable<Contributor> authors,
|
||||
IEnumerable<Contributor> narrators,
|
||||
Category category, string localeName)
|
||||
Category category,
|
||||
string localeName)
|
||||
{
|
||||
// validate
|
||||
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||
@@ -89,8 +100,9 @@ namespace DataLayer
|
||||
|
||||
// simple assigns
|
||||
Title = title.Trim();
|
||||
Description = description.Trim();
|
||||
Description = description?.Trim();
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
ContentType = contentType;
|
||||
|
||||
// assigns with biz logic
|
||||
ReplaceAuthors(authors);
|
||||
@@ -247,10 +259,6 @@ namespace DataLayer
|
||||
Category = category;
|
||||
}
|
||||
|
||||
// needed for v3 => v4 upgrade
|
||||
public void UpdateLocale(string localeName)
|
||||
=> Locale ??= localeName;
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {Title}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace DataLayer
|
||||
public Book Book { get; private set; }
|
||||
|
||||
public DateTime DateAdded { get; private set; }
|
||||
|
||||
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
|
||||
public string Account { get; private set; }
|
||||
|
||||
private LibraryBook() { }
|
||||
@@ -24,10 +22,6 @@ namespace DataLayer
|
||||
Account = account;
|
||||
}
|
||||
|
||||
// needed for v3 => v4 upgrade
|
||||
public void UpdateAccount(string account)
|
||||
=> Account ??= account;
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ namespace DataLayer
|
||||
NotLiberated = 0,
|
||||
Liberated = 1,
|
||||
/// <summary>Error occurred during liberation. Don't retry</summary>
|
||||
Error = 2
|
||||
Error = 2,
|
||||
|
||||
/// <summary>Application-state only. Not a valid persistence state.</summary>
|
||||
PartialDownload = 0x1000
|
||||
}
|
||||
|
||||
public class UserDefinedItem
|
||||
@@ -38,7 +41,15 @@ namespace DataLayer
|
||||
public string Tags
|
||||
{
|
||||
get => _tags;
|
||||
set => _tags = sanitize(value);
|
||||
set
|
||||
{
|
||||
var newTags = sanitize(value);
|
||||
if (_tags != newTags)
|
||||
{
|
||||
_tags = newTags;
|
||||
ItemChanged?.Invoke(this, nameof(Tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -94,12 +105,40 @@ namespace DataLayer
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses and book file location
|
||||
public LiberatedStatus BookStatus { get; set; }
|
||||
public string BookLocation { get; set; }
|
||||
public LiberatedStatus? PdfStatus { get; set; }
|
||||
#endregion
|
||||
#region LiberatedStatuses
|
||||
|
||||
private LiberatedStatus _bookStatus;
|
||||
private LiberatedStatus? _pdfStatus;
|
||||
public LiberatedStatus BookStatus
|
||||
{
|
||||
get => _bookStatus;
|
||||
set
|
||||
{
|
||||
if (_bookStatus != value)
|
||||
{
|
||||
_bookStatus = value;
|
||||
ItemChanged?.Invoke(this, nameof(BookStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
public LiberatedStatus? PdfStatus
|
||||
{
|
||||
get => _pdfStatus;
|
||||
set
|
||||
{
|
||||
if (_pdfStatus != value)
|
||||
{
|
||||
_pdfStatus = value;
|
||||
ItemChanged?.Invoke(this, nameof(PdfStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string> ItemChanged;
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace DataLayer
|
||||
// // overwrite collection
|
||||
// Entry(product).Collection(x => x.Narrators).Load();
|
||||
// product.Narrators = narrators;
|
||||
public DbSet<LibraryBook> Library { get; private set; }
|
||||
public DbSet<LibraryBook> LibraryBooks { get; private set; }
|
||||
public DbSet<Book> Books { get; private set; }
|
||||
public DbSet<Contributor> Contributors { get; private set; }
|
||||
public DbSet<Series> Series { get; private set; }
|
||||
|
||||
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal file
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal file
@@ -0,0 +1,387 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210821012137_RemoveUdiBookLocation")]
|
||||
partial class RemoveUdiBookLocation
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal file
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class RemoveUdiBookLocation : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookLocation",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
390
DataLayer/Migrations/20210901205042_BookIsEpisode.Designer.cs
generated
Normal file
390
DataLayer/Migrations/20210901205042_BookIsEpisode.Designer.cs
generated
Normal file
@@ -0,0 +1,390 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210901205042_BookIsEpisode")]
|
||||
partial class BookIsEpisode
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.9");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
24
DataLayer/Migrations/20210901205042_BookIsEpisode.cs
Normal file
24
DataLayer/Migrations/20210901205042_BookIsEpisode.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class BookIsEpisode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ContentType",
|
||||
table: "Books",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentType",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
}
|
||||
390
DataLayer/Migrations/20210902192153_RenameLibraryBooks.Designer.cs
generated
Normal file
390
DataLayer/Migrations/20210902192153_RenameLibraryBooks.Designer.cs
generated
Normal file
@@ -0,0 +1,390 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210902192153_RenameLibraryBooks")]
|
||||
partial class RenameLibraryBooks
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.9");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
63
DataLayer/Migrations/20210902192153_RenameLibraryBooks.cs
Normal file
63
DataLayer/Migrations/20210902192153_RenameLibraryBooks.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class RenameLibraryBooks : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Library",
|
||||
newName: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_LibraryBooks_Books_BookId",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_LibraryBooks",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "LibraryBooks",
|
||||
newName: "Library");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Library",
|
||||
table: "Library",
|
||||
column: "BookId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Library_Books_BookId",
|
||||
table: "Library",
|
||||
column: "BookId",
|
||||
principalTable: "Books",
|
||||
principalColumn: "BookId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace DataLayer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
.HasAnnotation("ProductVersion", "5.0.9");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
@@ -28,6 +28,9 @@ namespace DataLayer.Migrations
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -152,7 +155,7 @@ namespace DataLayer.Migrations
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
@@ -253,9 +256,6 @@ namespace DataLayer.Migrations
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("BookLocation")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public static class GenericPaging
|
||||
{
|
||||
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
|
||||
{
|
||||
if (pageSize < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
|
||||
|
||||
if (pageNumZeroStart > 0)
|
||||
query = query.Skip(pageNumZeroStart * pageSize);
|
||||
|
||||
return query.Take(pageSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace DataLayer
|
||||
{
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
// only library importing should directly query Book. All else should use LibraryBook
|
||||
public static class LibraryQueries
|
||||
public static class LibraryBookQueries
|
||||
{
|
||||
//// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid
|
||||
//public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
||||
@@ -17,14 +17,14 @@ namespace DataLayer
|
||||
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
|
||||
=> context
|
||||
.Library
|
||||
.LibraryBooks
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
|
||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.Library
|
||||
.LibraryBooks
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetLibraryBook(productId);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
FOR QUICK MIGRATION INSTRUCTIONS:
|
||||
_DB_NOTES.txt
|
||||
|
||||
|
||||
HOW TO CREATE: EF CORE PROJECT
|
||||
==============================
|
||||
example is for sqlite but the same works with MsSql
|
||||
|
||||
|
||||
nuget
|
||||
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
|
||||
Microsoft.EntityFrameworkCore.Sqlite
|
||||
|
||||
MIGRATIONS
|
||||
require core, not standard
|
||||
this can be a problem b/c standard and framework can only reference standard, not core
|
||||
TO USE MIGRATIONS (core and/or standard)
|
||||
add to csproj
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD
|
||||
edit csproj
|
||||
pluralize this xml tag
|
||||
from: TargetFramework
|
||||
to: TargetFrameworks
|
||||
inside of TargetFrameworks
|
||||
from: netstandard2.1
|
||||
to: netcoreapp3.1;netstandard2.1
|
||||
|
||||
run. error
|
||||
SQLite Error 1: 'no such table: Blogs'.
|
||||
|
||||
set project "Set as StartUp Project"
|
||||
|
||||
Tools >> Nuget Package Manager >> Package Manager Console
|
||||
default project: Examples\SQLite_NETCore2_0
|
||||
|
||||
PM> add-migration InitialCreate
|
||||
PM> Update-Database
|
||||
|
||||
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
|
||||
|
||||
new sqlite .db file created: Copy always/Copy if newer
|
||||
or copy .db file to destination
|
||||
|
||||
relative:
|
||||
optionsBuilder.UseSqlite("Data Source=blogging.db");
|
||||
absolute (use fwd slashes):
|
||||
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
|
||||
|
||||
|
||||
REFERENCE ARTICLES
|
||||
------------------
|
||||
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
|
||||
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
|
||||
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
@@ -19,13 +19,13 @@ namespace DtoImporterService
|
||||
new ContributorImporter(DbContext).Import(importItems);
|
||||
new SeriesImporter(DbContext).Import(importItems);
|
||||
new CategoryImporter(DbContext).Import(importItems);
|
||||
|
||||
|
||||
// get distinct
|
||||
var productIds = importItems.Select(i => i.DtoItem.ProductId).ToList();
|
||||
|
||||
var productIds = importItems.Select(i => i.DtoItem.ProductId).Distinct().ToList();
|
||||
|
||||
// load db existing => .Local
|
||||
loadLocal_books(productIds);
|
||||
|
||||
|
||||
// upsert
|
||||
var qtyNew = upsertBooks(importItems);
|
||||
return qtyNew;
|
||||
@@ -33,12 +33,30 @@ namespace DtoImporterService
|
||||
|
||||
private void loadLocal_books(List<string> productIds)
|
||||
{
|
||||
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId);
|
||||
// if this context has already loaded books, don't need to reload them. vestige from when context was long-lived. in practice, we now typically use a fresh context. this is quick though so no harm in leaving it.
|
||||
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId).ToList();
|
||||
var remainingProductIds = productIds
|
||||
.Distinct()
|
||||
.Except(localProductIds)
|
||||
.ToList();
|
||||
|
||||
#region // explanation of DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
|
||||
/*
|
||||
articles suggest loading to Local with
|
||||
context.Books.Load();
|
||||
we want Books and associated fields
|
||||
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
|
||||
this is emulating Load() but with also getting associated fields
|
||||
|
||||
from: Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
|
||||
// Summary:
|
||||
// Enumerates the query. When using Entity Framework, this causes the results of
|
||||
// the query to be loaded into the associated context. This is equivalent to calling
|
||||
// ToList and then throwing away the list (without the overhead of actually creating
|
||||
// the list).
|
||||
public static void Load<TSource>([NotNullAttribute] this IQueryable<TSource> source);
|
||||
*/
|
||||
#endregion
|
||||
|
||||
// GetBooks() eager loads Series, category, et al
|
||||
if (remainingProductIds.Any())
|
||||
DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
|
||||
@@ -67,6 +85,8 @@ namespace DtoImporterService
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;
|
||||
|
||||
// absence of authors is very rare, but possible
|
||||
if (!item.Authors?.Any() ?? true)
|
||||
item.Authors = new[] { new Person { Name = "", Asin = null } };
|
||||
@@ -105,6 +125,7 @@ namespace DtoImporterService
|
||||
item.TitleWithSubtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
authors,
|
||||
narrators,
|
||||
category,
|
||||
@@ -120,8 +141,8 @@ namespace DtoImporterService
|
||||
|
||||
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
|
||||
book.AddSupplementDownloadUrl(item.SupplementUrl);
|
||||
if (item.PdfUrl is not null)
|
||||
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
|
||||
|
||||
return book;
|
||||
}
|
||||
@@ -134,9 +155,6 @@ namespace DtoImporterService
|
||||
book.PictureId = item.PictureId;
|
||||
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
|
||||
|
||||
// needed during v3 => v4 migration
|
||||
book.UpdateLocale(importItem.LocaleName);
|
||||
|
||||
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
|
||||
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace DtoImporterService
|
||||
|
||||
private void loadLocal_categories(List<string> categoryIds)
|
||||
{
|
||||
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId);
|
||||
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId).ToList();
|
||||
var remainingCategoryIds = categoryIds
|
||||
.Distinct()
|
||||
.Except(localIds)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
@@ -47,57 +47,61 @@ namespace DtoImporterService
|
||||
|
||||
private void loadLocal_contributors(List<string> contributorNames)
|
||||
{
|
||||
// must include default/empty/missing
|
||||
contributorNames.Add(Contributor.GetEmpty().Name);
|
||||
|
||||
//// BAD: very inefficient
|
||||
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
|
||||
|
||||
// GOOD: Except() is efficient. Due to hashing, it's close to O(n)
|
||||
var localNames = DbContext.Contributors.Local.Select(c => c.Name);
|
||||
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
|
||||
var remainingContribNames = contributorNames
|
||||
.Distinct()
|
||||
.Except(localNames)
|
||||
.ToList();
|
||||
|
||||
// load existing => local
|
||||
// remember to include default/empty/missing
|
||||
var emptyName = Contributor.GetEmpty().Name;
|
||||
if (remainingContribNames.Any())
|
||||
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
|
||||
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPeople(List<Person> people)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
|
||||
var newPeople = people
|
||||
.Select(p => p.Name)
|
||||
.Distinct()
|
||||
.Except(localNames)
|
||||
.ToList();
|
||||
|
||||
foreach (var p in people)
|
||||
var groupby = people.GroupBy(
|
||||
p => p.Name,
|
||||
p => p,
|
||||
(key, g) => new { Name = key, People = g.ToList() }
|
||||
);
|
||||
foreach (var name in newPeople)
|
||||
{
|
||||
// Should be 'Single' not 'First'. A user had a duplicate get in somehow though so I'm now using 'First' defensively
|
||||
var person = DbContext.Contributors.Local.FirstOrDefault(c => c.Name == p.Name);
|
||||
if (person == null)
|
||||
{
|
||||
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
|
||||
qtyNew++;
|
||||
}
|
||||
var p = groupby.Single(g => g.Name == name).People.First();
|
||||
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
return newPeople.Count;
|
||||
}
|
||||
|
||||
// only use after loading contributors => local
|
||||
private int upsertPublishers(List<string> publishers)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
|
||||
var newPublishers = publishers
|
||||
.Distinct()
|
||||
.Except(localNames)
|
||||
.ToList();
|
||||
|
||||
foreach (var publisherName in publishers)
|
||||
{
|
||||
if (DbContext.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
|
||||
{
|
||||
DbContext.Contributors.Add(new Contributor(publisherName));
|
||||
qtyNew++;
|
||||
}
|
||||
}
|
||||
foreach (var pub in newPublishers)
|
||||
DbContext.Contributors.Add(new Contributor(pub));
|
||||
|
||||
return qtyNew;
|
||||
return newPublishers.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
public class LibraryImporter : ItemsImporterBase
|
||||
public class LibraryBookImporter : ItemsImporterBase
|
||||
{
|
||||
public LibraryImporter(LibationContext context) : base(context) { }
|
||||
public LibraryBookImporter(LibationContext context) : base(context) { }
|
||||
|
||||
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
@@ -28,13 +27,14 @@ namespace DtoImporterService
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache. it'll be fixed in pre .net5/efcore5
|
||||
// sqlite cannot alter pk. the work around is an extensive headache
|
||||
// - update: now possible in .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var currentLibraryProductIds = DbContext.Library.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
|
||||
|
||||
foreach (var newItem in newItems)
|
||||
@@ -43,16 +43,7 @@ namespace DtoImporterService
|
||||
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId);
|
||||
DbContext.Library.Add(libraryBook);
|
||||
}
|
||||
|
||||
// needed for v3 => v4 upgrade
|
||||
var toUpdate = DbContext.Library.Where(l => l.Account == null);
|
||||
foreach (var u in toUpdate)
|
||||
{
|
||||
var item = importItems.FirstOrDefault(ii => ii.DtoItem.ProductId == u.Book.AudibleProductId);
|
||||
if (item != null)
|
||||
u.UpdateAccount(item.AccountId);
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
}
|
||||
|
||||
var qtyNew = newItems.Count;
|
||||
34
DtoImporterService/PerfLogger.cs
Normal file
34
DtoImporterService/PerfLogger.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace DtoImporterService
|
||||
{
|
||||
public record timeLogEntry(string msg, long totalElapsed, long delta);
|
||||
public static class PerfLogger
|
||||
{
|
||||
private static Stopwatch sw = new Stopwatch();
|
||||
private static List<timeLogEntry> __log { get; } = new List<timeLogEntry> { new("begin", 0, 0) };
|
||||
|
||||
public static void logTime(string s)
|
||||
{
|
||||
var totalElapsed = sw.ElapsedMilliseconds;
|
||||
|
||||
var prev = __log.Last().totalElapsed;
|
||||
var delta = totalElapsed - prev;
|
||||
|
||||
__log.Add(new(s, totalElapsed, delta));
|
||||
}
|
||||
public static void logRestart()
|
||||
{
|
||||
__log.Clear();
|
||||
__log.Add(new("begin", 0, 0));
|
||||
sw.Restart();
|
||||
}
|
||||
public static void stop() => sw.Stop();
|
||||
public static string logOutput =>
|
||||
$"{nameof(timeLogEntry.msg)}\t{nameof(timeLogEntry.totalElapsed)}\t{nameof(timeLogEntry.delta)}\r\n"
|
||||
+ __log.Select(t => $"{t.msg}\t{t.totalElapsed}\t{t.delta}").Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using InternalUtilities;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace DtoImporterService
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private void loadLocal_series(List<AudibleApiDTOs.Series> series)
|
||||
private void loadLocal_series(List<AudibleApi.Common.Series> series)
|
||||
{
|
||||
var seriesIds = series.Select(s => s.SeriesId).ToList();
|
||||
var localIds = DbContext.Series.Local.Select(s => s.AudibleSeriesId).ToList();
|
||||
@@ -42,7 +42,7 @@ namespace DtoImporterService
|
||||
DbContext.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList();
|
||||
}
|
||||
|
||||
private int upsertSeries(List<AudibleApiDTOs.Series> requestedSeries)
|
||||
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace FileLiberator
|
||||
{
|
||||
public class ConvertToMp3 : IAudioDecodable
|
||||
{
|
||||
|
||||
private Mp4File m4bBook;
|
||||
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
@@ -30,14 +29,29 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
private long fileSize;
|
||||
public ConvertToMp3()
|
||||
{
|
||||
RequestCoverArt += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
TitleDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = e });
|
||||
AuthorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = e });
|
||||
NarratorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = e });
|
||||
CoverImageDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = e?.Length });
|
||||
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
private long fileSize;
|
||||
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
||||
|
||||
public void Cancel() => m4bBook?.Cancel();
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
{
|
||||
var path = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
|
||||
}
|
||||
|
||||
@@ -49,7 +63,7 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
|
||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||
|
||||
|
||||
@@ -15,29 +15,43 @@ namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : IAudioDecodable
|
||||
{
|
||||
private AaxcDownloadConverter aaxcDownloader;
|
||||
|
||||
private AaxcDownloadConverter aaxcDownloader;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageDiscovered;
|
||||
public event EventHandler<string> StreamingBegin;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageDiscovered;
|
||||
public event EventHandler<string> StreamingBegin;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
public DownloadDecryptBook()
|
||||
{
|
||||
RequestCoverArt += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
TitleDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = e });
|
||||
AuthorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = e });
|
||||
NarratorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = e });
|
||||
CoverImageDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = e?.Length });
|
||||
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
||||
if (libraryBook.Book.Audio_Exists)
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
@@ -47,14 +61,12 @@ namespace FileLiberator
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
// moves files and returns dest dir
|
||||
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
|
||||
if (!finalAudioExists)
|
||||
if (!moveResults.movedAudioFile)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
// only need to update if success. if failure, it will remain at 0 == NotLiberated
|
||||
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated, outputAudioFilename);
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
@@ -127,7 +139,7 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
{
|
||||
if (e is null && Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
@@ -144,10 +156,10 @@ namespace FileLiberator
|
||||
{
|
||||
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
|
||||
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
|
||||
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
|
||||
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
|
||||
}
|
||||
|
||||
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
@@ -162,6 +174,7 @@ namespace FileLiberator
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
|
||||
|
||||
bool movedAudioFile = false;
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest
|
||||
@@ -174,11 +187,13 @@ namespace FileLiberator
|
||||
Cue.UpdateFileName(f, audioFileName);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
|
||||
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
|
||||
}
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return destinationDir;
|
||||
return (destinationDir, movedAudioFile);
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
@@ -201,29 +216,28 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
private static void validate(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
|
||||
public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
|
||||
public DownloadFile()
|
||||
{
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
}
|
||||
|
||||
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
@@ -15,16 +15,24 @@ namespace FileLiberator
|
||||
{
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book);
|
||||
&& !libraryBook.Book.PDF_Exists;
|
||||
|
||||
public DownloadPdf()
|
||||
{
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
|
||||
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
|
||||
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -32,14 +40,14 @@ namespace FileLiberator
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var existingPath = Path.GetDirectoryName(ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book));
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
|
||||
if (existingPath != null)
|
||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||
|
||||
var full = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.PDF.StorageDirectory,
|
||||
AudibleFileStorage.PdfStorageDirectory,
|
||||
libraryBook.Book.Title,
|
||||
Path.GetExtension(file),
|
||||
libraryBook.Book.AudibleProductId);
|
||||
@@ -49,7 +57,7 @@ namespace FileLiberator
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
|
||||
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
@@ -58,10 +66,12 @@ namespace FileLiberator
|
||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
||||
proposedDownloadFilePath,
|
||||
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
|
||||
|
||||
return actualDownloadedFilePath;
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book)
|
||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||
=> !File.Exists(actualDownloadedFilePath)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
@@ -18,10 +17,8 @@ namespace FileLiberator
|
||||
|
||||
|
||||
// when used in foreach: stateful. deferred execution
|
||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
|
||||
=> DbContexts.GetContext()
|
||||
.GetLibrary_Flat_NoTracking()
|
||||
.Where(libraryBook => processable.Validate(libraryBook));
|
||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook => processable.Validate(libraryBook));
|
||||
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
|
||||
23
FileLiberator/LoggerUtilities.cs
Normal file
23
FileLiberator/LoggerUtilities.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public static class LoggerUtilities
|
||||
{
|
||||
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
||||
=> (
|
||||
id: libraryBook.Book.AudibleProductId,
|
||||
title: libraryBook.Book.Title,
|
||||
locale: libraryBook.Book.Locale,
|
||||
account: libraryBook.Account.ToMask()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,16 @@ namespace FileManager
|
||||
protected abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||
|
||||
public static string PdfStorageDirectory => BooksDirectory;
|
||||
|
||||
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
||||
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
|
||||
|
||||
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||
|
||||
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
@@ -34,13 +36,13 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
private IEnumerable<string> extensions_noDots { get; }
|
||||
protected IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
@@ -50,39 +52,25 @@ namespace FileManager
|
||||
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
BookDirectoryFiles.RefreshFiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example for full books:
|
||||
/// Search recursively in _books directory. Full book exists if either are true
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId) => GetPath(productId) != null;
|
||||
|
||||
public string GetPath(string productId)
|
||||
protected string GetFilePath(string productId)
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
string storageDir = StorageDirectory;
|
||||
string regexPattern = $@"{productId}.*?\.({extAggr})$";
|
||||
string firstOrNull;
|
||||
|
||||
if (storageDir == BooksDirectory)
|
||||
if (StorageDirectory == BooksDirectory)
|
||||
{
|
||||
//If user changed the BooksDirectory, reinitialize.
|
||||
if (storageDir != BookDirectoryFiles.RootDirectory)
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
lock (BookDirectoryFiles)
|
||||
{
|
||||
if (storageDir != BookDirectoryFiles.RootDirectory)
|
||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||
{
|
||||
BookDirectoryFiles = new BackgroundFileSystem(storageDir, "*.*", SearchOption.AllDirectories);
|
||||
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +81,7 @@ namespace FileManager
|
||||
{
|
||||
firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories)
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
|
||||
}
|
||||
|
||||
@@ -103,6 +91,21 @@ namespace FileManager
|
||||
FilePathCache.Upsert(productId, FileType, firstOrNull);
|
||||
return firstOrNull;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
public string GetDestDir(string title, string asin)
|
||||
{
|
||||
@@ -118,32 +121,8 @@ namespace FileManager
|
||||
|
||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
public const string SKIP_FILE_EXT = "libhack";
|
||||
|
||||
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public string CreateSkipFile(string title, string asin, string contents = null)
|
||||
{
|
||||
var destinationDir = GetDestDir(title, asin);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
|
||||
File.WriteAllText(path, contents ?? string.Empty);
|
||||
|
||||
return path;
|
||||
}
|
||||
public string GetPath(string productId) => GetFilePath(productId);
|
||||
}
|
||||
|
||||
public class AaxcFileStorage : AudibleFileStorage
|
||||
@@ -156,17 +135,13 @@ namespace FileManager
|
||||
public override string StorageDirectory => DownloadsInProgress;
|
||||
|
||||
public AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
}
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
{
|
||||
protected override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public PdfFileStorage() : base(FileType.PDF) { }
|
||||
/// <summary>
|
||||
/// Example for full books:
|
||||
/// Search recursively in _books directory. Full book exists if either are true
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,14 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
class BackgroundFileSystem
|
||||
/// <summary>
|
||||
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
|
||||
///
|
||||
/// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
|
||||
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
|
||||
/// this state and download again.
|
||||
/// </summary>
|
||||
internal class BackgroundFileSystem
|
||||
{
|
||||
public string RootDirectory { get; private set; }
|
||||
public string SearchPattern { get; private set; }
|
||||
|
||||
@@ -46,13 +46,17 @@ namespace FileManager
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public T GetNonString<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
/// <returns>Value was changed</returns>
|
||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
=> persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
@@ -67,39 +71,7 @@ namespace FileManager
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
#region MainForm: X, Y, Width, Height, MainFormIsMaximized
|
||||
public int MainFormX
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormX));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormX), value);
|
||||
}
|
||||
|
||||
public int MainFormY
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormY));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormY), value);
|
||||
}
|
||||
|
||||
public int MainFormWidth
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormWidth));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormWidth), value);
|
||||
}
|
||||
|
||||
public int MainFormHeight
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(MainFormHeight));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormHeight), value);
|
||||
}
|
||||
|
||||
public bool MainFormIsMaximized
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MainFormIsMaximized));
|
||||
set => persistentDictionary.SetNonString(nameof(MainFormIsMaximized), value);
|
||||
}
|
||||
#endregion
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
@@ -130,6 +102,29 @@ namespace FileManager
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
|
||||
public enum BadBookAction
|
||||
{
|
||||
[Description("Ask each time what action to take.")]
|
||||
Ask = 0,
|
||||
[Description("Stop processing books.")]
|
||||
Abort = 1,
|
||||
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||
Retry = 2,
|
||||
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[Description("When liberating books and there is an error, Libation should:")]
|
||||
public BadBookAction BadBook
|
||||
{
|
||||
get
|
||||
{
|
||||
var badBookStr = persistentDictionary.GetString(nameof(BadBook));
|
||||
return Enum.TryParse<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
|
||||
}
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region known directories
|
||||
@@ -190,13 +185,9 @@ namespace FileManager
|
||||
|
||||
#region logging
|
||||
private IConfigurationRoot configuration;
|
||||
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
//// with code. also persists to Settings.json
|
||||
//SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
|
||||
//// hack which achieves the same, in memory only
|
||||
//configuration["Serilog:WriteTo:1:Args:path"] = logPath;
|
||||
|
||||
configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
@@ -210,16 +201,8 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
var logLevelEnum = Enum<LogEventLevel>.Parse(logLevelStr);
|
||||
return logLevelEnum;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LogEventLevel.Information;
|
||||
}
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
|
||||
}
|
||||
set
|
||||
{
|
||||
@@ -248,11 +231,10 @@ namespace FileManager
|
||||
#region singleton stuff
|
||||
public static Configuration Instance { get; } = new Configuration();
|
||||
private Configuration() { }
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region LibationFiles
|
||||
|
||||
private const string APPSETTINGS_JSON = "appsettings.json";
|
||||
#region LibationFiles
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
@@ -269,12 +251,16 @@ namespace FileManager
|
||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
// Config init in Program.ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// Config init in ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||
bool settingWasChanged = SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
|
||||
// BAD: Serilog.WriteTo[1].Args
|
||||
// "[1]" assumes ordinal position
|
||||
// GOOD: Serilog.WriteTo[?(@.Name=='File')].Args
|
||||
var jsonpath = "Serilog.WriteTo[?(@.Name=='File')].Args";
|
||||
|
||||
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||
|
||||
return libationFilesPathCache;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace FileManager
|
||||
{
|
||||
public static class FilePathCache
|
||||
{
|
||||
private const string FILENAME = "FileLocations.json";
|
||||
internal class CacheEntry
|
||||
{
|
||||
public string Id { get; set; }
|
||||
@@ -18,7 +19,7 @@ namespace FileManager
|
||||
|
||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
|
||||
|
||||
static FilePathCache()
|
||||
{
|
||||
@@ -84,7 +85,7 @@ namespace FileManager
|
||||
try { resave(); }
|
||||
catch (IOException ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
|
||||
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace FileManager
|
||||
{
|
||||
var obj = GetObject(propertyName);
|
||||
if (obj is null) return default;
|
||||
if (obj is JToken jToken) return jToken.Value<T>();
|
||||
if (obj is JValue jValue) return jValue.Value<T>();
|
||||
if (obj is JObject jObject) return jObject.ToObject<T>();
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
@@ -58,26 +58,22 @@ namespace InternalUtilities
|
||||
|
||||
private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups)
|
||||
{
|
||||
var items = await api.GetAllLibraryItemsAsync(responseGroups);
|
||||
var items = new List<Item>();
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
//{
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//}
|
||||
#endif
|
||||
if (!items.Any())
|
||||
items = await api.GetAllLibraryItemsAsync(responseGroups);
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText("library.json", AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
|
||||
// remove episode parents
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
#region // episode handling. doesn't quite work
|
||||
// // add individual/children episodes
|
||||
// var childIds = items
|
||||
// .Where(i => i.Episodes)
|
||||
// .SelectMany(ep => ep.Relationships)
|
||||
// .Where(r => r.RelationshipToProduct == AudibleApiDTOs.RelationshipToProduct.Child && r.RelationshipType == AudibleApiDTOs.RelationshipType.Episode)
|
||||
// .Select(c => c.Asin)
|
||||
// .ToList();
|
||||
// foreach (var childId in childIds)
|
||||
// {
|
||||
// var bookResult = await api.GetLibraryBookAsync(childId, AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
// var bookItem = AudibleApiDTOs.LibraryDtoV10.FromJson(bookResult.ToString()).Item;
|
||||
// items.Add(bookItem);
|
||||
// }
|
||||
#endregion
|
||||
await manageEpisodesAsync(api, items);
|
||||
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
@@ -91,6 +87,143 @@ namespace InternalUtilities
|
||||
return items;
|
||||
}
|
||||
|
||||
#region episodes and podcasts
|
||||
private static async Task manageEpisodesAsync(Api api, List<Item> items)
|
||||
{
|
||||
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
|
||||
try
|
||||
{
|
||||
// get parents
|
||||
var parents = items.Where(i => i.IsEpisodes).ToList();
|
||||
#if DEBUG
|
||||
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText("parents.json", parentsDebug);
|
||||
#endif
|
||||
|
||||
if (!parents.Any())
|
||||
return;
|
||||
|
||||
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
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
// add children
|
||||
var children = await getEpisodesAsync(api, parents);
|
||||
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
|
||||
items.AddRange(children);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<Item>> getEpisodesAsync(Api api, List<Item> parents)
|
||||
{
|
||||
var results = new List<Item>();
|
||||
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
var children = await getEpisodeChildrenAsync(api, parent);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
child.PurchaseDate = parent.PurchaseDate;
|
||||
// parent is essentially a series
|
||||
child.Series = new Series[]
|
||||
{
|
||||
new Series
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
Sequence = parent.Relationships.Single(r => r.Asin == child.Asin).Sort.ToString(),
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
// overload (read: abuse) IsEpisodes flag
|
||||
child.Relationships = new Relationship[]
|
||||
{
|
||||
new Relationship
|
||||
{
|
||||
RelationshipToProduct = RelationshipToProduct.Child,
|
||||
RelationshipType = RelationshipType.Episode
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
results.AddRange(children);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static async Task<List<Item>> getEpisodeChildrenAsync(Api api, Item parent)
|
||||
{
|
||||
var childrenIds = parent.Relationships
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||
.Select(r => r.Asin)
|
||||
.ToList();
|
||||
|
||||
// fetch children in batches
|
||||
const int batchSize = 20;
|
||||
|
||||
var results = new List<Item>();
|
||||
|
||||
for (var i = 1; ; i++)
|
||||
{
|
||||
var idBatch = childrenIds.Skip((i - 1) * batchSize).Take(batchSize).ToList();
|
||||
if (!idBatch.Any())
|
||||
break;
|
||||
|
||||
List<Item> childrenBatch;
|
||||
try
|
||||
{
|
||||
childrenBatch = await api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
#if DEBUG
|
||||
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
|
||||
{
|
||||
ParentId = parent.Asin,
|
||||
ParentTitle = parent.Title,
|
||||
BatchNumber = i,
|
||||
ChildIdBatch = idBatch
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
|
||||
// the service returned no results. probably indicates an error. stop running batches
|
||||
if (!childrenBatch.Any())
|
||||
break;
|
||||
|
||||
results.AddRange(childrenBatch);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug("Parent episodes/podcasts series. Children found. {@DebugInfo}", new
|
||||
{
|
||||
ParentId = parent.Asin,
|
||||
ParentTitle = parent.Title,
|
||||
ChildCount = childrenIds.Count
|
||||
});
|
||||
|
||||
if (childrenIds.Count != results.Count)
|
||||
{
|
||||
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
|
||||
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static List<IValidator> getValidators()
|
||||
{
|
||||
var type = typeof(IValidator);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace InternalUtilities
|
||||
{
|
||||
@@ -76,9 +76,9 @@ namespace InternalUtilities
|
||||
|
||||
var distinct = items.GetSeriesDistinct();
|
||||
if (distinct.Any(s => s.SeriesId is null))
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesId)}", nameof(items)));
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesId)}", nameof(items)));
|
||||
if (distinct.Any(s => s.SeriesName is null))
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesName)}", nameof(items)));
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesName)}", nameof(items)));
|
||||
|
||||
return exceptions;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using AudibleApiDTOs;
|
||||
using AudibleApi.Common;
|
||||
|
||||
namespace InternalUtilities
|
||||
{
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<PackageReference Include="AudibleApi" Version="1.2.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
150
Libation.sln
150
Libation.sln
@@ -5,6 +5,7 @@ VisualStudioVersion = 16.0.28803.156
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
|
||||
__TODO.txt = __TODO.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
@@ -34,59 +35,26 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Tests", "0 Tests", "{38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Demos and Examples", "0 Demos and Examples", "{F61184E7-2426-4A13-ACEF-5689928E2CE2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Demos", "..\Dinah.Core\_Demos\Dinah.Core.Demos\Dinah.Core.Demos.csproj", "{9F1AA3DE-962F-469B-82B2-46F93491389B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Tests", "..\Dinah.Core\_Tests\Dinah.Core.Tests\Dinah.Core.Tests.csproj", "{E874D000-AD3A-4629-AC65-7219C2C7C1F0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCommon", "..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj", "{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitAllRepos", "..\GitAllRepos\GitAllRepos\GitAllRepos.csproj", "{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Tests", "..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj", "{111420E2-D4F0-4068-B46A-C4B6DCC823DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsDesigner", "WinFormsDesigner\WinFormsDesigner.csproj", "{0807616A-A77A-4B08-A65A-1582B09E114B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj", "{35803735-B669-4090-9681-CC7F7FABDC71}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2.Tests", "..\LuceneNet303r2\LuceneNet303r2.Tests\LuceneNet303r2.Tests.csproj", "{5A7681A5-60D9-480B-9AC7-63E0812A2548}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{428163C3-D558-4914-B570-A92069521877} = {428163C3-D558-4914-B570-A92069521877}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs", "..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj", "{C03C5D65-3B7F-453B-972F-23950B7E0604}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs.Tests", "..\audible api\AudibleApi\_Tests\AudibleApiDTOs.Tests\AudibleApiDTOs.Tests.csproj", "{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\_Demos\AudibleApiClientExample\AudibleApiClientExample.csproj", "{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||
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}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\LibationCli.csproj", "{428163C3-D558-4914-B570-A92069521877}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -118,82 +86,18 @@ Global
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.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
|
||||
@@ -202,53 +106,37 @@ Global
|
||||
{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
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{E874D000-AD3A-4629-AC65-7219C2C7C1F0} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{C03C5D65-3B7F-453B-972F-23950B7E0604} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{94BEB7CC-511D-45AB-9F09-09BE858EE486} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{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}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
41
LibationCli/LibationCli.csproj
Normal file
41
LibationCli/LibationCli.csproj
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
When LibationWinForms and LibationCli output to the same dir, LibationCli must build before LibationWinForms
|
||||
|
||||
VS > rt-clik solution > Project Build Order...
|
||||
Dependencies [tab]
|
||||
Projects: LibationWinForms
|
||||
manually check LibationCli
|
||||
-->
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\LibationWinForms\bin\Debug</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\LibationWinForms\bin\Release</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
14
LibationCli/Options/ConvertOptions.cs
Normal file
14
LibationCli/Options/ConvertOptions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("convert", HelpText = "Convert mp4 to mp3.")]
|
||||
public class ConvertOptions : ProcessableOptionsBase
|
||||
{
|
||||
protected override Task ProcessAsync() => RunAsync(CreateProcessable<FileLiberator.ConvertToMp3>());
|
||||
}
|
||||
}
|
||||
55
LibationCli/Options/ExportOptions.cs
Normal file
55
LibationCli/Options/ExportOptions.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("export", HelpText = "Must include path and flag for export file type: --xlsx , --csv , --json]")]
|
||||
public class ExportOptions : OptionsBase
|
||||
{
|
||||
[Option(shortName: 'p', longName: "path", Required = true, HelpText = "Path to save file to.")]
|
||||
public string FilePath { get; set; }
|
||||
|
||||
#region explanation of mutually exclusive options
|
||||
/*
|
||||
giving these SetName values makes them mutually exclusive. they are in different sets. eg:
|
||||
class Options
|
||||
{
|
||||
[Option("username", SetName = "auth")]
|
||||
public string Username { get; set; }
|
||||
[Option("password", SetName = "auth")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Option("guestaccess", SetName = "guest")]
|
||||
public bool GuestAccess { get; set; }
|
||||
}
|
||||
*/
|
||||
#endregion
|
||||
[Option(shortName: 'x', longName: "xlsx", SetName = "xlsx", Required = true)]
|
||||
public bool xlsx { get; set; }
|
||||
|
||||
[Option(shortName: 'c', longName: "csv", SetName = "csv", Required = true)]
|
||||
public bool csv { get; set; }
|
||||
|
||||
[Option(shortName: 'j', longName: "json", SetName = "json", Required = true)]
|
||||
public bool json { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
if (xlsx)
|
||||
LibraryExporter.ToXlsx(FilePath);
|
||||
if (csv)
|
||||
LibraryExporter.ToCsv(FilePath);
|
||||
if (json)
|
||||
LibraryExporter.ToJson(FilePath);
|
||||
|
||||
Console.WriteLine($"Library exported to: {FilePath}");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
LibationCli/Options/LiberateOptions.cs
Normal file
37
LibationCli/Options/LiberateOptions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("liberate", HelpText = "Liberate: book and pdf backups. Default: download and decrypt all un-liberated titles and download pdfs. "
|
||||
+ "Optional: use 'pdf' flag to only download pdfs.")]
|
||||
public class LiberateOptions : ProcessableOptionsBase
|
||||
{
|
||||
[Option(shortName: 'p', longName: "pdf", Required = false, Default = false, HelpText = "Flag to only download pdfs")]
|
||||
public bool PdfOnly { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
=> PdfOnly
|
||||
? RunAsync(CreateProcessable<DownloadPdf>())
|
||||
: RunAsync(CreateBackupBook());
|
||||
|
||||
private static IProcessable CreateBackupBook()
|
||||
{
|
||||
var downloadPdf = CreateProcessable<DownloadPdf>();
|
||||
|
||||
//Chain pdf download on DownloadDecryptBook.Completed
|
||||
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||
{
|
||||
await downloadPdf.TryProcessAsync(e);
|
||||
}
|
||||
|
||||
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook>(onDownloadDecryptBookCompleted);
|
||||
return downloadDecryptBook;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
LibationCli/Options/ScanOptions.cs
Normal file
79
LibationCli/Options/ScanOptions.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("scan", HelpText = "Scan library. Default: scan all accounts. Optional: use 'account' flag to specify a single account.")]
|
||||
public class ScanOptions : OptionsBase
|
||||
{
|
||||
[Value(0, MetaName = "Accounts", HelpText = "Optional: nicknames of accounts to scan.", Required = false)]
|
||||
public IEnumerable<string> AccountNicknames { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
var accounts = getAccounts();
|
||||
if (!accounts.Any())
|
||||
{
|
||||
Console.WriteLine("No accounts. Exiting.");
|
||||
Environment.ExitCode = (int)ExitCode.RunTimeError;
|
||||
return;
|
||||
}
|
||||
|
||||
var _accounts = accounts.ToArray();
|
||||
|
||||
var intro
|
||||
= (_accounts.Length == 1)
|
||||
? "Scanning Audible library. This may take a few minutes."
|
||||
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
|
||||
Console.WriteLine(intro);
|
||||
|
||||
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync(
|
||||
(account) => null,
|
||||
_accounts);
|
||||
|
||||
Console.WriteLine("Scan complete.");
|
||||
Console.WriteLine($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}");
|
||||
}
|
||||
|
||||
private Account[] getAccounts()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.GetAll().ToArray();
|
||||
|
||||
if (!AccountNicknames.Any())
|
||||
return accounts;
|
||||
|
||||
var found = accounts.Where(acct => AccountNicknames.Contains(acct.AccountName)).ToArray();
|
||||
var notFound = AccountNicknames.Except(found.Select(f => f.AccountName)).ToArray();
|
||||
|
||||
// no accounts found. do not continue
|
||||
if (!found.Any())
|
||||
{
|
||||
Console.WriteLine("Accounts not found:");
|
||||
foreach (var nf in notFound)
|
||||
Console.WriteLine($"- {nf}");
|
||||
return found;
|
||||
}
|
||||
|
||||
// some accounts not found. continue after message
|
||||
if (notFound.Any())
|
||||
{
|
||||
Console.WriteLine("Accounts found:");
|
||||
foreach (var f in found)
|
||||
Console.WriteLine($"- {f}");
|
||||
Console.WriteLine("Accounts not found:");
|
||||
foreach (var nf in notFound)
|
||||
Console.WriteLine($"- {nf}");
|
||||
}
|
||||
|
||||
// else: all accounts area found. silently continue
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
LibationCli/Options/_OptionsBase.cs
Normal file
31
LibationCli/Options/_OptionsBase.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
public abstract class OptionsBase
|
||||
{
|
||||
public async Task Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Environment.ExitCode = (int)ExitCode.RunTimeError;
|
||||
|
||||
Console.WriteLine("ERROR");
|
||||
Console.WriteLine("=====");
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task ProcessAsync();
|
||||
}
|
||||
}
|
||||
58
LibationCli/Options/_ProcessableOptionsBase.cs
Normal file
58
LibationCli/Options/_ProcessableOptionsBase.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
// streamlined, non-Forms copy of ProcessorAutomationController
|
||||
public abstract class ProcessableOptionsBase : OptionsBase
|
||||
{
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)
|
||||
where TProcessable : IProcessable, new()
|
||||
{
|
||||
var strProc = new TProcessable();
|
||||
|
||||
strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
|
||||
strProc.Completed += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
|
||||
|
||||
strProc.Completed += completedAction;
|
||||
|
||||
return strProc;
|
||||
}
|
||||
|
||||
protected static async Task RunAsync(IProcessable Processable)
|
||||
{
|
||||
foreach (var libraryBook in Processable.GetValidLibraryBooks(DbContexts.GetLibrary_Flat_NoTracking()))
|
||||
await ProcessOneAsync(Processable, libraryBook, false);
|
||||
|
||||
var done = "Done. All books have been processed";
|
||||
Console.WriteLine(done);
|
||||
Serilog.Log.Logger.Information(done);
|
||||
}
|
||||
|
||||
private static async Task ProcessOneAsync(IProcessable Processable, LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
|
||||
|
||||
if (statusHandler.IsSuccess)
|
||||
return;
|
||||
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
Serilog.Log.Logger.Error(errorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = "Error processing book. Skipping. This book will be tried again on next attempt. For options of skipping or marking as error, retry with main Libation app.";
|
||||
Console.WriteLine(msg + ". See log for more details.");
|
||||
Serilog.Log.Logger.Error(ex, $"{msg} {{@DebugInfo}}", new { Book = libraryBook.LogFriendly() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
LibationCli/Program.cs
Normal file
84
LibationCli/Program.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
public enum ExitCode
|
||||
{
|
||||
ProcessCompletedSuccessfully = 0,
|
||||
NonRunNonError = 1,
|
||||
ParseError = 2,
|
||||
RunTimeError = 3
|
||||
}
|
||||
class Program
|
||||
{
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
Setup.SubscribeToDatabaseEvents();
|
||||
|
||||
var types = Setup.LoadVerbs();
|
||||
|
||||
#if DEBUG
|
||||
string input = null;
|
||||
|
||||
//input = " export --help";
|
||||
//input = " scan cupidneedsglasses";
|
||||
//input = " liberate ";
|
||||
|
||||
|
||||
// note: this hack will fail for quoted file paths with spaces because it will break on those spaces
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var setBreakPointHere = args;
|
||||
#endif
|
||||
|
||||
var result = Parser.Default.ParseArguments(args, types);
|
||||
|
||||
// if successfully parsed
|
||||
// async: run parsed options
|
||||
await result.WithParsedAsync<OptionsBase>(opt => opt.Run());
|
||||
|
||||
// if not successfully parsed
|
||||
// sync: handle parse errors
|
||||
result.WithNotParsed(errors => HandleErrors(result, errors));
|
||||
|
||||
return Environment.ExitCode;
|
||||
}
|
||||
|
||||
private static void HandleErrors(ParserResult<object> result, IEnumerable<Error> errors)
|
||||
{
|
||||
var errorsList = errors.ToList();
|
||||
if (errorsList.Any(e => e.Tag.In(ErrorType.HelpRequestedError, ErrorType.VersionRequestedError, ErrorType.HelpVerbRequestedError)))
|
||||
{
|
||||
Environment.ExitCode = (int)ExitCode.NonRunNonError;
|
||||
return;
|
||||
}
|
||||
|
||||
Environment.ExitCode = (int)ExitCode.ParseError;
|
||||
|
||||
if (errorsList.Any(e => e.Tag.In(ErrorType.NoVerbSelectedError)))
|
||||
{
|
||||
Console.WriteLine("No verb selected");
|
||||
return;
|
||||
}
|
||||
|
||||
var helpText = HelpText.AutoBuild(result,
|
||||
h => HelpText.DefaultParsingErrorsHandler(result, h),
|
||||
e => e);
|
||||
Console.WriteLine(helpText);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
LibationCli/Setup.cs
Normal file
63
LibationCli/Setup.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
public static class Setup
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations();
|
||||
LibationScaffolding.RunPostMigrationScaffolding();
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate();
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void checkForUpdate()
|
||||
{
|
||||
var (hasUpgrade, zipUrl, htmlUrl, zipName) = LibationScaffolding.GetLatestRelease();
|
||||
if (!hasUpgrade)
|
||||
return;
|
||||
|
||||
var origColor = Console.ForegroundColor;
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"UPDATE AVAILABLE @ {zipUrl}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ForegroundColor = origColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SubscribeToDatabaseEvents()
|
||||
{
|
||||
DataLayer.UserDefinedItem.ItemChanged += (sender, e) => ApplicationServices.LibraryCommands.UpdateUserDefinedItem(((DataLayer.UserDefinedItem)sender).Book);
|
||||
}
|
||||
|
||||
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>5.5.0.12</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DefineConstants>TRACE;DEBUG</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSBump" Version="2.3.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationWinForms\LibationWinForms.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,496 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi.Authorization;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationLauncher
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
static extern bool AllocConsole();
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
//// uncomment to see Console. MUST be called before anything writes to Console. Might only work from VS
|
||||
//AllocConsole();
|
||||
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// must occur before access to Configuration instance
|
||||
migrate_to_v5_2_0__pre_config();
|
||||
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
createSettings(config);
|
||||
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
migrate_to_v5_0_0(config);
|
||||
migrate_to_v5_2_0__post_config(config);
|
||||
//migrate_to_v5_4_1(config);// comment out until after vacation
|
||||
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate(config);
|
||||
#endif
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
private static void createSettings(Configuration config)
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
|
||||
static void CancelInstallation()
|
||||
{
|
||||
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Application.Exit();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settigns in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
var setupDialog = new SetupDialog();
|
||||
if (setupDialog.ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
|
||||
if (libationFilesDialog.ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
// path did not result in valid settings
|
||||
MessageBox.Show(
|
||||
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (libationFilesDialog.ShowDialog() != DialogResult.Yes)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
config.AllowLibationFixup = true;
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
#region migrate to v5.0.0 re-register device if device info not in settings
|
||||
private static void migrate_to_v5_0_0(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||
return;
|
||||
|
||||
var accountsPersister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
var accounts = accountsPersister?.AccountsSettings?.Accounts;
|
||||
if (accounts is null)
|
||||
return;
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var identity = account?.IdentityTokens;
|
||||
|
||||
if (identity is null)
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identity.DeviceType) &&
|
||||
!string.IsNullOrWhiteSpace(identity.DeviceSerialNumber) &&
|
||||
!string.IsNullOrWhiteSpace(identity.AmazonAccountId))
|
||||
continue;
|
||||
|
||||
var authorize = new Authorize(identity.Locale);
|
||||
|
||||
try
|
||||
{
|
||||
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
|
||||
identity.Invalidate();
|
||||
|
||||
var api = AudibleApiActions.GetApiAsync(new LibationWinForms.Login.WinformResponder(account), account).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Don't care if it fails
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate to v5.2.0
|
||||
// get rid of meta-directories, combine DownloadsInProgressEnum and DecryptInProgressEnum => InProgress
|
||||
private static void migrate_to_v5_2_0__pre_config()
|
||||
{
|
||||
{
|
||||
var settingsKey = "DownloadsInProgressEnum";
|
||||
if (UNSAFE_MigrationHelper.Settings_TryGet(settingsKey, out var value))
|
||||
{
|
||||
UNSAFE_MigrationHelper.Settings_Delete(settingsKey);
|
||||
UNSAFE_MigrationHelper.Settings_Insert("InProgress", translatePath(value));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
UNSAFE_MigrationHelper.Settings_Delete("DecryptInProgressEnum");
|
||||
}
|
||||
|
||||
{ // appsettings.json
|
||||
var appSettingsKey = UNSAFE_MigrationHelper.LIBATION_FILES_KEY;
|
||||
if (UNSAFE_MigrationHelper.APPSETTINGS_TryGet(appSettingsKey, out var value))
|
||||
UNSAFE_MigrationHelper.APPSETTINGS_Update(appSettingsKey, translatePath(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static string translatePath(string path)
|
||||
=> path switch
|
||||
{
|
||||
"AppDir" => @".\LibationFiles",
|
||||
"MyDocs" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LibationFiles")),
|
||||
"UserProfile" => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")),
|
||||
"WinTemp" => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")),
|
||||
_ => path
|
||||
};
|
||||
|
||||
private 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
|
||||
|
||||
#region migrate to v5.4.1 see comment
|
||||
// this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually
|
||||
// implement the portion which removes FilePaths.json, at which time this method will be a proper migration
|
||||
//
|
||||
// I'm iterating through safe steps toward getting rid of the live scanner except to track audiobook files as a convenience
|
||||
// such as clicking the stop light to open its location. live scanning will be replaced with state tracking in the database.
|
||||
|
||||
// FilePaths.json => db. long running. fire and forget
|
||||
private static void migrate_to_v5_4_1(Configuration config)
|
||||
=> new System.Threading.Thread(() => migrate_to_v5_4_1_thread(config)) { IsBackground = true }.Start();
|
||||
private static void migrate_to_v5_4_1_thread(Configuration config)
|
||||
{
|
||||
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
|
||||
if (!File.Exists(filePaths))
|
||||
return;
|
||||
|
||||
using var context = ApplicationServices.DbContexts.GetContext();
|
||||
context.Books.Load();
|
||||
|
||||
var jArr = JArray.Parse(File.ReadAllText(filePaths));
|
||||
|
||||
foreach (var jToken in jArr)
|
||||
{
|
||||
var asinToken = jToken["Id"];
|
||||
var fileTypeToken = jToken["FileType"];
|
||||
var pathToken = jToken["Path"];
|
||||
if (asinToken is null || fileTypeToken is null || pathToken is null ||
|
||||
asinToken.Type != JTokenType.String || fileTypeToken.Type != JTokenType.Integer || pathToken.Type != JTokenType.String)
|
||||
continue;
|
||||
|
||||
var asin = asinToken.Value<string>();
|
||||
var fileType = (FileType)fileTypeToken.Value<int>();
|
||||
var path = pathToken.Value<string>();
|
||||
|
||||
if (fileType == FileType.Unknown || fileType == FileType.AAXC)
|
||||
continue;
|
||||
|
||||
var book = context.Books.Local.FirstOrDefault(b => b.AudibleProductId == asin);
|
||||
if (book is null)
|
||||
continue;
|
||||
|
||||
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
||||
if (fileType == FileType.PDF)
|
||||
book.UserDefinedItem.PdfStatus = LiberatedStatus.Liberated;
|
||||
|
||||
if (fileType == FileType.Audio)
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
book.UserDefinedItem.BookLocation = path;
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
|
||||
}
|
||||
debugStopwatch.Stop();
|
||||
var debugTotal = debugStopwatch.Elapsed;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
{
|
||||
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", "File" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
// for this sink to work, a path must be provided. we override this below
|
||||
{ "path", Path.Combine(config.LibationFiles, "_Log.log") },
|
||||
{ "rollingInterval", "Month" },
|
||||
// Serilog template formatting examples
|
||||
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||||
// 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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "Using", new JArray{ "Dinah.Core" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller" } },
|
||||
};
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
}
|
||||
|
||||
// to restore original: Console.SetOut(origOut);
|
||||
private static TextWriter origOut { get; } = Console.Out;
|
||||
|
||||
private static void configureLogging(Configuration config)
|
||||
{
|
||||
config.ConfigureLogging();
|
||||
|
||||
// Fwd Console to serilog.
|
||||
// Serilog also writes to Console (should probably change this) so it might be asking for trouble.
|
||||
// SerilogTextWriter needs to be more robust and tested. Esp the Write() methods.
|
||||
// Empirical testing so far has shown no issues.
|
||||
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
|
||||
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
Log.Logger.Information("Begin Libation. {@DebugInfo}", new
|
||||
{
|
||||
Version = BuildVersion.ToString(),
|
||||
#if DEBUG
|
||||
Mode = "Debug",
|
||||
#else
|
||||
Mode = "Release",
|
||||
#endif
|
||||
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
|
||||
config.InProgress,
|
||||
|
||||
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
|
||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
|
||||
|
||||
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
|
||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
||||
});
|
||||
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
}
|
||||
|
||||
private static void checkForUpdate(Configuration config)
|
||||
{
|
||||
string zipUrl;
|
||||
string selectedPath;
|
||||
|
||||
try
|
||||
{
|
||||
// timed out
|
||||
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
if (latest is null)
|
||||
return;
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
if (!Version.TryParse(latestVersionString, out var latestRelease))
|
||||
return;
|
||||
|
||||
// we're up to date
|
||||
if (latestRelease <= BuildVersion)
|
||||
return;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
zipUrl = zip?.BrowserDownloadUrl;
|
||||
|
||||
Log.Logger.Information("Update available: {@DebugInfo}", new {
|
||||
latestRelease = latestRelease.ToString(),
|
||||
latest.HtmlUrl,
|
||||
zipUrl
|
||||
});
|
||||
|
||||
if (zipUrl is null)
|
||||
{
|
||||
MessageBox.Show(latest.HtmlUrl, "New version available");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"New version available @ {latest.HtmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
using var fileSelector = new SaveFileDialog { FileName = zip.Name, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
selectedPath = fileSelector.FileName;
|
||||
}
|
||||
catch (AggregateException aggEx)
|
||||
{
|
||||
Log.Logger.Error(aggEx, "Checking for new version too often");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error checking for update", "Error checking for update", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error downloading update", "Error downloading update", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Octokit.Release getLatestRelease(TimeSpan timeout)
|
||||
{
|
||||
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
|
||||
if (task.Wait(timeout))
|
||||
return task.Result;
|
||||
|
||||
Log.Logger.Information("Timed out");
|
||||
return null;
|
||||
}
|
||||
private static Octokit.Release getLatestRelease()
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
return latest;
|
||||
}
|
||||
|
||||
private static Version BuildVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB |
@@ -9,7 +9,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj" />
|
||||
<PackageReference Include="LuceneNet303r2" Version="3.0.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -9,24 +9,24 @@ using Lucene.Net.Search;
|
||||
namespace LibationSearchEngine
|
||||
{
|
||||
// field names are case specific and, due to StandardAnalyzer, content is case INspecific
|
||||
public static class LuceneExtensions
|
||||
internal static class LuceneExtensions
|
||||
{
|
||||
public static void AddRaw(this Document document, string name, string value)
|
||||
internal static void AddRaw(this Document document, string name, string value)
|
||||
=> document.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED));
|
||||
|
||||
public static void AddAnalyzed(this Document document, string name, string value)
|
||||
internal static void AddAnalyzed(this Document document, string name, string value)
|
||||
{
|
||||
if (value != null)
|
||||
document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
|
||||
public static void AddNotAnalyzed(this Document document, string name, string value)
|
||||
internal static void AddNotAnalyzed(this Document document, string name, string value)
|
||||
=> document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.NOT_ANALYZED));
|
||||
|
||||
public static void AddBool(this Document document, string name, bool value)
|
||||
internal static void AddBool(this Document document, string name, bool value)
|
||||
=> document.Add(new Field(name.ToLowerInvariant(), value.ToString(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS));
|
||||
|
||||
public static Query GetQuery(this Analyzer analyzer, string defaultField, string searchString)
|
||||
internal static Query GetQuery(this Analyzer analyzer, string defaultField, string searchString)
|
||||
=> new QueryParser(SearchEngine.Version, defaultField.ToLowerInvariant(), analyzer).Parse(searchString);
|
||||
|
||||
// put all numbers, including dates, into this format:
|
||||
|
||||
@@ -107,14 +107,14 @@ namespace LibationSearchEngine
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
|
||||
new Dictionary<string, Func<LibraryBook, bool>>
|
||||
{
|
||||
["HasDownloads"] = lb => lb.Book.Supplements.Any(),
|
||||
["HasDownload"] = lb => lb.Book.Supplements.Any(),
|
||||
["Downloads"] = lb => lb.Book.Supplements.Any(),
|
||||
["Download"] = lb => lb.Book.Supplements.Any(),
|
||||
["HasPDFs"] = lb => lb.Book.Supplements.Any(),
|
||||
["HasPDF"] = lb => lb.Book.Supplements.Any(),
|
||||
["PDFs"] = lb => lb.Book.Supplements.Any(),
|
||||
["PDF"] = lb => lb.Book.Supplements.Any(),
|
||||
["HasDownloads"] = lb => lb.Book.HasPdf,
|
||||
["HasDownload"] = lb => lb.Book.HasPdf,
|
||||
["Downloads"] = lb => lb.Book.HasPdf,
|
||||
["Download"] = lb => lb.Book.HasPdf,
|
||||
["HasPDFs"] = lb => lb.Book.HasPdf,
|
||||
["HasPDF"] = lb => lb.Book.HasPdf,
|
||||
["PDFs"] = lb => lb.Book.HasPdf,
|
||||
["PDF"] = lb => lb.Book.HasPdf,
|
||||
|
||||
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
|
||||
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
|
||||
@@ -129,6 +129,11 @@ namespace LibationSearchEngine
|
||||
["IsLiberated"] = lb => isLiberated(lb.Book),
|
||||
["Liberated"] = lb => isLiberated(lb.Book),
|
||||
["LiberatedError"] = lb => liberatedError(lb.Book),
|
||||
|
||||
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -139,9 +144,7 @@ namespace LibationSearchEngine
|
||||
return authors.Intersect(narrators).Any();
|
||||
}
|
||||
|
||||
private static bool isLiberated(Book book)
|
||||
=> book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated
|
||||
|| AudibleFileStorage.Audio.Exists(book.AudibleProductId);
|
||||
private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated;
|
||||
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
|
||||
|
||||
// use these common fields in the "all" default search field
|
||||
@@ -194,25 +197,9 @@ namespace LibationSearchEngine
|
||||
#endregion
|
||||
|
||||
#region create and update index
|
||||
/// <summary>
|
||||
/// create new. ie: full re-index
|
||||
/// </summary>
|
||||
/// <param name="overwrite"></param>
|
||||
public void CreateNewIndex(LibationContext context, bool overwrite = true)
|
||||
/// <summary>create new. ie: full re-index</summary>
|
||||
public void CreateNewIndex(IEnumerable<LibraryBook> library, bool overwrite = true)
|
||||
{
|
||||
// 300 titles: 200- 400 ms
|
||||
// 1021 titles: 1777-2250 ms
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var stamps = new List<long>();
|
||||
void log() => stamps.Add(sw.ElapsedMilliseconds);
|
||||
|
||||
|
||||
log();
|
||||
|
||||
var library = context.GetLibrary_Flat_NoTracking();
|
||||
|
||||
log();
|
||||
|
||||
// location of index/create the index
|
||||
using var index = getIndex();
|
||||
var exists = IndexReader.IndexExists(index);
|
||||
@@ -226,8 +213,6 @@ namespace LibationSearchEngine
|
||||
var doc = createBookIndexDocument(libraryBook);
|
||||
ixWriter.AddDocument(doc);
|
||||
}
|
||||
|
||||
log();
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using Dinah.Core.Threading;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@@ -8,9 +8,7 @@ namespace LibationWinForms
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public AsyncNotifyPropertyChanged() { }
|
||||
|
||||
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
=>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) });
|
||||
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
=> this.UIThreadAsync(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
class AudioConvertForm : AudioDecodeForm
|
||||
{
|
||||
public AudioConvertForm()
|
||||
{
|
||||
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
#region AudioDecodeForm overrides
|
||||
public override string DecodeActionName => "Converting";
|
||||
#endregion
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using Dinah.Core.Threading;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
@@ -39,7 +39,6 @@ namespace LibationWinForms.BookLiberation
|
||||
#endregion
|
||||
|
||||
#region IStreamable event handler overrides
|
||||
public override void OnStreamingBegin(object sender, string beginString) { }
|
||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||
{
|
||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||
@@ -48,13 +47,12 @@ namespace LibationWinForms.BookLiberation
|
||||
if (downloadProgress.ProgressPercentage == 0)
|
||||
updateRemainingTime(0);
|
||||
else
|
||||
progressBar1.UIThread(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage);
|
||||
progressBar1.UIThreadAsync(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage);
|
||||
}
|
||||
|
||||
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
|
||||
=> updateRemainingTime((int)timeRemaining.TotalSeconds);
|
||||
|
||||
public override void OnStreamingCompleted(object sender, string completedString) { }
|
||||
#endregion
|
||||
|
||||
#region IAudioDecodable event handlers
|
||||
@@ -63,7 +61,7 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public override void OnTitleDiscovered(object sender, string title)
|
||||
{
|
||||
this.UIThread(() => this.Text = DecodeActionName + " " + title);
|
||||
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
|
||||
this.title = title;
|
||||
updateBookInfo();
|
||||
}
|
||||
@@ -81,14 +79,14 @@ namespace LibationWinForms.BookLiberation
|
||||
}
|
||||
|
||||
public override void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
||||
=> pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
||||
#endregion
|
||||
|
||||
// thread-safe UI updates
|
||||
private void updateBookInfo()
|
||||
=> bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
|
||||
private void updateRemainingTime(int remaining)
|
||||
=> remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
|
||||
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
class AudioDecryptForm : AudioDecodeForm
|
||||
{
|
||||
public AudioDecryptForm()
|
||||
{
|
||||
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
#region AudioDecodeForm overrides
|
||||
public override string DecodeActionName => "Decrypting";
|
||||
#endregion
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Threading;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
@@ -19,7 +18,7 @@ namespace LibationWinForms.BookLiberation
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||
logTb.UIThreadAsync(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||
}
|
||||
|
||||
public void FinalizeUI()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using FileLiberator;
|
||||
using System;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Threading;
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationWinForms.BookLiberation.BaseForms
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
||||
streamable.StreamingCompleted += OnStreamingCompleted;
|
||||
streamable.StreamingCompleted += OnStreamingCompletedClose;
|
||||
|
||||
FormClosed += UnsubscribeStreamable;
|
||||
Disposed += UnsubscribeStreamable;
|
||||
}
|
||||
private void Subscribe(IProcessable processable)
|
||||
{
|
||||
@@ -81,7 +81,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
||||
}
|
||||
private void UnsubscribeStreamable(object sender, EventArgs e)
|
||||
{
|
||||
FormClosed -= UnsubscribeStreamable;
|
||||
Disposed -= UnsubscribeStreamable;
|
||||
|
||||
Streamable.StreamingBegin -= OnStreamingBeginShow;
|
||||
Streamable.StreamingBegin -= OnStreamingBegin;
|
||||
@@ -122,8 +122,8 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
||||
/// <summary>
|
||||
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
|
||||
/// </summary>
|
||||
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(() => Close());
|
||||
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
|
||||
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThreadAsync(Close);
|
||||
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThreadAsync(Dispose);
|
||||
|
||||
/// <summary>
|
||||
/// If StreamingBegin is fired from a worker thread, the window will be created on that
|
||||
@@ -132,7 +132,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
||||
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
|
||||
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
|
||||
/// </summary>
|
||||
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show);
|
||||
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThreadAsync(Show);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.Threading;
|
||||
using LibationWinForms.BookLiberation.BaseForms;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
@@ -21,7 +20,7 @@ namespace LibationWinForms.BookLiberation
|
||||
#region IStreamable event handler overrides
|
||||
public override void OnStreamingBegin(object sender, string beginString)
|
||||
{
|
||||
filenameLbl.UIThread(() => filenameLbl.Text = beginString);
|
||||
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
|
||||
}
|
||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||
{
|
||||
@@ -29,11 +28,11 @@ namespace LibationWinForms.BookLiberation
|
||||
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
||||
return;
|
||||
|
||||
progressLbl.UIThread(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}");
|
||||
progressLbl.UIThreadAsync(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}");
|
||||
|
||||
var d = double.Parse(downloadProgress.BytesReceived.ToString()) / double.Parse(downloadProgress.TotalBytesToReceive.Value.ToString()) * 100.0;
|
||||
var i = int.Parse(Math.Truncate(d).ToString());
|
||||
progressBar1.UIThread(() => progressBar1.Value = i);
|
||||
progressBar1.UIThreadAsync(() => progressBar1.Value = i);
|
||||
|
||||
lastDownloadProgress = DateTime.Now;
|
||||
}
|
||||
@@ -50,14 +49,14 @@ namespace LibationWinForms.BookLiberation
|
||||
private void timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
// if no update in the last 30 seconds, display frozen label
|
||||
lastUpdateLbl.UIThread(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
|
||||
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
|
||||
if (lastUpdateLbl.Visible)
|
||||
{
|
||||
var diff = DateTime.Now - lastDownloadProgress;
|
||||
var min = (int)diff.TotalMinutes;
|
||||
var minText = min > 0 ? $"{min}min " : "";
|
||||
|
||||
lastUpdateLbl.UIThread(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
|
||||
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
|
||||
}
|
||||
}
|
||||
private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop();
|
||||
|
||||
@@ -50,24 +50,24 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public static class ProcessorAutomationController
|
||||
{
|
||||
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
|
||||
public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
|
||||
|
||||
var logMe = LogMe.RegisterForm();
|
||||
var backupBook = CreateBackupBook(completedAction, logMe);
|
||||
var backupBook = CreateBackupBook(logMe);
|
||||
|
||||
// continue even if libraryBook is null. we'll display even that in the processing box
|
||||
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
|
||||
}
|
||||
|
||||
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
public static async Task BackupAllBooksAsync()
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
|
||||
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
var backupBook = CreateBackupBook(completedAction, logMe);
|
||||
var backupBook = CreateBackupBook(logMe);
|
||||
|
||||
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
@@ -84,19 +84,19 @@ namespace LibationWinForms.BookLiberation
|
||||
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
|
||||
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
public static async Task BackupAllPdfsAsync()
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
|
||||
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
|
||||
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction);
|
||||
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
|
||||
|
||||
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
|
||||
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
|
||||
private static IProcessable CreateBackupBook(LogMe logMe)
|
||||
{
|
||||
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
|
||||
|
||||
@@ -104,7 +104,6 @@ namespace LibationWinForms.BookLiberation
|
||||
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||
{
|
||||
await downloadPdf.TryProcessAsync(e);
|
||||
completedAction(sender, e);
|
||||
}
|
||||
|
||||
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
|
||||
@@ -176,7 +175,7 @@ namespace LibationWinForms.BookLiberation
|
||||
protected abstract string SkipDialogText { get; }
|
||||
protected abstract MessageBoxButtons SkipDialogButtons { get; }
|
||||
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
|
||||
protected abstract DialogResult CreateSkipFileResult { get; }
|
||||
protected abstract DialogResult SkipResult { get; }
|
||||
|
||||
public async Task RunBackupAsync()
|
||||
{
|
||||
@@ -197,8 +196,6 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
string logMessage;
|
||||
|
||||
try
|
||||
{
|
||||
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
|
||||
@@ -208,18 +205,28 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
LogMe.Error(errorMessage);
|
||||
|
||||
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMe.Error(ex);
|
||||
|
||||
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
|
||||
}
|
||||
|
||||
return showRetry(libraryBook);
|
||||
}
|
||||
|
||||
private bool showRetry(LibraryBook libraryBook)
|
||||
{
|
||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||
|
||||
DialogResult? dialogResult = FileManager.Configuration.Instance.BadBook switch
|
||||
{
|
||||
FileManager.Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
FileManager.Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
FileManager.Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
||||
FileManager.Configuration.BadBookAction.Ask => null,
|
||||
_ => null
|
||||
};
|
||||
|
||||
string details;
|
||||
try
|
||||
{
|
||||
@@ -239,20 +246,16 @@ $@" Title: {libraryBook.Book.Title}
|
||||
details = "[Error retrieving details]";
|
||||
}
|
||||
|
||||
var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
|
||||
// if null then ask user
|
||||
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
|
||||
|
||||
if (dialogResult == DialogResult.Abort)
|
||||
return false;
|
||||
|
||||
if (dialogResult == CreateSkipFileResult)
|
||||
if (dialogResult == SkipResult)
|
||||
{
|
||||
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
|
||||
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
|
||||
LogMe.Info($@"
|
||||
Created new 'skip' file
|
||||
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
|
||||
{path}
|
||||
".Trim());
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
||||
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -273,7 +276,7 @@ An error occurred while trying to process this book. Skip this book permanently?
|
||||
".Trim();
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
|
||||
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
|
||||
protected override DialogResult SkipResult => DialogResult.Yes;
|
||||
|
||||
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
|
||||
: base(logMe, processable)
|
||||
@@ -294,7 +297,7 @@ An error occurred while trying to process this book. Skip this book permanently?
|
||||
An error occurred while trying to process this book.
|
||||
{0}
|
||||
|
||||
- ABORT: stop processing books.
|
||||
- ABORT: Stop processing books.
|
||||
|
||||
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
|
||||
|
||||
@@ -302,7 +305,7 @@ An error occurred while trying to process this book.
|
||||
".Trim();
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
|
||||
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
|
||||
protected override DialogResult SkipResult => DialogResult.Ignore;
|
||||
|
||||
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||
: base(logMe, processable, automatedBackupsForm) { }
|
||||
@@ -310,7 +313,7 @@ An error occurred while trying to process this book.
|
||||
protected override async Task RunAsync()
|
||||
{
|
||||
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
|
||||
foreach (var libraryBook in Processable.GetValidLibraryBooks())
|
||||
foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))
|
||||
{
|
||||
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
|
||||
if (!keepGoing)
|
||||
|
||||
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal file
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class DataGridViewImageButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
|
||||
{
|
||||
var w = image.Width;
|
||||
var h = image.Height;
|
||||
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
|
||||
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
|
||||
|
||||
graphics.DrawImage(image, new Rectangle(x, y, w, h));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public abstract class DataGridViewImageButtonColumn : DataGridViewButtonColumn
|
||||
{
|
||||
private DataGridViewImageButtonCell _cellTemplate;
|
||||
public override DataGridViewCell CellTemplate
|
||||
{
|
||||
get => GetCellTemplate();
|
||||
set
|
||||
{
|
||||
if (value is DataGridViewImageButtonCell cellTemplate)
|
||||
_cellTemplate = cellTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract DataGridViewImageButtonCell NewCell();
|
||||
|
||||
private DataGridViewImageButtonCell GetCellTemplate()
|
||||
{
|
||||
if (_cellTemplate is null)
|
||||
return NewCell();
|
||||
else
|
||||
return _cellTemplate;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
var clone = (DataGridViewImageButtonColumn)base.Clone();
|
||||
clone._cellTemplate = _cellTemplate;
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
public class DataGridViewImageButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
|
||||
{
|
||||
var w = image.Width;
|
||||
var h = image.Height;
|
||||
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
|
||||
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
|
||||
|
||||
graphics.DrawImage(image, new Rectangle(x, y, w, h));
|
||||
}
|
||||
}
|
||||
}
|
||||
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
@@ -28,64 +28,210 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SaveBtn = new System.Windows.Forms.Button();
|
||||
this.newTagsTb = new System.Windows.Forms.TextBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// SaveBtn
|
||||
//
|
||||
this.SaveBtn.Location = new System.Drawing.Point(396, 25);
|
||||
this.SaveBtn.Name = "SaveBtn";
|
||||
this.SaveBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.SaveBtn.TabIndex = 1;
|
||||
this.SaveBtn.Text = "Save";
|
||||
this.SaveBtn.UseVisualStyleBackColor = true;
|
||||
this.SaveBtn.Click += new System.EventHandler(this.SaveBtn_Click);
|
||||
//
|
||||
// newTagsTb
|
||||
//
|
||||
this.newTagsTb.Location = new System.Drawing.Point(12, 27);
|
||||
this.newTagsTb.Name = "newTagsTb";
|
||||
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.newTagsTb.Size = new System.Drawing.Size(375, 20);
|
||||
this.newTagsTb.TabIndex = 0;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(424, 13);
|
||||
this.label1.TabIndex = 2;
|
||||
this.label1.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.newTagsTb = new System.Windows.Forms.TextBox();
|
||||
this.tagsDescLbl = new System.Windows.Forms.Label();
|
||||
this.coverPb = new System.Windows.Forms.PictureBox();
|
||||
this.detailsTb = new System.Windows.Forms.TextBox();
|
||||
this.tagsGb = new System.Windows.Forms.GroupBox();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.liberatedGb = new System.Windows.Forms.GroupBox();
|
||||
this.pdfLiberatedCb = new System.Windows.Forms.ComboBox();
|
||||
this.pdfLiberatedLbl = new System.Windows.Forms.Label();
|
||||
this.bookLiberatedCb = new System.Windows.Forms.ComboBox();
|
||||
this.bookLiberatedLbl = new System.Windows.Forms.Label();
|
||||
this.liberatedDescLbl = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this.coverPb)).BeginInit();
|
||||
this.tagsGb.SuspendLayout();
|
||||
this.liberatedGb.SuspendLayout();
|
||||
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(376, 427);
|
||||
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 = 3;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// newTagsTb
|
||||
//
|
||||
this.newTagsTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.newTagsTb.Location = new System.Drawing.Point(7, 40);
|
||||
this.newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.newTagsTb.Name = "newTagsTb";
|
||||
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.newTagsTb.Size = new System.Drawing.Size(556, 23);
|
||||
this.newTagsTb.TabIndex = 1;
|
||||
//
|
||||
// tagsDescLbl
|
||||
//
|
||||
this.tagsDescLbl.AutoSize = true;
|
||||
this.tagsDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
this.tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.tagsDescLbl.Name = "tagsDescLbl";
|
||||
this.tagsDescLbl.Size = new System.Drawing.Size(458, 15);
|
||||
this.tagsDescLbl.TabIndex = 0;
|
||||
this.tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
|
||||
"ores";
|
||||
//
|
||||
// EditTagsDialog
|
||||
//
|
||||
this.AcceptButton = this.SaveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(483, 60);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.newTagsTb);
|
||||
this.Controls.Add(this.SaveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "EditTagsDialog";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Tags";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
//
|
||||
// coverPb
|
||||
//
|
||||
this.coverPb.Location = new System.Drawing.Point(12, 12);
|
||||
this.coverPb.Name = "coverPb";
|
||||
this.coverPb.Size = new System.Drawing.Size(80, 80);
|
||||
this.coverPb.TabIndex = 3;
|
||||
this.coverPb.TabStop = false;
|
||||
//
|
||||
// detailsTb
|
||||
//
|
||||
this.detailsTb.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.detailsTb.Location = new System.Drawing.Point(98, 12);
|
||||
this.detailsTb.Multiline = true;
|
||||
this.detailsTb.Name = "detailsTb";
|
||||
this.detailsTb.ReadOnly = true;
|
||||
this.detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||
this.detailsTb.Size = new System.Drawing.Size(484, 202);
|
||||
this.detailsTb.TabIndex = 1;
|
||||
//
|
||||
// tagsGb
|
||||
//
|
||||
this.tagsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.tagsGb.Controls.Add(this.tagsDescLbl);
|
||||
this.tagsGb.Controls.Add(this.newTagsTb);
|
||||
this.tagsGb.Location = new System.Drawing.Point(12, 220);
|
||||
this.tagsGb.Name = "tagsGb";
|
||||
this.tagsGb.Size = new System.Drawing.Size(570, 73);
|
||||
this.tagsGb.TabIndex = 0;
|
||||
this.tagsGb.TabStop = false;
|
||||
this.tagsGb.Text = "Edit Tags";
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.Location = new System.Drawing.Point(494, 427);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 4;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// liberatedGb
|
||||
//
|
||||
this.liberatedGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.liberatedGb.Controls.Add(this.pdfLiberatedCb);
|
||||
this.liberatedGb.Controls.Add(this.pdfLiberatedLbl);
|
||||
this.liberatedGb.Controls.Add(this.bookLiberatedCb);
|
||||
this.liberatedGb.Controls.Add(this.bookLiberatedLbl);
|
||||
this.liberatedGb.Controls.Add(this.liberatedDescLbl);
|
||||
this.liberatedGb.Location = new System.Drawing.Point(12, 299);
|
||||
this.liberatedGb.Name = "liberatedGb";
|
||||
this.liberatedGb.Size = new System.Drawing.Size(570, 122);
|
||||
this.liberatedGb.TabIndex = 2;
|
||||
this.liberatedGb.TabStop = false;
|
||||
this.liberatedGb.Text = "Liberated status: Whether the book/pdf has been downloaded";
|
||||
//
|
||||
// pdfLiberatedCb
|
||||
//
|
||||
this.pdfLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.pdfLiberatedCb.FormattingEnabled = true;
|
||||
this.pdfLiberatedCb.Location = new System.Drawing.Point(244, 86);
|
||||
this.pdfLiberatedCb.Name = "pdfLiberatedCb";
|
||||
this.pdfLiberatedCb.Size = new System.Drawing.Size(121, 23);
|
||||
this.pdfLiberatedCb.TabIndex = 4;
|
||||
//
|
||||
// pdfLiberatedLbl
|
||||
//
|
||||
this.pdfLiberatedLbl.AutoSize = true;
|
||||
this.pdfLiberatedLbl.Location = new System.Drawing.Point(210, 89);
|
||||
this.pdfLiberatedLbl.Name = "pdfLiberatedLbl";
|
||||
this.pdfLiberatedLbl.Size = new System.Drawing.Size(28, 15);
|
||||
this.pdfLiberatedLbl.TabIndex = 3;
|
||||
this.pdfLiberatedLbl.Text = "PDF";
|
||||
//
|
||||
// bookLiberatedCb
|
||||
//
|
||||
this.bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.bookLiberatedCb.FormattingEnabled = true;
|
||||
this.bookLiberatedCb.Location = new System.Drawing.Point(47, 86);
|
||||
this.bookLiberatedCb.Name = "bookLiberatedCb";
|
||||
this.bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
|
||||
this.bookLiberatedCb.TabIndex = 2;
|
||||
//
|
||||
// bookLiberatedLbl
|
||||
//
|
||||
this.bookLiberatedLbl.AutoSize = true;
|
||||
this.bookLiberatedLbl.Location = new System.Drawing.Point(7, 89);
|
||||
this.bookLiberatedLbl.Name = "bookLiberatedLbl";
|
||||
this.bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
|
||||
this.bookLiberatedLbl.TabIndex = 1;
|
||||
this.bookLiberatedLbl.Text = "Book";
|
||||
//
|
||||
// liberatedDescLbl
|
||||
//
|
||||
this.liberatedDescLbl.AutoSize = true;
|
||||
this.liberatedDescLbl.Location = new System.Drawing.Point(20, 31);
|
||||
this.liberatedDescLbl.Name = "liberatedDescLbl";
|
||||
this.liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
|
||||
this.liberatedDescLbl.TabIndex = 0;
|
||||
this.liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to" +
|
||||
" Downloaded";
|
||||
//
|
||||
// BookDetailsDialog
|
||||
//
|
||||
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(594, 466);
|
||||
this.Controls.Add(this.liberatedGb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.tagsGb);
|
||||
this.Controls.Add(this.detailsTb);
|
||||
this.Controls.Add(this.coverPb);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
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 = "BookDetailsDialog";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Book Details";
|
||||
((System.ComponentModel.ISupportInitialize)(this.coverPb)).EndInit();
|
||||
this.tagsGb.ResumeLayout(false);
|
||||
this.tagsGb.PerformLayout();
|
||||
this.liberatedGb.ResumeLayout(false);
|
||||
this.liberatedGb.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button SaveBtn;
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.TextBox newTagsTb;
|
||||
private System.Windows.Forms.Label label1;
|
||||
}
|
||||
private System.Windows.Forms.Label tagsDescLbl;
|
||||
private System.Windows.Forms.PictureBox coverPb;
|
||||
private System.Windows.Forms.TextBox detailsTb;
|
||||
private System.Windows.Forms.GroupBox tagsGb;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.GroupBox liberatedGb;
|
||||
private System.Windows.Forms.ComboBox pdfLiberatedCb;
|
||||
private System.Windows.Forms.Label pdfLiberatedLbl;
|
||||
private System.Windows.Forms.ComboBox bookLiberatedCb;
|
||||
private System.Windows.Forms.Label bookLiberatedLbl;
|
||||
private System.Windows.Forms.Label liberatedDescLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class BookDetailsDialog : Form
|
||||
{
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
public string Text { get; set; }
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
|
||||
public string NewTags { get; private set; }
|
||||
public LiberatedStatus BookLiberatedStatus { get; private set; }
|
||||
public LiberatedStatus? PdfLiberatedStatus { get; private set; }
|
||||
|
||||
private LibraryBook _libraryBook { get; }
|
||||
private Book Book => _libraryBook.Book;
|
||||
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
public BookDetailsDialog(string title, string rawTags) : this()
|
||||
public BookDetailsDialog(LibraryBook libraryBook) : this()
|
||||
{
|
||||
this.Text = $"Edit Tags - {title}";
|
||||
_libraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
initDetails();
|
||||
initTags();
|
||||
initLiberated();
|
||||
}
|
||||
// 1st draft: lazily cribbed from GridEntry.ctor()
|
||||
private void initDetails()
|
||||
{
|
||||
this.Text = Book.Title;
|
||||
|
||||
this.newTagsTb.Text = rawTags;
|
||||
(var isDefault, var picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
||||
this.coverPb.Image = Dinah.Core.Drawing.ImageReader.ToImage(picture);
|
||||
|
||||
var t = @$"
|
||||
Title: {Book.Title}
|
||||
Author(s): {Book.AuthorNames}
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(" > ", Book.CategoriesNames)}
|
||||
Purchase Date: {_libraryBook.DateAdded.ToString("d")}
|
||||
".Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Book.SeriesNames))
|
||||
t += $"\r\nSeries: {Book.SeriesNames}";
|
||||
|
||||
var bookRating = Book.Rating?.ToStarString();
|
||||
if (!string.IsNullOrWhiteSpace(bookRating))
|
||||
t += $"\r\nBook Rating:\r\n{bookRating}";
|
||||
|
||||
var myRating = Book.UserDefinedItem.Rating?.ToStarString();
|
||||
if (!string.IsNullOrWhiteSpace(myRating))
|
||||
t += $"\r\nMy Rating:\r\n{myRating}";
|
||||
|
||||
this.detailsTb.Text = t;
|
||||
}
|
||||
private void initTags() => this.newTagsTb.Text = Book.UserDefinedItem.Tags;
|
||||
private void initLiberated()
|
||||
{
|
||||
{
|
||||
var status = Book.UserDefinedItem.BookStatus;
|
||||
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
|
||||
// this should only appear if is already an error. User should not be able to set status to error, only away from error
|
||||
if (status == LiberatedStatus.Error)
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Error, Text = "Error" });
|
||||
|
||||
setDefaultComboBox(this.bookLiberatedCb, status);
|
||||
}
|
||||
|
||||
{
|
||||
var status = Book.UserDefinedItem.PdfStatus;
|
||||
|
||||
if (status is null)
|
||||
this.pdfLiberatedCb.Enabled = false;
|
||||
else
|
||||
{
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
|
||||
setDefaultComboBox(this.pdfLiberatedCb, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void setDefaultComboBox(ComboBox comboBox, LiberatedStatus? status)
|
||||
{
|
||||
if (!status.HasValue)
|
||||
{
|
||||
comboBox.SelectedIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var item = comboBox.Items.Cast<liberatedComboBoxItem>().SingleOrDefault(item => item.Status == status.Value);
|
||||
if (item is not null)
|
||||
comboBox.SelectedItem = item;
|
||||
else
|
||||
comboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void SaveBtn_Click(object sender, EventArgs e)
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
NewTags = this.newTagsTb.Text;
|
||||
DialogResult = DialogResult.OK;
|
||||
|
||||
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
|
||||
|
||||
if (this.pdfLiberatedCb.Enabled)
|
||||
PdfLiberatedStatus = ((liberatedComboBoxItem)this.pdfLiberatedCb.SelectedItem).Status;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<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">
|
||||
|
||||
280
LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs
generated
280
LibationWinForms/Dialogs/RemoveBooksDialog.Designer.cs
generated
@@ -29,147 +29,147 @@ namespace LibationWinForms.Dialogs
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this._dataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.removeDataGridViewCheckBoxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.coverDataGridViewImageColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
||||
this.btnRemoveBooks = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _dataGridView
|
||||
//
|
||||
this._dataGridView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this._dataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.removeDataGridViewCheckBoxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.coverDataGridViewImageColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
||||
this.btnRemoveBooks = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _dataGridView
|
||||
//
|
||||
this._dataGridView.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._dataGridView.AutoGenerateColumns = false;
|
||||
this._dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this._dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this._dataGridView.AutoGenerateColumns = false;
|
||||
this._dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this._dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.removeDataGridViewCheckBoxColumn,
|
||||
this.coverDataGridViewImageColumn,
|
||||
this.titleDataGridViewTextBoxColumn,
|
||||
this.authorsDataGridViewTextBoxColumn,
|
||||
this.miscDataGridViewTextBoxColumn,
|
||||
this.purchaseDateGridViewTextBoxColumn});
|
||||
this._dataGridView.DataSource = this.gridEntryBindingSource;
|
||||
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this._dataGridView.DefaultCellStyle = dataGridViewCellStyle2;
|
||||
this._dataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this._dataGridView.Name = "_dataGridView";
|
||||
this._dataGridView.RowHeadersVisible = false;
|
||||
this._dataGridView.RowTemplate.Height = 82;
|
||||
this._dataGridView.Size = new System.Drawing.Size(800, 409);
|
||||
this._dataGridView.TabIndex = 0;
|
||||
//
|
||||
// removeDataGridViewCheckBoxColumn
|
||||
//
|
||||
this.removeDataGridViewCheckBoxColumn.DataPropertyName = "Remove";
|
||||
this.removeDataGridViewCheckBoxColumn.FalseValue = "False";
|
||||
this.removeDataGridViewCheckBoxColumn.Frozen = true;
|
||||
this.removeDataGridViewCheckBoxColumn.HeaderText = "Remove";
|
||||
this.removeDataGridViewCheckBoxColumn.MinimumWidth = 60;
|
||||
this.removeDataGridViewCheckBoxColumn.Name = "removeDataGridViewCheckBoxColumn";
|
||||
this.removeDataGridViewCheckBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.removeDataGridViewCheckBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.removeDataGridViewCheckBoxColumn.TrueValue = "True";
|
||||
this.removeDataGridViewCheckBoxColumn.Width = 60;
|
||||
//
|
||||
// coverDataGridViewImageColumn
|
||||
//
|
||||
this.coverDataGridViewImageColumn.DataPropertyName = "Cover";
|
||||
this.coverDataGridViewImageColumn.HeaderText = "Cover";
|
||||
this.coverDataGridViewImageColumn.MinimumWidth = 80;
|
||||
this.coverDataGridViewImageColumn.Name = "coverDataGridViewImageColumn";
|
||||
this.coverDataGridViewImageColumn.ReadOnly = true;
|
||||
this.coverDataGridViewImageColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.coverDataGridViewImageColumn.Width = 80;
|
||||
//
|
||||
// titleDataGridViewTextBoxColumn
|
||||
//
|
||||
this.titleDataGridViewTextBoxColumn.DataPropertyName = "Title";
|
||||
this.titleDataGridViewTextBoxColumn.HeaderText = "Title";
|
||||
this.titleDataGridViewTextBoxColumn.Name = "titleDataGridViewTextBoxColumn";
|
||||
this.titleDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.titleDataGridViewTextBoxColumn.Width = 200;
|
||||
//
|
||||
// authorsDataGridViewTextBoxColumn
|
||||
//
|
||||
this.authorsDataGridViewTextBoxColumn.DataPropertyName = "Authors";
|
||||
this.authorsDataGridViewTextBoxColumn.HeaderText = "Authors";
|
||||
this.authorsDataGridViewTextBoxColumn.Name = "authorsDataGridViewTextBoxColumn";
|
||||
this.authorsDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
//
|
||||
// miscDataGridViewTextBoxColumn
|
||||
//
|
||||
this.miscDataGridViewTextBoxColumn.DataPropertyName = "Misc";
|
||||
this.miscDataGridViewTextBoxColumn.HeaderText = "Misc";
|
||||
this.miscDataGridViewTextBoxColumn.Name = "miscDataGridViewTextBoxColumn";
|
||||
this.miscDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.miscDataGridViewTextBoxColumn.Width = 150;
|
||||
//
|
||||
// purchaseDateGridViewTextBoxColumn
|
||||
//
|
||||
this.purchaseDateGridViewTextBoxColumn.DataPropertyName = "PurchaseDate";
|
||||
this.purchaseDateGridViewTextBoxColumn.HeaderText = "Purchase Date";
|
||||
this.purchaseDateGridViewTextBoxColumn.Name = "purchaseDateGridViewTextBoxColumn";
|
||||
this.purchaseDateGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.purchaseDateGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
//
|
||||
// gridEntryBindingSource
|
||||
//
|
||||
this.gridEntryBindingSource.AllowNew = false;
|
||||
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.Dialogs.RemovableGridEntry);
|
||||
//
|
||||
// btnRemoveBooks
|
||||
//
|
||||
this.btnRemoveBooks.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnRemoveBooks.Location = new System.Drawing.Point(570, 419);
|
||||
this.btnRemoveBooks.Name = "btnRemoveBooks";
|
||||
this.btnRemoveBooks.Size = new System.Drawing.Size(218, 23);
|
||||
this.btnRemoveBooks.TabIndex = 1;
|
||||
this.btnRemoveBooks.Text = "Remove Selected Books from Libation";
|
||||
this.btnRemoveBooks.UseVisualStyleBackColor = true;
|
||||
this.btnRemoveBooks.Click += new System.EventHandler(this.btnRemoveBooks_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 423);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(178, 15);
|
||||
this.label1.TabIndex = 2;
|
||||
this.label1.Text = "{0} book{1} selected for removal.";
|
||||
//
|
||||
// RemoveBooksDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.btnRemoveBooks);
|
||||
this.Controls.Add(this._dataGridView);
|
||||
this.Name = "RemoveBooksDialog";
|
||||
this.Text = "RemoveBooksDialog";
|
||||
this.Shown += new System.EventHandler(this.RemoveBooksDialog_Shown);
|
||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this._dataGridView.DataSource = this.gridEntryBindingSource;
|
||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this._dataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
this._dataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this._dataGridView.Name = "_dataGridView";
|
||||
this._dataGridView.RowHeadersVisible = false;
|
||||
this._dataGridView.RowTemplate.Height = 82;
|
||||
this._dataGridView.Size = new System.Drawing.Size(730, 409);
|
||||
this._dataGridView.TabIndex = 0;
|
||||
//
|
||||
// removeDataGridViewCheckBoxColumn
|
||||
//
|
||||
this.removeDataGridViewCheckBoxColumn.DataPropertyName = "Remove";
|
||||
this.removeDataGridViewCheckBoxColumn.FalseValue = "False";
|
||||
this.removeDataGridViewCheckBoxColumn.Frozen = true;
|
||||
this.removeDataGridViewCheckBoxColumn.HeaderText = "Remove";
|
||||
this.removeDataGridViewCheckBoxColumn.MinimumWidth = 80;
|
||||
this.removeDataGridViewCheckBoxColumn.Name = "removeDataGridViewCheckBoxColumn";
|
||||
this.removeDataGridViewCheckBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.removeDataGridViewCheckBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.removeDataGridViewCheckBoxColumn.TrueValue = "True";
|
||||
this.removeDataGridViewCheckBoxColumn.Width = 80;
|
||||
//
|
||||
// coverDataGridViewImageColumn
|
||||
//
|
||||
this.coverDataGridViewImageColumn.DataPropertyName = "Cover";
|
||||
this.coverDataGridViewImageColumn.HeaderText = "Cover";
|
||||
this.coverDataGridViewImageColumn.MinimumWidth = 80;
|
||||
this.coverDataGridViewImageColumn.Name = "coverDataGridViewImageColumn";
|
||||
this.coverDataGridViewImageColumn.ReadOnly = true;
|
||||
this.coverDataGridViewImageColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.coverDataGridViewImageColumn.Width = 80;
|
||||
//
|
||||
// titleDataGridViewTextBoxColumn
|
||||
//
|
||||
this.titleDataGridViewTextBoxColumn.DataPropertyName = "Title";
|
||||
this.titleDataGridViewTextBoxColumn.HeaderText = "Title";
|
||||
this.titleDataGridViewTextBoxColumn.Name = "titleDataGridViewTextBoxColumn";
|
||||
this.titleDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.titleDataGridViewTextBoxColumn.Width = 200;
|
||||
//
|
||||
// authorsDataGridViewTextBoxColumn
|
||||
//
|
||||
this.authorsDataGridViewTextBoxColumn.DataPropertyName = "Authors";
|
||||
this.authorsDataGridViewTextBoxColumn.HeaderText = "Authors";
|
||||
this.authorsDataGridViewTextBoxColumn.Name = "authorsDataGridViewTextBoxColumn";
|
||||
this.authorsDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
//
|
||||
// miscDataGridViewTextBoxColumn
|
||||
//
|
||||
this.miscDataGridViewTextBoxColumn.DataPropertyName = "Misc";
|
||||
this.miscDataGridViewTextBoxColumn.HeaderText = "Misc";
|
||||
this.miscDataGridViewTextBoxColumn.Name = "miscDataGridViewTextBoxColumn";
|
||||
this.miscDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.miscDataGridViewTextBoxColumn.Width = 150;
|
||||
//
|
||||
// purchaseDateGridViewTextBoxColumn
|
||||
//
|
||||
this.purchaseDateGridViewTextBoxColumn.DataPropertyName = "PurchaseDate";
|
||||
this.purchaseDateGridViewTextBoxColumn.HeaderText = "Purchase Date";
|
||||
this.purchaseDateGridViewTextBoxColumn.Name = "purchaseDateGridViewTextBoxColumn";
|
||||
this.purchaseDateGridViewTextBoxColumn.ReadOnly = true;
|
||||
this.purchaseDateGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
//
|
||||
// gridEntryBindingSource
|
||||
//
|
||||
this.gridEntryBindingSource.AllowNew = false;
|
||||
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.Dialogs.RemovableGridEntry);
|
||||
//
|
||||
// btnRemoveBooks
|
||||
//
|
||||
this.btnRemoveBooks.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnRemoveBooks.Location = new System.Drawing.Point(500, 419);
|
||||
this.btnRemoveBooks.Name = "btnRemoveBooks";
|
||||
this.btnRemoveBooks.Size = new System.Drawing.Size(218, 23);
|
||||
this.btnRemoveBooks.TabIndex = 1;
|
||||
this.btnRemoveBooks.Text = "Remove Selected Books from Libation";
|
||||
this.btnRemoveBooks.UseVisualStyleBackColor = true;
|
||||
this.btnRemoveBooks.Click += new System.EventHandler(this.btnRemoveBooks_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 423);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(178, 15);
|
||||
this.label1.TabIndex = 2;
|
||||
this.label1.Text = "{0} book{1} selected for removal.";
|
||||
//
|
||||
// RemoveBooksDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(730, 450);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.btnRemoveBooks);
|
||||
this.Controls.Add(this._dataGridView);
|
||||
this.Name = "RemoveBooksDialog";
|
||||
this.Text = "RemoveBooksDialog";
|
||||
this.Shown += new System.EventHandler(this.RemoveBooksDialog_Shown);
|
||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -179,11 +179,11 @@ namespace LibationWinForms.Dialogs
|
||||
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
||||
private System.Windows.Forms.Button btnRemoveBooks;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewImageColumn coverDataGridViewImageColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn titleDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn authorsDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGridViewTextBoxColumn;
|
||||
}
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewImageColumn coverDataGridViewImageColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn titleDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn authorsDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGridViewTextBoxColumn;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
using System;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Login;
|
||||
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 DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Login;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class RemoveBooksDialog : Form
|
||||
{
|
||||
public bool BooksRemoved { get; private set; }
|
||||
|
||||
private Account[] _accounts { get; }
|
||||
private readonly List<LibraryBook> _libraryBooks;
|
||||
private readonly SortableBindingList<RemovableGridEntry> _removableGridEntries;
|
||||
@@ -26,15 +25,19 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
public RemoveBooksDialog(params Account[] accounts)
|
||||
{
|
||||
_libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
|
||||
_libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
_accounts = accounts;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
_labelFormat = label1.Text;
|
||||
|
||||
_dataGridView.CellContentClick += (s, e) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||
_dataGridView.CellValueChanged += DataGridView1_CellValueChanged;
|
||||
_dataGridView.BindingContextChanged += (s, e) => UpdateSelection();
|
||||
_dataGridView.CellContentClick += (_, _) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||
_dataGridView.CellValueChanged += (_, _) => UpdateSelection();
|
||||
_dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged;
|
||||
|
||||
var orderedGridEntries = _libraryBooks
|
||||
.Select(lb => new RemovableGridEntry(lb))
|
||||
@@ -47,10 +50,10 @@ namespace LibationWinForms.Dialogs
|
||||
_dataGridView.Enabled = false;
|
||||
}
|
||||
|
||||
private void DataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||||
private void _dataGridView_BindingContextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (e.ColumnIndex == 0)
|
||||
UpdateSelection();
|
||||
_dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending);
|
||||
UpdateSelection();
|
||||
}
|
||||
|
||||
private async void RemoveBooksDialog_Shown(object sender, EventArgs e)
|
||||
@@ -59,9 +62,9 @@ namespace LibationWinForms.Dialogs
|
||||
return;
|
||||
try
|
||||
{
|
||||
var rmovedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
|
||||
|
||||
var removable = _removableGridEntries.Where(rge => rmovedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId));
|
||||
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)).ToList();
|
||||
|
||||
if (!removable.Any())
|
||||
return;
|
||||
@@ -84,7 +87,7 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void btnRemoveBooks_Click(object sender, EventArgs e)
|
||||
private async void btnRemoveBooks_Click(object sender, EventArgs e)
|
||||
{
|
||||
var selectedBooks = SelectedEntries.ToList();
|
||||
|
||||
@@ -105,26 +108,18 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
if (result == DialogResult.Yes)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var libBooks = context.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var removeLibraryBooks = libBooks.Where(lb => selectedBooks.Any(rge => rge.AudibleProductId == lb.Book.AudibleProductId)).ToList();
|
||||
|
||||
context.Library.RemoveRange(removeLibraryBooks);
|
||||
context.SaveChanges();
|
||||
var idsToRemove = selectedBooks.Select(rge => rge.AudibleProductId).ToList();
|
||||
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
|
||||
|
||||
foreach (var rEntry in selectedBooks)
|
||||
_removableGridEntries.Remove(rEntry);
|
||||
|
||||
BooksRemoved = removeLibraryBooks.Count > 0;
|
||||
|
||||
UpdateSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelection()
|
||||
{
|
||||
_dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending);
|
||||
var selectedCount = SelectedCount;
|
||||
label1.Text = string.Format(_labelFormat, selectedCount, selectedCount != 1 ? "s" : string.Empty);
|
||||
btnRemoveBooks.Enabled = selectedCount > 0;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<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">
|
||||
@@ -58,4 +57,7 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="gridEntryBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
165
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
165
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -33,16 +33,24 @@
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
||||
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.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.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
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();
|
||||
//
|
||||
@@ -53,27 +61,27 @@
|
||||
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 = 2;
|
||||
this.booksLocationDescLbl.TabIndex = 1;
|
||||
this.booksLocationDescLbl.Text = "[book desc]";
|
||||
//
|
||||
// inProgressDescLbl
|
||||
//
|
||||
this.inProgressDescLbl.AutoSize = true;
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 127);
|
||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 149);
|
||||
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 = 1;
|
||||
this.inProgressDescLbl.TabIndex = 15;
|
||||
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, 419);
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 445);
|
||||
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 = 4;
|
||||
this.saveBtn.TabIndex = 17;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
@@ -82,40 +90,122 @@
|
||||
//
|
||||
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, 419);
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 445);
|
||||
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 = 5;
|
||||
this.cancelBtn.TabIndex = 18;
|
||||
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.Left)
|
||||
this.advancedSettingsGb.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.convertLossyRb);
|
||||
this.advancedSettingsGb.Controls.Add(this.convertLosslessRb);
|
||||
this.advancedSettingsGb.Controls.Add(this.badBookGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
|
||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||
this.advancedSettingsGb.Controls.Add(this.allowLibationFixupCbox);
|
||||
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, 232);
|
||||
this.advancedSettingsGb.TabIndex = 5;
|
||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 258);
|
||||
this.advancedSettingsGb.TabIndex = 6;
|
||||
this.advancedSettingsGb.TabStop = false;
|
||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// 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, 22);
|
||||
this.badBookGb.Name = "badBookGb";
|
||||
this.badBookGb.Size = new System.Drawing.Size(529, 124);
|
||||
this.badBookGb.TabIndex = 11;
|
||||
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 = 15;
|
||||
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 = 14;
|
||||
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 = 13;
|
||||
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 = 12;
|
||||
this.badBookAskRb.TabStop = true;
|
||||
this.badBookAskRb.Text = "[ask desc]";
|
||||
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptAndConvertGb
|
||||
//
|
||||
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, 22);
|
||||
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||
this.decryptAndConvertGb.TabIndex = 7;
|
||||
this.decryptAndConvertGb.TabStop = false;
|
||||
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||
//
|
||||
// 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(262, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 8;
|
||||
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
||||
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(7, 88);
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 81);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(242, 19);
|
||||
this.convertLossyRb.TabIndex = 0;
|
||||
this.convertLossyRb.TabIndex = 10;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (Lossy)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -123,10 +213,10 @@
|
||||
//
|
||||
this.convertLosslessRb.AutoSize = true;
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(7, 63);
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 56);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(327, 19);
|
||||
this.convertLosslessRb.TabIndex = 0;
|
||||
this.convertLosslessRb.TabIndex = 9;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books as .M4B files (Lossless Mp4a format)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
@@ -135,30 +225,17 @@
|
||||
//
|
||||
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(10, 175);
|
||||
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 197);
|
||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
|
||||
this.inProgressSelectControl.TabIndex = 2;
|
||||
//
|
||||
// allowLibationFixupCbox
|
||||
//
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(7, 22);
|
||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 0;
|
||||
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
this.inProgressSelectControl.TabIndex = 16;
|
||||
//
|
||||
// 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 = 4;
|
||||
this.logsBtn.TabIndex = 5;
|
||||
this.logsBtn.Text = "Open log folder";
|
||||
this.logsBtn.UseVisualStyleBackColor = true;
|
||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||
@@ -170,7 +247,7 @@
|
||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||
this.booksSelectControl.Name = "booksSelectControl";
|
||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
||||
this.booksSelectControl.TabIndex = 1;
|
||||
this.booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
@@ -181,7 +258,7 @@
|
||||
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 = 1;
|
||||
this.booksGb.TabIndex = 0;
|
||||
this.booksGb.TabStop = false;
|
||||
this.booksGb.Text = "Books location";
|
||||
//
|
||||
@@ -191,7 +268,7 @@
|
||||
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 = 2;
|
||||
this.loggingLevelLbl.TabIndex = 3;
|
||||
this.loggingLevelLbl.Text = "Logging level";
|
||||
//
|
||||
// loggingLevelCb
|
||||
@@ -201,7 +278,7 @@
|
||||
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 = 3;
|
||||
this.loggingLevelCb.TabIndex = 4;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
@@ -209,7 +286,7 @@
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 462);
|
||||
this.ClientSize = new System.Drawing.Size(933, 488);
|
||||
this.Controls.Add(this.logsBtn);
|
||||
this.Controls.Add(this.loggingLevelCb);
|
||||
this.Controls.Add(this.loggingLevelLbl);
|
||||
@@ -225,6 +302,10 @@
|
||||
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);
|
||||
@@ -247,5 +328,11 @@
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
private System.Windows.Forms.Label loggingLevelLbl;
|
||||
private System.Windows.Forms.ComboBox loggingLevelCb;
|
||||
private System.Windows.Forms.GroupBox decryptAndConvertGb;
|
||||
private System.Windows.Forms.GroupBox badBookGb;
|
||||
private System.Windows.Forms.RadioButton badBookRetryRb;
|
||||
private System.Windows.Forms.RadioButton badBookAbortRb;
|
||||
private System.Windows.Forms.RadioButton badBookAskRb;
|
||||
private System.Windows.Forms.RadioButton badBookIgnoreRb;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,21 @@ namespace LibationWinForms.Dialogs
|
||||
Configuration.KnownDirectories.LibationFiles
|
||||
}, Configuration.KnownDirectories.WinTemp);
|
||||
inProgressSelectControl.SelectDirectory(config.InProgress);
|
||||
|
||||
badBookGb.Text = desc(nameof(config.BadBook));
|
||||
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
|
||||
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
|
||||
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
||||
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
||||
var rb = config.BadBook switch
|
||||
{
|
||||
Configuration.BadBookAction.Ask => this.badBookAskRb,
|
||||
Configuration.BadBookAction.Abort => this.badBookAbortRb,
|
||||
Configuration.BadBookAction.Retry => this.badBookRetryRb,
|
||||
Configuration.BadBookAction.Ignore => this.badBookIgnoreRb,
|
||||
_ => this.badBookAskRb
|
||||
};
|
||||
rb.Checked = true;
|
||||
}
|
||||
|
||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||
@@ -111,6 +126,13 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||
|
||||
config.BadBook
|
||||
= badBookAskRb.Checked ? Configuration.BadBookAction.Ask
|
||||
: badBookAbortRb.Checked ? Configuration.BadBookAction.Abort
|
||||
: badBookRetryRb.Checked ? Configuration.BadBookAction.Retry
|
||||
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
||||
: Configuration.BadBookAction.Ask;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<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">
|
||||
|
||||
@@ -3,15 +3,17 @@ using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class EditTagsDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
|
||||
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
|
||||
{
|
||||
protected override DataGridViewImageButtonCell NewCell()
|
||||
=> new EditTagsDataGridViewImageButtonCell();
|
||||
public EditTagsDataGridViewImageButtonColumn()
|
||||
{
|
||||
CellTemplate = new EditTagsDataGridViewImageButtonCell();
|
||||
}
|
||||
}
|
||||
|
||||
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell
|
||||
{
|
||||
private static readonly Image ButtonImage = Properties.Resources.edit_tags_25x25;
|
||||
private static readonly Image ButtonImage = Properties.Resources.edit_25x25;
|
||||
private static readonly Color HiddenForeColor = Color.LightGray;
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
@@ -20,12 +22,12 @@ namespace LibationWinForms
|
||||
|
||||
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;
|
||||
|
||||
if (DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor != foreColor)
|
||||
if (DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor != foreColor)
|
||||
{
|
||||
DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor = foreColor;
|
||||
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = foreColor;
|
||||
}
|
||||
|
||||
if (tagsString.Length == 0)
|
||||
if (tagsString?.Length == 0)
|
||||
{
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
DrawButtonImage(graphics, ButtonImage, cellBounds);
|
||||
|
||||
17
LibationWinForms/Form1.Designer.cs
generated
17
LibationWinForms/Form1.Designer.cs
generated
@@ -173,18 +173,19 @@
|
||||
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
|
||||
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
|
||||
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
|
||||
//
|
||||
// removeAllAccountsToolStripMenuItem
|
||||
//
|
||||
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
|
||||
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
|
||||
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
|
||||
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
|
||||
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
|
||||
//
|
||||
// removeSomeAccountsToolStripMenuItem
|
||||
//
|
||||
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
|
||||
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
|
||||
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
|
||||
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
|
||||
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
|
||||
//
|
||||
@@ -201,23 +202,22 @@
|
||||
// beginBookBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
|
||||
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
|
||||
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// beginPdfBackupsToolStripMenuItem
|
||||
//
|
||||
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
|
||||
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// convertAllM4bToMp3ToolStripMenuItem
|
||||
//
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]";
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Visible = true;
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]...";
|
||||
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
|
||||
//
|
||||
// exportToolStripMenuItem
|
||||
@@ -310,7 +310,7 @@
|
||||
// springLbl
|
||||
//
|
||||
this.springLbl.Name = "springLbl";
|
||||
this.springLbl.Size = new System.Drawing.Size(517, 17);
|
||||
this.springLbl.Size = new System.Drawing.Size(548, 17);
|
||||
this.springLbl.Spring = true;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
@@ -353,7 +353,6 @@
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "Form1";
|
||||
this.Text = "Libation: Liberate your Library";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
|
||||
this.Load += new System.EventHandler(this.Form1_Load);
|
||||
this.menuStrip1.ResumeLayout(false);
|
||||
this.menuStrip1.PerformLayout();
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Drawing;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using Dinah.Core.Threading;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@@ -31,9 +29,11 @@ namespace LibationWinForms
|
||||
return;
|
||||
|
||||
// independent UI updates
|
||||
this.Load += setBackupCountsAsync;
|
||||
this.Load += (_, __) => RestoreSizeAndLocation();
|
||||
this.Load += (_, __) => RefreshImportMenu();
|
||||
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.Load += RefreshImportMenu;
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
@@ -46,102 +46,29 @@ namespace LibationWinForms
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
reloadGrid();
|
||||
// can't refactor into "this.Load => reloadGridAndUpdateBottomNumbers"
|
||||
// because loadInitialQuickFilterState must follow it
|
||||
reloadGridAndUpdateBottomNumbers();
|
||||
|
||||
// also applies filter. ONLY call AFTER loading grid
|
||||
loadInitialQuickFilterState();
|
||||
|
||||
}
|
||||
|
||||
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
SaveSizeAndLocation();
|
||||
}
|
||||
|
||||
private void RestoreSizeAndLocation()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var width = config.MainFormWidth;
|
||||
var height = config.MainFormHeight;
|
||||
|
||||
// too small -- something must have gone wrong. use defaults
|
||||
if (width < 25 || height < 25)
|
||||
{
|
||||
width = 1023;
|
||||
height = 578;
|
||||
}
|
||||
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (width > Screen.PrimaryScreen.WorkingArea.Width)
|
||||
width = Screen.PrimaryScreen.WorkingArea.Width;
|
||||
if (height > Screen.PrimaryScreen.WorkingArea.Height)
|
||||
height = Screen.PrimaryScreen.WorkingArea.Height;
|
||||
|
||||
var x = config.MainFormX;
|
||||
var y = config.MainFormY;
|
||||
|
||||
var rect = new System.Drawing.Rectangle(x, y, width, height);
|
||||
|
||||
// is proposed rect on a screen?
|
||||
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
||||
{
|
||||
this.StartPosition = FormStartPosition.Manual;
|
||||
this.DesktopBounds = rect;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
|
||||
this.Size = rect.Size;
|
||||
}
|
||||
|
||||
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
||||
this.WindowState = config.MainFormIsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
||||
}
|
||||
|
||||
private void SaveSizeAndLocation()
|
||||
{
|
||||
System.Drawing.Point location;
|
||||
System.Drawing.Size size;
|
||||
|
||||
// save location and size if the state is normal
|
||||
if (this.WindowState == FormWindowState.Normal)
|
||||
{
|
||||
location = this.Location;
|
||||
size = this.Size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// save the RestoreBounds if the form is minimized or maximized
|
||||
location = this.RestoreBounds.Location;
|
||||
size = this.RestoreBounds.Size;
|
||||
}
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
config.MainFormX = location.X;
|
||||
config.MainFormY = location.Y;
|
||||
|
||||
config.MainFormWidth = size.Width;
|
||||
config.MainFormHeight = size.Height;
|
||||
|
||||
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
|
||||
}
|
||||
|
||||
#region reload grid
|
||||
private bool isProcessingGridSelect = false;
|
||||
private void reloadGrid()
|
||||
private void reloadGridAndUpdateBottomNumbers(object _ = null, object __ = null)
|
||||
{
|
||||
// suppressed filter while init'ing UI
|
||||
var prev_isProcessingGridSelect = isProcessingGridSelect;
|
||||
isProcessingGridSelect = true;
|
||||
setGrid();
|
||||
this.UIThreadSync(() => setGrid());
|
||||
isProcessingGridSelect = prev_isProcessingGridSelect;
|
||||
|
||||
// UI init complete. now we can apply filter
|
||||
doFilter(lastGoodFilter);
|
||||
this.UIThreadAsync(() => doFilter(lastGoodFilter));
|
||||
|
||||
setBackupCounts(null, null);
|
||||
}
|
||||
|
||||
#region reload grid
|
||||
private ProductsGrid currProductsGrid;
|
||||
private void setGrid()
|
||||
{
|
||||
@@ -151,14 +78,12 @@ namespace LibationWinForms
|
||||
{
|
||||
gridPanel.Controls.Remove(currProductsGrid);
|
||||
currProductsGrid.VisibleCountChanged -= setVisibleCount;
|
||||
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
|
||||
currProductsGrid.Dispose();
|
||||
}
|
||||
|
||||
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
||||
currProductsGrid.VisibleCountChanged += setVisibleCount;
|
||||
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
|
||||
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
|
||||
gridPanel.UIThreadSync(() => gridPanel.Controls.Add(currProductsGrid));
|
||||
currProductsGrid.Display();
|
||||
}
|
||||
ResumeLayout();
|
||||
@@ -170,28 +95,57 @@ namespace LibationWinForms
|
||||
#endregion
|
||||
|
||||
#region bottom: backup counts
|
||||
private async void setBackupCountsAsync(object _, object __)
|
||||
{
|
||||
LibraryCommands.LibraryStats libraryStats = null;
|
||||
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw;
|
||||
private bool runBackupCountsAgain;
|
||||
|
||||
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
|
||||
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
|
||||
}
|
||||
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
|
||||
private void setBackupCounts(object _, object __)
|
||||
{
|
||||
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
|
||||
runBackupCountsAgain = true;
|
||||
|
||||
if (updateCountsBw is not null)
|
||||
return;
|
||||
|
||||
updateCountsBw = new System.ComponentModel.BackgroundWorker();
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
|
||||
updateCountsBw.RunWorkerAsync();
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
||||
{
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
|
||||
var libraryStats = LibraryCommands.GetCounts();
|
||||
e.Result = libraryStats;
|
||||
}
|
||||
updateCountsBw = null;
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
setBookBackupCounts(libraryStats);
|
||||
setPdfBackupCounts(libraryStats);
|
||||
}
|
||||
|
||||
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
||||
{
|
||||
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
|
||||
|
||||
// enable/disable export
|
||||
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
|
||||
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
|
||||
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
||||
|
||||
// update bottom numbers
|
||||
var pending = booksNoProgress + booksDownloadedOnly;
|
||||
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
|
||||
var statusStripText
|
||||
= !hasResults ? "No books. Begin by importing your library"
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
|
||||
: libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError)
|
||||
: pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp)
|
||||
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
@@ -200,31 +154,31 @@ namespace LibationWinForms
|
||||
: "All books have been liberated";
|
||||
|
||||
// update UI
|
||||
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
|
||||
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
||||
{
|
||||
var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
|
||||
|
||||
// update bottom numbers
|
||||
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
|
||||
var statusStripText
|
||||
= !hasResults ? ""
|
||||
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
|
||||
: $"| All {pdfsDownloaded} PDFs downloaded";
|
||||
: libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
|
||||
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
|
||||
|
||||
// update menu item
|
||||
var menuItemText
|
||||
= pdfsNotDownloaded > 0
|
||||
? $"{pdfsNotDownloaded} remaining"
|
||||
= libraryStats.pdfsNotDownloaded > 0
|
||||
? $"{libraryStats.pdfsNotDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
|
||||
// update UI
|
||||
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
|
||||
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
|
||||
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0);
|
||||
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -249,6 +203,7 @@ namespace LibationWinForms
|
||||
}
|
||||
private void filterBtn_Click(object sender, EventArgs e) => doFilter();
|
||||
|
||||
private bool isProcessingGridSelect = false;
|
||||
private string lastGoodFilter = "";
|
||||
private void doFilter(string filterString)
|
||||
{
|
||||
@@ -276,7 +231,7 @@ namespace LibationWinForms
|
||||
#endregion
|
||||
|
||||
#region Import menu
|
||||
public void RefreshImportMenu()
|
||||
public void RefreshImportMenu(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var count = persister.AccountsSettings.Accounts.Count;
|
||||
@@ -288,11 +243,6 @@ namespace LibationWinForms
|
||||
|
||||
removeLibraryBooksToolStripMenuItem.Visible = count != 0;
|
||||
|
||||
if (count == 1)
|
||||
{
|
||||
removeLibraryBooksToolStripMenuItem.Click += removeThisAccountToolStripMenuItem_Click;
|
||||
}
|
||||
|
||||
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
|
||||
removeAllAccountsToolStripMenuItem.Visible = count > 1;
|
||||
}
|
||||
@@ -330,13 +280,22 @@ namespace LibationWinForms
|
||||
scanLibraries(scanAccountsDialog.CheckedAccounts);
|
||||
}
|
||||
|
||||
private void removeThisAccountToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
// if 0 accounts, this will not be visible
|
||||
// if 1 account, run scanLibrariesRemovedBooks() on this account
|
||||
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||
var accounts = persister.AccountsSettings.GetAll();
|
||||
|
||||
if (accounts.Count != 1)
|
||||
return;
|
||||
|
||||
var firstAccount = accounts.Single();
|
||||
scanLibrariesRemovedBooks(firstAccount);
|
||||
}
|
||||
|
||||
// selectively remove books from all accounts
|
||||
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
@@ -344,6 +303,7 @@ namespace LibationWinForms
|
||||
scanLibrariesRemovedBooks(allAccounts.ToArray());
|
||||
}
|
||||
|
||||
// selectively remove books from some accounts
|
||||
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var scanAccountsDialog = new ScanAccountsDialog(this);
|
||||
@@ -361,9 +321,6 @@ namespace LibationWinForms
|
||||
{
|
||||
using var dialog = new RemoveBooksDialog(accounts);
|
||||
dialog.ShowDialog();
|
||||
|
||||
if (dialog.BooksRemoved)
|
||||
reloadGrid();
|
||||
}
|
||||
|
||||
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
|
||||
@@ -376,23 +333,30 @@ namespace LibationWinForms
|
||||
var newAdded = dialog.NewBooksAdded;
|
||||
|
||||
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
|
||||
|
||||
if (totalProcessed > 0)
|
||||
reloadGrid();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region liberate menu
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
|
||||
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
|
||||
|
||||
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
|
||||
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
|
||||
|
||||
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
|
||||
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
|
||||
+ "\r\n\r\nContinue?"
|
||||
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
|
||||
"Convert all M4b => Mp3?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning);
|
||||
if (result == DialogResult.Yes)
|
||||
await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
|
||||
}
|
||||
|
||||
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
|
||||
#endregion
|
||||
|
||||
#region Export menu
|
||||
@@ -486,6 +450,5 @@ namespace LibationWinForms
|
||||
|
||||
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
89
LibationWinForms/FormSaveExtension.cs
Normal file
89
LibationWinForms/FormSaveExtension.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using FileManager;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public static class FormSaveExtension
|
||||
{
|
||||
public static void RestoreSizeAndLocation(this Form form, Configuration config)
|
||||
{
|
||||
FormSizeAndPosition savedState = config.GetNonString<FormSizeAndPosition>(form.Name);
|
||||
|
||||
if (savedState is null)
|
||||
return;
|
||||
|
||||
// too small -- something must have gone wrong. use defaults
|
||||
if (savedState.Width < 25 || savedState.Height < 25)
|
||||
{
|
||||
savedState.Width = form.Width;
|
||||
savedState.Height = form.Height;
|
||||
}
|
||||
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (savedState.Width > Screen.PrimaryScreen.WorkingArea.Width)
|
||||
savedState.Width = Screen.PrimaryScreen.WorkingArea.Width;
|
||||
if (savedState.Height > Screen.PrimaryScreen.WorkingArea.Height)
|
||||
savedState.Height = Screen.PrimaryScreen.WorkingArea.Height;
|
||||
|
||||
var x = savedState.X;
|
||||
var y = savedState.Y;
|
||||
|
||||
var rect = new Rectangle(x, y, savedState.Width, savedState.Height);
|
||||
|
||||
// is proposed rect on a screen?
|
||||
if (Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect)))
|
||||
{
|
||||
form.StartPosition = FormStartPosition.Manual;
|
||||
form.DesktopBounds = rect;
|
||||
}
|
||||
else
|
||||
{
|
||||
form.StartPosition = FormStartPosition.WindowsDefaultLocation;
|
||||
form.Size = rect.Size;
|
||||
}
|
||||
|
||||
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
|
||||
form.WindowState = savedState.IsMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
|
||||
}
|
||||
|
||||
public static void SaveSizeAndLocation(this Form form, Configuration config)
|
||||
{
|
||||
Point location;
|
||||
Size size;
|
||||
var saveState = new FormSizeAndPosition();
|
||||
|
||||
// save location and size if the state is normal
|
||||
if (form.WindowState == FormWindowState.Normal)
|
||||
{
|
||||
location = form.Location;
|
||||
size = form.Size;
|
||||
}
|
||||
else
|
||||
{
|
||||
// save the RestoreBounds if the form is minimized or maximized
|
||||
location = form.RestoreBounds.Location;
|
||||
size = form.RestoreBounds.Size;
|
||||
}
|
||||
|
||||
saveState.X = location.X;
|
||||
saveState.Y = location.Y;
|
||||
|
||||
saveState.Width = size.Width;
|
||||
saveState.Height = size.Height;
|
||||
|
||||
saveState.IsMaximized = form.WindowState == FormWindowState.Maximized;
|
||||
|
||||
config.SetObject(form.Name, saveState);
|
||||
}
|
||||
}
|
||||
class FormSizeAndPosition
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
public int Height;
|
||||
public int Width;
|
||||
public bool IsMaximized;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,14 @@ using System.Linq;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Drawing;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
/// <summary>
|
||||
/// The View Model for a LibraryBook
|
||||
/// </summary>
|
||||
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||
{
|
||||
#region implementation properties
|
||||
@@ -25,12 +29,15 @@ namespace LibationWinForms
|
||||
|
||||
private Book Book => LibraryBook.Book;
|
||||
private Image _cover;
|
||||
private Action Refilter { get; }
|
||||
|
||||
public GridEntry(LibraryBook libraryBook)
|
||||
public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Refilter = refilterOnChanged;
|
||||
_memberValues = CreateMemberValueDictionary();
|
||||
|
||||
|
||||
//Get cover art. If it's default, subscribe to PictureCached
|
||||
{
|
||||
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
||||
@@ -47,9 +54,9 @@ namespace LibationWinForms
|
||||
Title = Book.Title;
|
||||
Series = Book.SeriesNames;
|
||||
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
||||
MyRating = ValueOrDefault(Book.UserDefinedItem.Rating?.ToStarString(), "");
|
||||
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
PurchaseDate = libraryBook.DateAdded.ToString("d");
|
||||
ProductRating = ValueOrDefault(Book.Rating?.ToStarString(), "");
|
||||
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
Authors = Book.AuthorNames;
|
||||
Narrators = Book.NarratorNames;
|
||||
Category = string.Join(" > ", Book.CategoriesNames);
|
||||
@@ -57,7 +64,7 @@ namespace LibationWinForms
|
||||
Description = GetDescriptionDisplay(Book);
|
||||
}
|
||||
|
||||
//DisplayTags and Liberate properties are live.
|
||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e)
|
||||
@@ -69,8 +76,78 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
#region Data Source properties
|
||||
#region detect changes to the model, update the view, and save to database.
|
||||
|
||||
/// <summary>
|
||||
/// This event handler receives notifications from the model that it has changed.
|
||||
/// Save to the database and notify the view that it's changed.
|
||||
/// </summary>
|
||||
private void UserDefinedItem_ItemChanged(object sender, string itemName)
|
||||
{
|
||||
var udi = sender as UserDefinedItem;
|
||||
|
||||
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
||||
return;
|
||||
|
||||
switch (itemName)
|
||||
{
|
||||
case nameof(udi.Tags):
|
||||
{
|
||||
Book.UserDefinedItem.Tags = udi.Tags;
|
||||
NotifyPropertyChanged(nameof(DisplayTags));
|
||||
}
|
||||
break;
|
||||
case nameof(udi.BookStatus):
|
||||
{
|
||||
Book.UserDefinedItem.BookStatus = udi.BookStatus;
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
}
|
||||
break;
|
||||
case nameof(udi.PdfStatus):
|
||||
{
|
||||
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!suspendCommit)
|
||||
Commit();
|
||||
}
|
||||
private bool suspendCommit = false;
|
||||
|
||||
/// <summary>
|
||||
/// Begin editing the model, suspending commits until <see cref="EndEdit"/> is called.
|
||||
/// </summary>
|
||||
public void BeginEdit() => suspendCommit = true;
|
||||
|
||||
/// <summary>
|
||||
/// Save all edits to the database.
|
||||
/// </summary>
|
||||
public void EndEdit()
|
||||
{
|
||||
Commit();
|
||||
suspendCommit = false;
|
||||
}
|
||||
|
||||
private void Commit()
|
||||
{
|
||||
// We don't want LiberatedStatus.PartialDownload to be a persistent status.
|
||||
// If display/icon status is PartialDownload then save NotLiberated to db then restore PartialDownload for display
|
||||
var displayStatus = Book.UserDefinedItem.BookStatus;
|
||||
var saveStatus = displayStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : displayStatus;
|
||||
Book.UserDefinedItem.BookStatus = saveStatus;
|
||||
|
||||
LibraryCommands.UpdateUserDefinedItem(Book);
|
||||
|
||||
Book.UserDefinedItem.BookStatus = displayStatus;
|
||||
|
||||
Refilter?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Model properties exposed to the view
|
||||
public Image Cover
|
||||
{
|
||||
get
|
||||
@@ -95,8 +172,23 @@ namespace LibationWinForms
|
||||
public string Category { get; }
|
||||
public string Misc { get; }
|
||||
public string Description { get; }
|
||||
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book));
|
||||
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
|
||||
@@ -120,7 +212,7 @@ namespace LibationWinForms
|
||||
{ nameof(Category), () => Category },
|
||||
{ nameof(Misc), () => Misc },
|
||||
{ nameof(DisplayTags), () => DisplayTags },
|
||||
{ nameof(Liberate), () => Liberate.Item1 }
|
||||
{ nameof(Liberate), () => Liberate.BookStatus }
|
||||
};
|
||||
|
||||
// Instantiate comparers for every exposed member object type.
|
||||
@@ -130,7 +222,7 @@ namespace LibationWinForms
|
||||
{ typeof(int), new ObjectComparer<int>() },
|
||||
{ typeof(float), new ObjectComparer<float>() },
|
||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||
{ typeof(LiberatedState), new ObjectComparer<LiberatedState>() },
|
||||
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
|
||||
};
|
||||
|
||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||
@@ -154,37 +246,7 @@ namespace LibationWinForms
|
||||
#endregion
|
||||
|
||||
#region Static library display functions
|
||||
|
||||
public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
|
||||
{
|
||||
(string libState, string image_lib) = liberatedStatus switch
|
||||
{
|
||||
LiberatedState.Liberated => ("Liberated", "green"),
|
||||
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
|
||||
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
|
||||
(string pdfState, string image_pdf) = pdfStatus switch
|
||||
{
|
||||
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
|
||||
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
|
||||
PdfState.NoPdf => ("", ""),
|
||||
_ => throw new Exception("Unexpected PDF state")
|
||||
};
|
||||
|
||||
var mouseoverText = libState + pdfState;
|
||||
|
||||
if (liberatedStatus == LiberatedState.NotDownloaded ||
|
||||
liberatedStatus == LiberatedState.PartialDownload ||
|
||||
pdfStatus == PdfState.NotDownloaded)
|
||||
mouseoverText += "\r\nClick to complete";
|
||||
|
||||
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
|
||||
|
||||
return (mouseoverText, buttonImage);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
|
||||
/// </summary>
|
||||
@@ -207,8 +269,8 @@ namespace LibationWinForms
|
||||
{
|
||||
var details = new List<string>();
|
||||
|
||||
var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]");
|
||||
var acct = ValueOrDefault(libraryBook.Account, "[unknown]");
|
||||
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
|
||||
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
|
||||
|
||||
details.Add($"Account: {locale} - {acct}");
|
||||
|
||||
@@ -228,10 +290,12 @@ namespace LibationWinForms
|
||||
return string.Join("\r\n", details);
|
||||
}
|
||||
|
||||
//Maybe add to Dinah StringExtensions?
|
||||
private static string ValueOrDefault(string value, string defaultValue)
|
||||
=> string.IsNullOrWhiteSpace(value) ? defaultValue : value;
|
||||
|
||||
#endregion
|
||||
|
||||
~GridEntry()
|
||||
{
|
||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
||||
FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.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>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<StartupObject />
|
||||
|
||||
<!-- Version is now in AppScaffolding.csproj -->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||
-->
|
||||
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@ using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class LiberateDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
|
||||
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
|
||||
{
|
||||
protected override DataGridViewImageButtonCell NewCell()
|
||||
=> new LiberateDataGridViewImageButtonCell();
|
||||
public LiberateDataGridViewImageButtonColumn()
|
||||
{
|
||||
CellTemplate = new LiberateDataGridViewImageButtonCell();
|
||||
}
|
||||
}
|
||||
|
||||
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
|
||||
@@ -18,14 +21,50 @@ namespace LibationWinForms
|
||||
{
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
if (value is (LiberatedState liberatedState, PdfState pdfState))
|
||||
if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
|
||||
{
|
||||
(string mouseoverText, Bitmap buttonImage) = GridEntry.GetLiberateDisplay(liberatedState, pdfState);
|
||||
var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
|
||||
|
||||
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
|
||||
|
||||
DrawButtonImage(graphics, buttonImage, cellBounds);
|
||||
|
||||
ToolTipText = mouseoverText;
|
||||
}
|
||||
}
|
||||
|
||||
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
|
||||
{
|
||||
if (liberatedStatus == LiberatedStatus.Error)
|
||||
return ("Book downloaded ERROR", SystemIcons.Error.ToBitmap());
|
||||
|
||||
(string libState, string image_lib) = liberatedStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => ("Liberated", "green"),
|
||||
LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
|
||||
LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"),
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
|
||||
(string pdfState, string image_pdf) = pdfStatus switch
|
||||
{
|
||||
LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"),
|
||||
LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"),
|
||||
LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"),
|
||||
null => ("", ""),
|
||||
_ => throw new Exception("Unexpected PDF state")
|
||||
};
|
||||
|
||||
var mouseoverText = libState + pdfState;
|
||||
|
||||
if (liberatedStatus == LiberatedStatus.NotLiberated ||
|
||||
liberatedStatus == LiberatedStatus.PartialDownload ||
|
||||
pdfStatus == LiberatedStatus.NotLiberated)
|
||||
mouseoverText += "\r\nClick to complete";
|
||||
|
||||
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
|
||||
|
||||
return (mouseoverText, buttonImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
LibationWinForms/ProductsGrid.Designer.cs
generated
8
LibationWinForms/ProductsGrid.Designer.cs
generated
@@ -92,7 +92,7 @@
|
||||
this.gridEntryDataGridView.ReadOnly = true;
|
||||
this.gridEntryDataGridView.RowHeadersVisible = false;
|
||||
this.gridEntryDataGridView.RowTemplate.Height = 82;
|
||||
this.gridEntryDataGridView.Size = new System.Drawing.Size(1505, 380);
|
||||
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380);
|
||||
this.gridEntryDataGridView.TabIndex = 0;
|
||||
//
|
||||
// dataGridViewImageButtonBoxColumn1
|
||||
@@ -103,7 +103,7 @@
|
||||
this.dataGridViewImageButtonBoxColumn1.ReadOnly = true;
|
||||
this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.dataGridViewImageButtonBoxColumn1.Width = 70;
|
||||
this.dataGridViewImageButtonBoxColumn1.Width = 75;
|
||||
//
|
||||
// dataGridViewImageColumn1
|
||||
//
|
||||
@@ -200,7 +200,7 @@
|
||||
// dataGridViewImageButtonBoxColumn2
|
||||
//
|
||||
this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags";
|
||||
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Edit Tags";
|
||||
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Tags and Details";
|
||||
this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2";
|
||||
this.dataGridViewImageButtonBoxColumn2.ReadOnly = true;
|
||||
this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
@@ -213,7 +213,7 @@
|
||||
this.Controls.Add(this.gridEntryDataGridView);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "ProductsGrid";
|
||||
this.Size = new System.Drawing.Size(1505, 380);
|
||||
this.Size = new System.Drawing.Size(1510, 380);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
@@ -29,7 +29,6 @@ namespace LibationWinForms
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler BackupCountsChanged;
|
||||
|
||||
// alias
|
||||
private DataGridView _dataGridView => gridEntryDataGridView;
|
||||
@@ -67,7 +66,7 @@ namespace LibationWinForms
|
||||
await Liberate_Click(liveGridEntry);
|
||||
break;
|
||||
case nameof(liveGridEntry.DisplayTags):
|
||||
EditTags_Click(liveGridEntry);
|
||||
Details_Click(liveGridEntry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -77,30 +76,33 @@ namespace LibationWinForms
|
||||
var libraryBook = liveGridEntry.LibraryBook;
|
||||
|
||||
// liberated: open explorer to file
|
||||
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
|
||||
if (libraryBook.Book.Audio_Exists)
|
||||
{
|
||||
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
|
||||
var filePath = FileManager.AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||
if (!Go.To.File(filePath))
|
||||
MessageBox.Show($"File not found:\r\n{filePath}");
|
||||
{
|
||||
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
||||
MessageBox.Show($"File not found" + suffix);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// else: liberate
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
|
||||
}
|
||||
|
||||
private void EditTags_Click(GridEntry liveGridEntry)
|
||||
private void Details_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags);
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
return;
|
||||
liveGridEntry.BeginEdit();
|
||||
|
||||
//Re-apply filters
|
||||
Filter();
|
||||
liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
|
||||
liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
|
||||
|
||||
liveGridEntry.EndEdit();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -117,8 +119,7 @@ namespace LibationWinForms
|
||||
//
|
||||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
using var context = DbContexts.GetContext();
|
||||
var lib = context.GetLibrary_Flat_NoTracking();
|
||||
var lib = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
// if no data. hide all columns. return
|
||||
if (!lib.Any())
|
||||
@@ -129,7 +130,7 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
var orderedGridEntries = lib
|
||||
.Select(lb => new GridEntry(lb)).ToList()
|
||||
.Select(lb => new GridEntry(lb, Filter)).ToList()
|
||||
// default load order
|
||||
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
|
||||
//// more advanced example: sort by author, then series, then title
|
||||
@@ -143,21 +144,6 @@ namespace LibationWinForms
|
||||
|
||||
// FILTER
|
||||
Filter();
|
||||
|
||||
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void RefreshRow(string productId)
|
||||
{
|
||||
var rowIndex = getRowIndex((ge) => ge.AudibleProductId == productId);
|
||||
|
||||
// update cells incl Liberate button text
|
||||
_dataGridView.InvalidateRow(rowIndex);
|
||||
|
||||
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
|
||||
Filter();
|
||||
|
||||
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -192,10 +178,7 @@ namespace LibationWinForms
|
||||
#endregion
|
||||
|
||||
#region DataGridView Macro
|
||||
|
||||
private int getRowIndex(Func<GridEntry, bool> func) => _dataGridView.GetRowIdOfBoundItem(func);
|
||||
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
317
LibationWinForms/Program.cs
Normal file
317
LibationWinForms/Program.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi.Authorization;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
static extern bool AllocConsole();
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
//// Uncomment to see Console. Must be called before anything writes to Console.
|
||||
//// Only use while debugging. Acts erratically in the wild
|
||||
//AllocConsole();
|
||||
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
RunInstaller(config);
|
||||
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations();
|
||||
|
||||
// migrations which require Forms or are long-running
|
||||
RunWindowsOnlyMigrations(config);
|
||||
|
||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate();
|
||||
#endif
|
||||
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding();
|
||||
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
|
||||
private static void RunInstaller(Configuration config)
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settigns in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
static void CancelInstallation()
|
||||
{
|
||||
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Application.Exit();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
var setupDialog = new SetupDialog();
|
||||
if (setupDialog.ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
|
||||
if (libationFilesDialog.ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
// path did not result in valid settings
|
||||
var continueResult = MessageBox.Show(
|
||||
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult != DialogResult.Yes)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
config.AllowLibationFixup = true;
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
private static void RunWindowsOnlyMigrations(Configuration config)
|
||||
{
|
||||
// only supported in winforms. don't move to app scaffolding
|
||||
migrate_to_v5_0_0(config);
|
||||
|
||||
// long running. won't get a chance to finish in cli. don't move to app scaffolding
|
||||
migrate_to_v5_5_0(config);
|
||||
}
|
||||
|
||||
#region migrate to v5.0.0 re-register device if device info not in settings
|
||||
private static void migrate_to_v5_0_0(Configuration config)
|
||||
{
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||
return;
|
||||
|
||||
var accountsPersister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
var accounts = accountsPersister?.AccountsSettings?.Accounts;
|
||||
if (accounts is null)
|
||||
return;
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var identity = account?.IdentityTokens;
|
||||
|
||||
if (identity is null)
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identity.DeviceType) &&
|
||||
!string.IsNullOrWhiteSpace(identity.DeviceSerialNumber) &&
|
||||
!string.IsNullOrWhiteSpace(identity.AmazonAccountId))
|
||||
continue;
|
||||
|
||||
var authorize = new Authorize(identity.Locale);
|
||||
|
||||
try
|
||||
{
|
||||
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
|
||||
identity.Invalidate();
|
||||
|
||||
var api = AudibleApiActions.GetApiAsync(new LibationWinForms.Login.WinformResponder(account), account).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Don't care if it fails
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
|
||||
private static void migrate_to_v5_5_0(Configuration config)
|
||||
=> new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start();
|
||||
private static void migrate_to_v5_5_0_thread(Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
|
||||
if (!File.Exists(filePaths))
|
||||
return;
|
||||
|
||||
var fileLocations = Path.Combine(config.LibationFiles, "FileLocations.json");
|
||||
if (!File.Exists(fileLocations))
|
||||
File.Copy(filePaths, fileLocations);
|
||||
|
||||
// files to be deleted at the end
|
||||
var libhackFilesToDelete = new List<string>();
|
||||
// .libhack files => errors
|
||||
var libhackFiles = Directory.EnumerateDirectories(config.Books, "*.libhack", SearchOption.AllDirectories);
|
||||
|
||||
using var context = ApplicationServices.DbContexts.GetContext();
|
||||
context.Books.Load();
|
||||
|
||||
var jArr = JArray.Parse(File.ReadAllText(filePaths));
|
||||
|
||||
foreach (var jToken in jArr)
|
||||
{
|
||||
var asinToken = jToken["Id"];
|
||||
var fileTypeToken = jToken["FileType"];
|
||||
var pathToken = jToken["Path"];
|
||||
if (asinToken is null || fileTypeToken is null || pathToken is null ||
|
||||
asinToken.Type != JTokenType.String || fileTypeToken.Type != JTokenType.Integer || pathToken.Type != JTokenType.String)
|
||||
continue;
|
||||
|
||||
var asin = asinToken.Value<string>();
|
||||
var fileType = (FileType)fileTypeToken.Value<int>();
|
||||
var path = pathToken.Value<string>();
|
||||
|
||||
if (fileType == FileType.Unknown || fileType == FileType.AAXC)
|
||||
continue;
|
||||
|
||||
var book = context.Books.Local.FirstOrDefault(b => b.AudibleProductId == asin);
|
||||
if (book is null)
|
||||
continue;
|
||||
|
||||
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
||||
if (fileType == FileType.PDF)
|
||||
book.UserDefinedItem.PdfStatus = LiberatedStatus.Liberated;
|
||||
|
||||
if (fileType == FileType.Audio)
|
||||
{
|
||||
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
|
||||
if (lhack is null)
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
else
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
||||
libhackFilesToDelete.Add(lhack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
// only do this after save changes
|
||||
foreach (var libhackFile in libhackFilesToDelete)
|
||||
File.Delete(libhackFile);
|
||||
|
||||
File.Delete(filePaths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static void checkForUpdate()
|
||||
{
|
||||
string zipUrl;
|
||||
string htmlUrl;
|
||||
string zipName;
|
||||
try
|
||||
{
|
||||
bool hasUpgrade;
|
||||
(hasUpgrade, zipUrl, htmlUrl, zipName) = AppScaffolding.LibationScaffolding.GetLatestRelease();
|
||||
|
||||
if (!hasUpgrade)
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error checking for update", "Error checking for update", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (zipUrl is null)
|
||||
{
|
||||
MessageBox.Show(htmlUrl, "New version available");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"New version available @ {htmlUrl}\r\nDownload the zip file?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
using var fileSelector = new SaveFileDialog { FileName = zipName, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
var selectedPath = fileSelector.FileName;
|
||||
|
||||
try
|
||||
{
|
||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBoxAlertAdmin.Show("Error downloading update", "Error downloading update", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
LibationWinForms/Properties/Resources.Designer.cs
generated
20
LibationWinForms/Properties/Resources.Designer.cs
generated
@@ -90,6 +90,26 @@ namespace LibationWinForms.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap edit_25x25 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("edit_25x25", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap edit_64x64 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("edit_64x64", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<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">
|
||||
@@ -53,12 +112,12 @@
|
||||
<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>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.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>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
@@ -68,6 +127,12 @@
|
||||
<data name="default_cover_80x80" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\img-coverart-prod-unavailable_80x80.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="edit_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\edit_25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="edit_64x64" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\edit_64x64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="edit_tags_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\edit-tags-25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user