Compare commits

...

14 Commits

Author SHA1 Message Date
Robert McRackan
8fe3896d76 build error bug fix 2021-09-13 10:35:41 -04:00
Robert McRackan
adcba34560 'bad book' setting labels 2021-09-11 08:01:58 -04:00
Robert McRackan
8e09d7e617 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-10 22:59:02 -04:00
Robert McRackan
197b50e3ac issue #111 -- setting for bad book abort/retry/ignore. default: ask 2021-09-10 22:58:34 -04:00
rmcrackan
ac2114e270 Add CLI documentation 2021-09-10 17:20:13 -04:00
Robert McRackan
29461701cd I meant to increm the minor version for cli 2021-09-10 17:04:47 -04:00
Robert McRackan
0f130c70f5 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 2021-09-10 16:54:32 -04:00
Robert McRackan
995637e843 add logging. helpful when viewing in console 2021-09-10 12:51:07 -04:00
Robert McRackan
9501687f86 Change build path. Necessary for possibly building a non-winforms exe to the same output dir 2021-09-09 16:04:17 -04:00
Robert McRackan
248dea3402 write to log, not console 2021-09-09 13:26:00 -04:00
Robert McRackan
1d420f5430 File Liberators should log their own progresss and not depend on controller or forms to do so. This means that LogMe will create duplicate log entries for non-form and non-controller calls but that's a refactor for a rainy day. 2021-09-09 11:27:03 -04:00
Robert McRackan
5f0a6b8526 bug fix: installer bug for users who say 'return user' but don't have valid files 2021-09-07 16:58:04 -04:00
Robert McRackan
c337c0b44e library book composite key comments 2021-09-07 13:35:59 -04:00
Robert McRackan
89207866f3 make sure that __log is never empty so that .Last() can't throw 2021-09-04 18:17:29 -04:00
39 changed files with 1507 additions and 642 deletions

View File

@@ -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;
}

View 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.2.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>

View 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;
}
}
}

View File

@@ -6,7 +6,7 @@ using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace LibationLauncher
namespace AppScaffolding
{
/// <summary>
///
@@ -20,7 +20,7 @@ namespace LibationLauncher
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);

View File

@@ -91,10 +91,11 @@ namespace ApplicationServices
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);
logTime($"post {nameof(importIntoDbAsync)}");
Log.Logger.Information($"Import: New count {newCount}");
Log.Logger.Information($"Import complete. New count {newCount}");
return (totalCount, newCount);
}

View File

@@ -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

View File

@@ -9,7 +9,7 @@ namespace DtoImporterService
public static class PerfLogger
{
private static Stopwatch sw = new Stopwatch();
private static List<timeLogEntry> __log { get; } = new List<timeLogEntry>();
private static List<timeLogEntry> __log { get; } = new List<timeLogEntry> { new("begin", 0, 0) };
public static void logTime(string s)
{

View File

@@ -14,7 +14,6 @@ namespace FileLiberator
{
public class ConvertToMp3 : IAudioDecodable
{
private Mp4File m4bBook;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
@@ -30,7 +29,22 @@ 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();

View File

@@ -15,7 +15,6 @@ namespace FileLiberator
{
public class DownloadDecryptBook : IAudioDecodable
{
private AaxcDownloadConverter aaxcDownloader;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
@@ -31,6 +30,21 @@ namespace FileLiberator
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 });
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);

View File

@@ -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();

View File

@@ -17,6 +17,15 @@ namespace FileLiberator
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !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);

View 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()
);
}
}

View File

@@ -51,9 +51,12 @@ namespace FileManager
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");
@@ -99,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
@@ -159,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();
@@ -179,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
{
@@ -217,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")]
@@ -238,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;
}

View File

@@ -7,7 +7,6 @@
<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>

View File

@@ -58,10 +58,19 @@ 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
//var itemsDebug = items.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("library.json", itemsDebug);
//// 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
await manageEpisodesAsync(api, items);

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="1.2.0.1" />
<PackageReference Include="AudibleApi" Version="1.2.1.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -41,8 +41,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoIm
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
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}"
EndProject
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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.Build.0 = Debug|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}.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
@@ -123,10 +129,11 @@ Global
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{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}
{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}

View 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>

View 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>());
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}

View 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
View 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
View 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();
}
}

View File

@@ -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.6.6.1</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>

View File

@@ -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
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.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;
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -196,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);
@@ -207,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
{
@@ -238,7 +246,8 @@ $@" 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;
@@ -288,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.)

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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">

View File

@@ -2,14 +2,20 @@
<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>
<ItemGroup>
@@ -18,6 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>

317
LibationWinForms/Program.cs Normal file
View 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);
}
}
}
}

View File

@@ -27,6 +27,7 @@
- [Files and folders](#files-and-folders)
- [Linux and Mac (unofficial)](#linux-and-mac)
- [Settings](#settings)
- [Command Line Interface](#command-line-interface)
## Audible audiobook manager
@@ -241,3 +242,49 @@ Although Libation only currently officially supports Windows, [some users](https
### Settings
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
Warnings about relying solely on on the CLI:
* CLI will not perform any upgrades.
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
```
help
libationcli --help
verb-specific help
libationcli scan --help
scan all libraries
libationcli scan
scan only libraries for specific accounts
libationcli scan nickname1 nickname2
convert all m4b files to mp3
libationcli convert
liberate all books and pdfs
libationcli liberate
liberate pdfs only
libationcli liberate --pdf
libationcli liberate -p
export library to file
libationcli export --path "C:\foo\bar\my.json" --json
libationcli export -p "C:\foo\bar\my.json" -j
```
Currently logs are written to Console and to file. This means they'll be printed in the CLI. To disable, find this in Settings.json and delete the 3 lines after `"WriteTo": [`
```
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console"
},
```

View File

@@ -6,7 +6,7 @@ Startup project: DataLayer
since we have mult contexts, must use -context:
Add-Migration MyComment -context LibationContext
Update-Database -context LibationContext
Startup project: reset to prev. eg: LibationLauncher
Startup project: reset to prev. eg: LibationWinForms
Migrations, detailed

View File

@@ -30,7 +30,7 @@ STRUCTURE
* 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
* 6 Application
UI and application launcher
GUI, CLI, and shared scaffolding
CODING GUIDELINES