From bb05847b2509a992cc969b978d16b3ec19cb9f82 Mon Sep 17 00:00:00 2001
From: Mbucari <37587114+Mbucari@users.noreply.github.com>
Date: Sun, 2 Jul 2023 14:08:27 -0600
Subject: [PATCH 1/8] Improve finding audio file by ID
---
Source/FileManager/LongPath.cs | 5 +++++
Source/LibationFileManager/AudibleFileStorage.cs | 11 ++++++++++-
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/Source/FileManager/LongPath.cs b/Source/FileManager/LongPath.cs
index 2175681a..956f46a4 100644
--- a/Source/FileManager/LongPath.cs
+++ b/Source/FileManager/LongPath.cs
@@ -163,6 +163,11 @@ namespace FileManager
public override string ToString() => Path;
+ public override int GetHashCode() => Path.GetHashCode();
+ public override bool Equals(object obj) => obj is LongPath other && Path == other.Path;
+ public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2);
+ public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2);
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs
index ef07230d..34dc5b64 100644
--- a/Source/LibationFileManager/AudibleFileStorage.cs
+++ b/Source/LibationFileManager/AudibleFileStorage.cs
@@ -126,7 +126,16 @@ namespace LibationFileManager
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
var regex = GetBookSearchRegex(productId);
- return BookDirectoryFiles.FindFiles(regex);
+
+ //Find all extant files matching the priductID
+ //using both the file system and the file path cache
+ return
+ FilePathCache
+ .GetFiles(productId)
+ .Where(c => c.fileType == FileType.Audio && File.Exists(c.path))
+ .Select(c => c.path)
+ .Union(BookDirectoryFiles.FindFiles(regex))
+ .ToList();
}
public void Refresh() => BookDirectoryFiles.RefreshFiles();
From 29803c6ba0e659a0016de64654c1a68b9cfdc90a Mon Sep 17 00:00:00 2001
From: Mbucari <37587114+Mbucari@users.noreply.github.com>
Date: Sun, 2 Jul 2023 14:32:54 -0600
Subject: [PATCH 2/8] Overhaul LibationCli
Add version verb with option to check for upgrade
Add Search verb to search the library
Add export file type inference
Add more set-status options
Add console progress bar and ETA
Add processable option to liberate specific book IDs
Scan accounts by nickname or account ID
Improve startup performance for halp and on parsing error
More useful error messages
---
Source/LibationCli/ConsoleProgressBar.cs | 77 ++++++++++++++
Source/LibationCli/HelpVerb.cs | 52 +++++++++
Source/LibationCli/LibationCli.csproj | 3 +-
Source/LibationCli/Options/ConvertOptions.cs | 5 +-
Source/LibationCli/Options/ExportOptions.cs | 44 +++++---
Source/LibationCli/Options/LiberateOptions.cs | 7 +-
Source/LibationCli/Options/ScanOptions.cs | 28 +++--
Source/LibationCli/Options/SearchOptions.cs | 50 +++++++++
.../Options/SetDownloadStatusOptions.cs | 82 +++++++++-----
Source/LibationCli/Options/VersionOptions.cs | 59 +++++++++++
Source/LibationCli/Options/_OptionsBase.cs | 39 +++++--
.../Options/_ProcessableOptionsBase.cs | 38 +++++--
Source/LibationCli/Program.cs | 100 ++++++++++++------
Source/LibationCli/Setup.cs | 39 ++-----
14 files changed, 477 insertions(+), 146 deletions(-)
create mode 100644 Source/LibationCli/ConsoleProgressBar.cs
create mode 100644 Source/LibationCli/HelpVerb.cs
create mode 100644 Source/LibationCli/Options/SearchOptions.cs
create mode 100644 Source/LibationCli/Options/VersionOptions.cs
diff --git a/Source/LibationCli/ConsoleProgressBar.cs b/Source/LibationCli/ConsoleProgressBar.cs
new file mode 100644
index 00000000..7369f427
--- /dev/null
+++ b/Source/LibationCli/ConsoleProgressBar.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+
+namespace LibationCli;
+
+internal class ConsoleProgressBar
+{
+ public TextWriter Output { get; }
+ public int MaxWidth { get; }
+ public char ProgressChar { get; }
+ public char NoProgressChar { get; }
+
+ public double? Progress
+ {
+ get => m_Progress;
+ set
+ {
+ m_Progress = value ?? 0;
+ WriteProgress();
+ }
+ }
+
+ public TimeSpan RemainingTime
+ {
+ get => m_RemainingTime;
+ set
+ {
+ m_RemainingTime = value;
+ WriteProgress();
+ }
+ }
+
+
+ private double m_Progress;
+ private TimeSpan m_RemainingTime;
+ private int m_LastWriteLength = 0;
+ private const int MAX_ETA_LEN = 10;
+ private readonly int m_NumProgressPieces;
+
+ public ConsoleProgressBar(
+ TextWriter output,
+ int maxWidth = 80,
+ char progressCharr = '#',
+ char noProgressChar = '.')
+ {
+ Output = output;
+ MaxWidth = maxWidth;
+ ProgressChar = progressCharr;
+ NoProgressChar = noProgressChar;
+ m_NumProgressPieces = MaxWidth - MAX_ETA_LEN - 4;
+ }
+
+ private void WriteProgress()
+ {
+ var numCompleted = (int)Math.Round(double.Min(100, m_Progress) * m_NumProgressPieces / 100);
+ var numRemaining = m_NumProgressPieces - numCompleted;
+ var progressBar = $"[{new string(ProgressChar, numCompleted)}{new string(NoProgressChar, numRemaining)}] ";
+
+ progressBar += RemainingTime.TotalMinutes > 1000
+ ? "ETA ∞"
+ : $"ETA {(int)RemainingTime.TotalMinutes}:{RemainingTime.Seconds:D2}";
+
+ Output.Write(new string('\b', m_LastWriteLength) + progressBar);
+ if (progressBar.Length < m_LastWriteLength)
+ {
+ var extra = m_LastWriteLength - progressBar.Length;
+ Output.Write(new string(' ', extra) + new string('\b', extra));
+ }
+ m_LastWriteLength = progressBar.Length;
+ }
+
+ public void Clear()
+ => Output.Write(
+ new string('\b', m_LastWriteLength)
+ + new string(' ', m_LastWriteLength)
+ + new string('\b', m_LastWriteLength));
+}
diff --git a/Source/LibationCli/HelpVerb.cs b/Source/LibationCli/HelpVerb.cs
new file mode 100644
index 00000000..b9e329cf
--- /dev/null
+++ b/Source/LibationCli/HelpVerb.cs
@@ -0,0 +1,52 @@
+using AppScaffolding;
+using CommandLine;
+using CommandLine.Text;
+
+namespace LibationCli;
+
+[Verb("help", HelpText = "Display more information on a specific command.")]
+internal class HelpVerb
+{
+ ///
+ /// Name of the verb to get help about
+ ///
+ [Value(0, Default = "")]
+ public string HelpType { get; set; }
+
+ ///
+ /// Create a base for
+ ///
+ public static HelpText CreateHelpText()
+ {
+ var auto = new HelpText
+ {
+ AutoVersion = false,
+ AutoHelp = false,
+ Heading = $"LibationCli v{LibationScaffolding.BuildVersion.ToString(3)}",
+ AdditionalNewLineAfterOption = true,
+ MaximumDisplayWidth = 80
+ };
+ return auto;
+ }
+
+ ///
+ /// Get the 's
+ ///
+ public HelpText GetHelpText()
+ {
+ var helpText = CreateHelpText();
+ var result = new Parser().ParseArguments(new string[] { HelpType }, Program.VerbTypes);
+ if (result.TypeInfo.Current == typeof(NullInstance))
+ {
+ //HelpType is not a defined verb so get LibationCli usage
+ helpText.AddVerbs(Program.VerbTypes);
+ }
+ else
+ {
+ helpText.AddDashesToOption = true;
+ helpText.AutoHelp = true;
+ helpText.AddOptions(result);
+ }
+ return helpText;
+ }
+}
diff --git a/Source/LibationCli/LibationCli.csproj b/Source/LibationCli/LibationCli.csproj
index 04926587..864e4ec6 100644
--- a/Source/LibationCli/LibationCli.csproj
+++ b/Source/LibationCli/LibationCli.csproj
@@ -3,7 +3,8 @@
Exe
- net7.0
+ net7.0-windows
+ win-x64
true
false
false
diff --git a/Source/LibationCli/Options/ConvertOptions.cs b/Source/LibationCli/Options/ConvertOptions.cs
index fa3b744b..b20f0152 100644
--- a/Source/LibationCli/Options/ConvertOptions.cs
+++ b/Source/LibationCli/Options/ConvertOptions.cs
@@ -1,8 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using CommandLine;
using System.Threading.Tasks;
-using CommandLine;
namespace LibationCli
{
diff --git a/Source/LibationCli/Options/ExportOptions.cs b/Source/LibationCli/Options/ExportOptions.cs
index 6d43a029..6e994718 100644
--- a/Source/LibationCli/Options/ExportOptions.cs
+++ b/Source/LibationCli/Options/ExportOptions.cs
@@ -1,10 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using ApplicationServices;
-using AudibleUtilities;
+using ApplicationServices;
using CommandLine;
+using System;
+using System.IO;
+using System.Threading.Tasks;
namespace LibationCli
{
@@ -29,26 +27,38 @@ namespace LibationCli
}
*/
#endregion
- [Option(shortName: 'x', longName: "xlsx", SetName = "xlsx", Required = true)]
+ [Option(shortName: 'x', longName: "xlsx", HelpText = "Microsoft Excel Spreadsheet", SetName = "Export Format")]
public bool xlsx { get; set; }
- [Option(shortName: 'c', longName: "csv", SetName = "csv", Required = true)]
+ [Option(shortName: 'c', longName: "csv", HelpText = "Comma-separated values", SetName = "Export Format")]
public bool csv { get; set; }
- [Option(shortName: 'j', longName: "json", SetName = "json", Required = true)]
+ [Option(shortName: 'j', longName: "json", HelpText = "JavaScript Object Notation", SetName = "Export Format")]
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}");
+ Action exporter
+ = csv ? LibraryExporter.ToCsv
+ : json ? LibraryExporter.ToJson
+ : xlsx ? LibraryExporter.ToXlsx
+ : Path.GetExtension(FilePath)?.ToLower() switch
+ {
+ ".xlsx" => LibraryExporter.ToXlsx,
+ ".csv" => LibraryExporter.ToCsv,
+ ".json" => LibraryExporter.ToJson,
+ _ => null
+ };
+ if (exporter is null)
+ {
+ PrintVerbUsage($"Undefined export format for file type \"{Path.GetExtension(FilePath)}\"");
+ }
+ else
+ {
+ exporter(FilePath);
+ Console.WriteLine($"Library exported to: {FilePath}");
+ }
return Task.CompletedTask;
}
}
diff --git a/Source/LibationCli/Options/LiberateOptions.cs b/Source/LibationCli/Options/LiberateOptions.cs
index 5c904771..ab1a8e78 100644
--- a/Source/LibationCli/Options/LiberateOptions.cs
+++ b/Source/LibationCli/Options/LiberateOptions.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using CommandLine;
+using CommandLine;
using DataLayer;
using FileLiberator;
+using System.Threading.Tasks;
namespace LibationCli
{
diff --git a/Source/LibationCli/Options/ScanOptions.cs b/Source/LibationCli/Options/ScanOptions.cs
index 5f1be8e3..43c888dd 100644
--- a/Source/LibationCli/Options/ScanOptions.cs
+++ b/Source/LibationCli/Options/ScanOptions.cs
@@ -1,18 +1,18 @@
-using System;
+using ApplicationServices;
+using AudibleUtilities;
+using CommandLine;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using ApplicationServices;
-using AudibleUtilities;
-using CommandLine;
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 AccountNicknames { get; set; }
+ [Value(0, MetaName = "Accounts", HelpText = "Optional: user ID or nicknames of accounts to scan.", Required = false)]
+ public IEnumerable AccountNames { get; set; }
protected override async Task ProcessAsync()
{
@@ -42,13 +42,19 @@ namespace LibationCli
private Account[] getAccounts()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
- var accounts = persister.AccountsSettings.GetAll().ToArray();
+ var allAccounts = persister.AccountsSettings.GetAll().ToArray();
- if (!AccountNicknames.Any())
- return accounts;
+ if (!AccountNames.Any())
+ return allAccounts;
- var found = accounts.Where(acct => AccountNicknames.Contains(acct.AccountName)).ToArray();
- var notFound = AccountNicknames.Except(found.Select(f => f.AccountName)).ToArray();
+ var accountNames = AccountNames.Select(n => n.ToLower()).ToArray();
+
+ var found
+ = allAccounts
+ .Where(acct => accountNames.Contains(acct.AccountName.ToLower()) || accountNames.Contains(acct.AccountId.ToLower()))
+ .ToArray();
+
+ var notFound = allAccounts.Except(found).ToArray();
// no accounts found. do not continue
if (!found.Any())
diff --git a/Source/LibationCli/Options/SearchOptions.cs b/Source/LibationCli/Options/SearchOptions.cs
new file mode 100644
index 00000000..5ed00d6a
--- /dev/null
+++ b/Source/LibationCli/Options/SearchOptions.cs
@@ -0,0 +1,50 @@
+using ApplicationServices;
+using CommandLine;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace LibationCli.Options
+{
+ [Verb("search", HelpText = "Search for books in your library")]
+ internal class SearchOptions : OptionsBase
+ {
+ [Value(0, MetaName = "query", Required = true, HelpText = "Lucene query test to search")]
+ public IEnumerable Query { get; set; }
+
+ protected override Task ProcessAsync()
+ {
+ var query = string.Join(" ", Query).Trim('\"');
+ var results = SearchEngineCommands.Search(query).Docs.ToList();
+
+ Console.WriteLine($"Found {results.Count} matching results.");
+
+ const string nextPrompt = "Press any key for the next 10 results or Esc for all results";
+ bool waitForNextBatch = true;
+
+ for (int i = 0; i < results.Count; i += 10)
+ {
+ foreach (var doc in results.Skip(i).Take(10))
+ Console.WriteLine(getDocDisplay(doc.Doc));
+
+ if (waitForNextBatch)
+ {
+ Console.Write(nextPrompt);
+ waitForNextBatch = Console.ReadKey().Key != ConsoleKey.Escape;
+ ReplaceConsoleText(Console.Out, nextPrompt.Length, "");
+ Console.SetCursorPosition(0, Console.CursorTop);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static string getDocDisplay(Lucene.Net.Documents.Document doc)
+ {
+ var title = doc.GetField("title");
+ var id = doc.GetField("_ID_");
+ return $"[{id.StringValue}] - {title.StringValue}";
+ }
+ }
+}
diff --git a/Source/LibationCli/Options/SetDownloadStatusOptions.cs b/Source/LibationCli/Options/SetDownloadStatusOptions.cs
index 0bde6a84..1c5f43ac 100644
--- a/Source/LibationCli/Options/SetDownloadStatusOptions.cs
+++ b/Source/LibationCli/Options/SetDownloadStatusOptions.cs
@@ -1,37 +1,69 @@
-using System;
+using ApplicationServices;
+using CommandLine;
+using DataLayer;
+using Dinah.Core;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using ApplicationServices;
-using AudibleUtilities;
-using CommandLine;
namespace LibationCli
{
- [Verb("set-status", HelpText = """
- Set download statuses throughout library based on whether each book's audio file can be found.
- Must include at least one flag: --downloaded , --not-downloaded.
- Downloaded: If the audio file can be found, set download status to 'Downloaded'.
- Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
+ [Verb("set-status", HelpText = """
+ Set download statuses throughout library based on whether each book's audio file can be found.
""")]
- public class SetDownloadStatusOptions : OptionsBase
- {
- [Option(shortName: 'd', longName: "downloaded", Required = true)]
- public bool SetDownloaded { get; set; }
+ public class SetDownloadStatusOptions : OptionsBase
+ {
+ [Option(shortName: 'd', longName: "downloaded", Group = "Download Status", HelpText = "set download status to 'Downloaded'")]
+ public bool SetDownloaded { get; set; }
- [Option(shortName: 'n', longName: "not-downloaded", Required = true)]
- public bool SetNotDownloaded { get; set; }
+ [Option(shortName: 'n', longName: "not-downloaded", Group = "Download Status", HelpText = "set download status to 'Downloaded'")]
+ public bool SetNotDownloaded { get; set; }
- protected override async Task ProcessAsync()
- {
- var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
+ [Option("force", HelpText = "Set the download status regardless of whether the book's audio file can be found. Only one download status option may be used with this option.")]
+ public bool Force { get; set; }
- var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
- await Task.Run(() => bulkSetStatus.Discover());
- bulkSetStatus.Execute();
+ [Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books on which to set download status.")]
+ public IEnumerable Asins { get; set; }
- foreach (var msg in bulkSetStatus.Messages)
- Console.WriteLine(msg);
- }
- }
+ protected override async Task ProcessAsync()
+ {
+ if (Force && SetDownloaded && SetNotDownloaded)
+ {
+ PrintVerbUsage("ERROR:\nWhen run with --force option, only one download status option may be used.");
+ return;
+ }
+
+ var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
+
+ if (Asins.Any())
+ {
+ var asins = Asins.Select(a => a.ToLower()).ToArray();
+ libraryBooks = libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asins)).ToList();
+
+ if (libraryBooks.Count == 0)
+ {
+ Console.Error.WriteLine("Could not find any books matching asins");
+ return;
+ }
+ }
+
+ if (Force)
+ {
+ var status = SetDownloaded ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
+
+ var num = libraryBooks.UpdateBookStatus(status);
+ Console.WriteLine($"Set LiberatedStatus to '{status}' on {"book".PluralizeWithCount(num)}");
+ }
+ else
+ {
+ var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
+ await Task.Run(() => bulkSetStatus.Discover());
+ bulkSetStatus.Execute();
+
+ foreach (var msg in bulkSetStatus.Messages)
+ Console.WriteLine(msg);
+ }
+ }
+ }
}
diff --git a/Source/LibationCli/Options/VersionOptions.cs b/Source/LibationCli/Options/VersionOptions.cs
new file mode 100644
index 00000000..4ebced9e
--- /dev/null
+++ b/Source/LibationCli/Options/VersionOptions.cs
@@ -0,0 +1,59 @@
+using AppScaffolding;
+using CommandLine;
+using System;
+using System.Threading.Tasks;
+
+namespace LibationCli.Options;
+
+[Verb("version", HelpText = "Display version information.")]
+internal class VersionOptions : OptionsBase
+{
+ [Option('c', "check", Required = false, HelpText = "Check if an upgrade is available")]
+ public bool CheckForUpgrade { get; set; }
+
+ protected override Task ProcessAsync()
+ {
+ const string checkingForUpgrade = "Checking for upgrade...";
+ Console.WriteLine($"Libation {LibationScaffolding.Variety} v{LibationScaffolding.BuildVersion.ToString(3)}");
+
+ if (CheckForUpgrade)
+ {
+ Console.Write(checkingForUpgrade);
+
+ var origColor = Console.ForegroundColor;
+ try
+ {
+ var upgradeProperties = LibationScaffolding.GetLatestRelease();
+
+ if (upgradeProperties is null)
+ {
+ Console.ForegroundColor = ConsoleColor.Green;
+ ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, "No available upgrade");
+ Console.WriteLine();
+ }
+ else
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, $"Upgrade Available: v{upgradeProperties.LatestRelease.ToString(3)}");
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine(upgradeProperties.ZipUrl);
+ Console.WriteLine();
+ Console.WriteLine("Release Notes");
+ Console.WriteLine("=============");
+ Console.WriteLine(upgradeProperties.Notes);
+ }
+ }
+ catch
+ {
+ Console.Error.WriteLine("ERROR CHECKING FOR UPGRADE");
+ }
+ finally
+ {
+ Console.ForegroundColor = origColor;
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/Source/LibationCli/Options/_OptionsBase.cs b/Source/LibationCli/Options/_OptionsBase.cs
index 40578b75..cce3f0ee 100644
--- a/Source/LibationCli/Options/_OptionsBase.cs
+++ b/Source/LibationCli/Options/_OptionsBase.cs
@@ -1,8 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using CommandLine;
+using System;
+using System.IO;
+using System.Reflection;
using System.Threading.Tasks;
-using CommandLine;
namespace LibationCli
{
@@ -17,15 +17,34 @@ namespace LibationCli
catch (Exception ex)
{
Environment.ExitCode = (int)ExitCode.RunTimeError;
-
- Console.Error.WriteLine("ERROR");
- Console.Error.WriteLine("=====");
- Console.Error.WriteLine(ex.Message);
- Console.Error.WriteLine();
- Console.Error.WriteLine(ex.StackTrace);
+ PrintVerbUsage(new string[]
+ {
+ "ERROR",
+ "=====",
+ ex.Message,
+ "",
+ ex.StackTrace
+ });
}
}
+ protected void PrintVerbUsage(params string[] linesBeforeUsage)
+ {
+ var verb = GetType().GetCustomAttribute().Name;
+ var helpText = new HelpVerb { HelpType = verb }.GetHelpText();
+ helpText.AddPreOptionsLines(linesBeforeUsage);
+ helpText.AddPreOptionsLine("");
+ helpText.AddPreOptionsLine($"{verb} Usage:");
+ Console.Error.WriteLine(helpText);
+ }
+
+ protected static void ReplaceConsoleText(TextWriter writer, int previousLength, string newText)
+ {
+ writer.Write(new string('\b', previousLength));
+ writer.Write(newText);
+ writer.Write(new string(' ', int.Max(0, previousLength - newText.Length)));
+ }
+
protected abstract Task ProcessAsync();
}
}
diff --git a/Source/LibationCli/Options/_ProcessableOptionsBase.cs b/Source/LibationCli/Options/_ProcessableOptionsBase.cs
index 664499c1..bb4cd735 100644
--- a/Source/LibationCli/Options/_ProcessableOptionsBase.cs
+++ b/Source/LibationCli/Options/_ProcessableOptionsBase.cs
@@ -1,23 +1,34 @@
-using System;
+using ApplicationServices;
+using CommandLine;
+using DataLayer;
+using Dinah.Core;
+using FileLiberator;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using ApplicationServices;
-using CommandLine;
-using DataLayer;
-using FileLiberator;
namespace LibationCli
{
public abstract class ProcessableOptionsBase : OptionsBase
{
+
+ [Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books to process.")]
+ public IEnumerable Asins { get; set; }
+
protected static TProcessable CreateProcessable(EventHandler completedAction = null)
where TProcessable : Processable, new()
{
+ var progressBar = new ConsoleProgressBar(Console.Out);
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 += (o, e) =>
+ {
+ progressBar.Clear();
+ Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
+ };
strProc.Completed += (s, e) =>
{
@@ -32,12 +43,23 @@ namespace LibationCli
}
};
+ strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
+ strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
+
return strProc;
}
- protected static async Task RunAsync(Processable Processable)
+ protected async Task RunAsync(Processable Processable)
{
- foreach (var libraryBook in Processable.GetValidLibraryBooks(DbContexts.GetLibrary_Flat_NoTracking()))
+ var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking().AsEnumerable();
+
+ if (Asins.Any())
+ {
+ var asinsLower = Asins.Select(a => a.ToLower()).ToArray();
+ libraryBooks = libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asinsLower));
+ }
+
+ foreach (var libraryBook in Processable.GetValidLibraryBooks(libraryBooks))
await ProcessOneAsync(Processable, libraryBook, false);
var done = "Done. All books have been processed";
diff --git a/Source/LibationCli/Program.cs b/Source/LibationCli/Program.cs
index 3bdce3da..19538dd5 100644
--- a/Source/LibationCli/Program.cs
+++ b/Source/LibationCli/Program.cs
@@ -1,12 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using CommandLine;
+using CommandLine;
using CommandLine.Text;
using Dinah.Core;
-using Dinah.Core.Collections;
-using Dinah.Core.Collections.Generic;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
namespace LibationCli
{
@@ -19,47 +16,63 @@ namespace LibationCli
}
class Program
{
- static async Task Main(string[] args)
+ public readonly static Type[] VerbTypes = Setup.LoadVerbs();
+ static async Task Main(string[] args)
{
- //***********************************************//
- // //
- // do not use Configuration before this line //
- // //
- //***********************************************//
- Setup.Initialize();
-
- var types = Setup.LoadVerbs();
#if DEBUG
string input = null;
+ //input = " set-status -n --force B017V4IM1G";
+ //input = " liberate B017V4IM1G";
+ //input = " convert B017V4IM1G";
+ //input = " search \"-liberated\"";
//input = " export --help";
+ //input = " version --check";
//input = " scan rmcrackan";
+ //input = " help set-status";
//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);
+ var result = new Parser(ConfigureParser).ParseArguments(args, VerbTypes);
- // if successfully parsed
- // async: run parsed options
- await result.WithParsedAsync(opt => opt.Run());
+ if (result.Value is HelpVerb helper)
+ Console.Error.WriteLine(helper.GetHelpText());
+ else if (result.TypeInfo.Current == typeof(HelpVerb))
+ {
+ //Error parsing the command, but the verb type was identified as HelpVerb
+ //Print LibationCli usage
+ var helpText = HelpVerb.CreateHelpText();
+ helpText.AddVerbs(VerbTypes);
+ Console.Error.WriteLine(helpText);
+ }
+ else if (result.Errors.Any())
+ HandleErrors(result);
+ else
+ {
+ //Everything parsed correctly, so execute the command
- // if not successfully parsed
- // sync: handle parse errors
- result.WithNotParsed(errors => HandleErrors(result, errors));
+ //***********************************************//
+ // //
+ // do not use Configuration before this line //
+ // //
+ //***********************************************//
+ Setup.Initialize();
- return Environment.ExitCode;
+ // if successfully parsed
+ // async: run parsed options
+ await result.WithParsedAsync(opt => opt.Run());
+ }
}
- private static void HandleErrors(ParserResult
-
+
diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs
index 3c10db4c..e8e0423f 100644
--- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs
+++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs
@@ -51,6 +51,34 @@ namespace AaxDecrypter
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("©", "©");
+
+ //Add audiobook shelf tags
+ //https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
+ const string tagDomain = "com.pilabor.tone";
+
+ AaxFile.AppleTags.Title = DownloadOptions.Title;
+
+ if (DownloadOptions.Subtitle is string subtitle)
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
+
+ if (DownloadOptions.Publisher is string publisher)
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
+
+ if (DownloadOptions.Language is string language)
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
+
+ if (DownloadOptions.AudibleProductId is string asin)
+ {
+ AaxFile.AppleTags.Asin = asin;
+ AaxFile.AppleTags.AppleListBox.EditOrAddTag("asin", asin);
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
+ }
+
+ if (DownloadOptions.SeriesName is string series)
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
+
+ if (DownloadOptions.SeriesNumber is float part)
+ AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
}
//Finishing configuring lame encoder.
diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs
index ccbb3808..6edca962 100644
--- a/Source/AaxDecrypter/IDownloadOptions.cs
+++ b/Source/AaxDecrypter/IDownloadOptions.cs
@@ -21,7 +21,14 @@ namespace AaxDecrypter
long DownloadSpeedBps { get; }
ChapterInfo ChapterInfo { get; }
bool FixupFile { get; }
- NAudio.Lame.LameConfig LameConfig { get; }
+ string AudibleProductId { get; }
+ string Title { get; }
+ string Subtitle { get; }
+ string Publisher { get; }
+ string Language { get; }
+ string SeriesName { get; }
+ float? SeriesNumber { get; }
+ NAudio.Lame.LameConfig LameConfig { get; }
bool Downsample { get; }
bool MatchSourceBitrate { get; }
bool MoveMoovToBeginning { get; }
diff --git a/Source/AaxDecrypter/MpegUtil.cs b/Source/AaxDecrypter/MpegUtil.cs
index d7ab9c37..f863e963 100644
--- a/Source/AaxDecrypter/MpegUtil.cs
+++ b/Source/AaxDecrypter/MpegUtil.cs
@@ -1,4 +1,5 @@
using AAXClean;
+using AAXClean.Codecs;
using NAudio.Lame;
using System;
@@ -6,6 +7,7 @@ namespace AaxDecrypter
{
public static class MpegUtil
{
+ private const string TagDomain = "com.pilabor.tone";
public static void ConfigureLameOptions(Mp4File mp4File, LameConfig lameConfig, bool downsample, bool matchSourceBitrate)
{
double bitrateMultiple = 1;
@@ -36,6 +38,21 @@ namespace AaxDecrypter
else if (lameConfig.VBR == VBRMode.ABR)
lameConfig.ABRRateKbps = kbps;
}
+
+ //Setup metadata tags
+ lameConfig.ID3 = mp4File.AppleTags.ToIDTags();
+
+ if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
+ lameConfig.ID3.Subtitle = subtitle;
+
+ if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "LANGUAGE") is string lang)
+ lameConfig.ID3.UserDefinedText.Add("LANGUAGE", lang);
+
+ if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SERIES") is string series)
+ lameConfig.ID3.UserDefinedText.Add("SERIES", series);
+
+ if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "PART") is string part)
+ lameConfig.ID3.UserDefinedText.Add("PART", part);
}
}
}
diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs
index 7b46170d..76b80967 100644
--- a/Source/FileLiberator/DownloadOptions.cs
+++ b/Source/FileLiberator/DownloadOptions.cs
@@ -21,6 +21,13 @@ namespace FileLiberator
public TimeSpan RuntimeLength { get; init; }
public OutputFormat OutputFormat { get; init; }
public ChapterInfo ChapterInfo { get; init; }
+ public string Title => LibraryBook.Book.Title;
+ public string Subtitle => LibraryBook.Book.Subtitle;
+ public string Publisher => LibraryBook.Book.Publisher;
+ public string Language => LibraryBook.Book.Language;
+ public string AudibleProductId => LibraryBookDto.AudibleProductId;
+ public string SeriesName => LibraryBookDto.SeriesName;
+ public float? SeriesNumber => LibraryBookDto.SeriesNumber;
public NAudio.Lame.LameConfig LameConfig { get; init; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;