LibationCli and structural changes to support it incl: no LibationLauncher, just LibationWinForms. LibationWinForms builds to a different output dir so cli can be deployed easily. Versioning number is moved to scaffolding library shared by both apps
This commit is contained in:
parent
995637e843
commit
0f130c70f5
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.6.8.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>
|
||||||
325
AppScaffolding/LibationScaffolding.cs
Normal file
325
AppScaffolding/LibationScaffolding.cs
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ using Dinah.Core;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace LibationLauncher
|
namespace AppScaffolding
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@ -20,7 +20,7 @@ namespace LibationLauncher
|
|||||||
internal static class UNSAFE_MigrationHelper
|
internal static class UNSAFE_MigrationHelper
|
||||||
{
|
{
|
||||||
#region appsettings.json
|
#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);
|
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
|
||||||
|
|
||||||
@ -51,9 +51,12 @@ namespace FileManager
|
|||||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||||
|
|
||||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||||
/// <returns>Value was changed</returns>
|
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
{
|
||||||
=> persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||||
|
if (settingWasChanged)
|
||||||
|
configuration?.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||||
|
|
||||||
@ -159,13 +162,9 @@ namespace FileManager
|
|||||||
|
|
||||||
#region logging
|
#region logging
|
||||||
private IConfigurationRoot configuration;
|
private IConfigurationRoot configuration;
|
||||||
|
|
||||||
public void ConfigureLogging()
|
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()
|
configuration = new ConfigurationBuilder()
|
||||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||||
.Build();
|
.Build();
|
||||||
@ -221,7 +220,7 @@ namespace FileManager
|
|||||||
|
|
||||||
#region LibationFiles
|
#region LibationFiles
|
||||||
|
|
||||||
private const string APPSETTINGS_JSON = "appsettings.json";
|
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
|
||||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||||
|
|
||||||
[Description("Location for storage of program-created files")]
|
[Description("Location for storage of program-created files")]
|
||||||
@ -238,12 +237,16 @@ namespace FileManager
|
|||||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
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
|
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||||
bool settingWasChanged = SetWithJsonPath("Serilog.WriteTo[1].Args", "path", logPath, true);
|
|
||||||
if (settingWasChanged)
|
// BAD: Serilog.WriteTo[1].Args
|
||||||
configuration?.Reload();
|
// "[1]" assumes ordinal position
|
||||||
|
// GOOD: Serilog.WriteTo[?(@.Name=='File')].Args
|
||||||
|
var jsonpath = "Serilog.WriteTo[?(@.Name=='File')].Args";
|
||||||
|
|
||||||
|
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||||
|
|
||||||
return libationFilesPathCache;
|
return libationFilesPathCache;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||||
<PackageReference Include="Octokit" Version="0.50.0" />
|
|
||||||
<PackageReference Include="Polly" Version="7.2.2" />
|
<PackageReference Include="Polly" Version="7.2.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
21
Libation.sln
21
Libation.sln
@ -41,8 +41,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoIm
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
||||||
@ -51,6 +49,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests"
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\LibationCli.csproj", "{428163C3-D558-4914-B570-A92069521877}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -93,10 +95,6 @@ Global
|
|||||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B95650EA-25F0-449E-BA5D-99126BC5D730}.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.ActiveCfg = Debug|Any CPU
|
||||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@ -109,6 +107,14 @@ Global
|
|||||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -123,10 +129,11 @@ Global
|
|||||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||||
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
|
||||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||||
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||||
|
|||||||
33
LibationCli/LibationCli.csproj
Normal file
33
LibationCli/LibationCli.csproj
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<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,36 +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>
|
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
|
||||||
|
|
||||||
<Version>5.6.7.4</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,508 +0,0 @@
|
|||||||
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 Dinah.Core.IO;
|
|
||||||
using Dinah.Core.Logging;
|
|
||||||
using FileManager;
|
|
||||||
using InternalUtilities;
|
|
||||||
using LibationWinForms;
|
|
||||||
using LibationWinForms.Dialogs;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
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_5_0(config);
|
|
||||||
|
|
||||||
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
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
#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.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 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 |
@ -2,14 +2,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net5.0-windows</TargetFramework>
|
<TargetFramework>net5.0-windows</TargetFramework>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||||
|
<AssemblyName>Libation</AssemblyName>
|
||||||
|
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
<StartupObject />
|
||||||
|
|
||||||
|
<!-- Version is now in AppScaffolding.csproj -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -18,6 +24,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||||
|
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ Startup project: DataLayer
|
|||||||
since we have mult contexts, must use -context:
|
since we have mult contexts, must use -context:
|
||||||
Add-Migration MyComment -context LibationContext
|
Add-Migration MyComment -context LibationContext
|
||||||
Update-Database -context LibationContext
|
Update-Database -context LibationContext
|
||||||
Startup project: reset to prev. eg: LibationLauncher
|
Startup project: reset to prev. eg: LibationWinForms
|
||||||
|
|
||||||
|
|
||||||
Migrations, detailed
|
Migrations, detailed
|
||||||
|
|||||||
@ -30,7 +30,7 @@ STRUCTURE
|
|||||||
* 5 Domain Utilities (db aware)
|
* 5 Domain Utilities (db aware)
|
||||||
This is often where database, domain-ignorant util.s, and non-database capabilities come together to provide the domain-conscious access points
|
This is often where database, domain-ignorant util.s, and non-database capabilities come together to provide the domain-conscious access points
|
||||||
* 6 Application
|
* 6 Application
|
||||||
UI and application launcher
|
GUI, CLI, and shared scaffolding
|
||||||
|
|
||||||
|
|
||||||
CODING GUIDELINES
|
CODING GUIDELINES
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user