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
23 changed files with 1161 additions and 572 deletions
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue