Address rmcrackan comments and refactor

This commit is contained in:
MBucari
2026-01-02 13:04:35 -07:00
parent f6b96fc210
commit 396d2c8a95
7 changed files with 87 additions and 68 deletions

View File

@@ -7,6 +7,7 @@ using Avalonia.Threading;
using DataLayer; using DataLayer;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using LibationUiBase.ProcessQueue;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -31,9 +32,9 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
}; };
VM.Books[0].AvailableCodec = "xHE-AAC"; VM.Books[0].AvailableCodec = "xHE-AAC";
VM.Books[0].AvailableBitrate = 256; VM.Books[0].AvailableBitrate = 256;
VM.Books[0].ScanStatus = BookScanStatus.Completed; VM.Books[0].ScanStatus = ProcessBookStatus.Completed;
VM.Books[1].ScanStatus = BookScanStatus.Error; VM.Books[1].ScanStatus = ProcessBookStatus.Failed;
VM.Books[2].ScanStatus = BookScanStatus.Cancelled; VM.Books[2].ScanStatus = ProcessBookStatus.Cancelled;
VM.SignificantCount = 1; VM.SignificantCount = 1;
} }
else else
@@ -49,7 +50,13 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
private async void Opened_ShowInitialMessage(object? sender, System.EventArgs e) private async void Opened_ShowInitialMessage(object? sender, System.EventArgs e)
{ {
await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.OK, MessageBoxIcon.Information); if (!VM.ShowFindBetterQualityBooksHelp)
return;
var result = await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
if (result == DialogResult.No)
{
VM.ShowFindBetterQualityBooksHelp = false;
}
} }
private async void Opened_LoadLibrary(object? sender, System.EventArgs e) private async void Opened_LoadLibrary(object? sender, System.EventArgs e)
@@ -102,7 +109,7 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
if (VM.IsScanning) if (VM.IsScanning)
VM.StopScan(); VM.StopScan();
else else
await Task.Run(VM.ScanAsync); await VM.ScanAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -138,15 +145,15 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
} }
} }
public static FuncValueConverter<BookScanStatus, IBrush?> RowConverter { get; } = new(status => public static FuncValueConverter<ProcessBookStatus, IBrush?> RowConverter { get; } = new(status =>
{ {
var brush = status switch var brush = status switch
{ {
BookScanStatus.Completed => "ProcessQueueBookCompletedBrush", ProcessBookStatus.Completed => "ProcessQueueBookCompletedBrush",
BookScanStatus.Cancelled => "ProcessQueueBookCancelledBrush", ProcessBookStatus.Cancelled => "ProcessQueueBookCancelledBrush",
BookScanStatus.Error => "ProcessQueueBookFailedBrush", ProcessBookStatus.Failed => "ProcessQueueBookFailedBrush",
_ => null, _ => null,
}; };
return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null; return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null;
}); });
} }

View File

@@ -1,15 +1,8 @@
using DataLayer; using DataLayer;
using LibationUiBase.ProcessQueue;
namespace LibationUiBase; namespace LibationUiBase;
public enum BookScanStatus
{
None,
Error,
Cancelled,
Completed,
}
public class BookDataViewModel : ReactiveObject public class BookDataViewModel : ReactiveObject
{ {
public LibraryBook LibraryBook { get; } public LibraryBook LibraryBook { get; }
@@ -48,6 +41,6 @@ public class BookDataViewModel : ReactiveObject
public string? BitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); } public string? BitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? AvailableBitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); } public string? AvailableBitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public bool IsSignificant { get => field; private set => RaiseAndSetIfChanged(ref field, value); } public bool IsSignificant { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public BookScanStatus ScanStatus { get => field; set => RaiseAndSetIfChanged(ref field, value); } public ProcessBookStatus ScanStatus { get => field; set => RaiseAndSetIfChanged(ref field, value); }
private static string? GetBitrateString(int bitrate) => bitrate > 0 ? $"{bitrate} kbps" : null; private static string? GetBitrateString(int bitrate) => bitrate > 0 ? $"{bitrate} kbps" : null;
} }

View File

@@ -4,6 +4,7 @@ using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.Net.Http; using Dinah.Core.Net.Http;
using LibationFileManager; using LibationFileManager;
using LibationUiBase.ProcessQueue;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -34,11 +35,13 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
When done, click the 'Mark X books as Not Liberated' to allow Libation to re-download those books in the higher. When done, click the 'Mark X books as Not Liberated' to allow Libation to re-download those books in the higher.
Note: make sure you adjust your download quality settings before re-liberating the books. Note: make sure you adjust your download quality settings before re-liberating the books.
Display this help message again in the future?
"""; """;
public event EventHandler<BookDataViewModel>? BookScanned; public event EventHandler<BookDataViewModel>? BookScanned;
public IList<BookDataViewModel>? Books { get => field; set => RaiseAndSetIfChanged(ref field, value); } public IList<BookDataViewModel>? Books { get => field; set => RaiseAndSetIfChanged(ref field, value); }
public bool ShowFindBetterQualityBooksHelp { get => Configuration.Instance.GetNonString(defaultValue: true); set => Configuration.Instance.SetNonString(value); }
public bool ScanWidevine { get; set; } public bool ScanWidevine { get; set; }
public int SignificantCount public int SignificantCount
{ {
@@ -93,7 +96,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
{ {
b.AvailableBitrate = 0; b.AvailableBitrate = 0;
b.AvailableCodec = null; b.AvailableCodec = null;
b.ScanStatus = BookScanStatus.None; b.ScanStatus = ProcessBookStatus.Queued;
} }
ScanCount = $"0 of {Books.Count:N0} scanned"; ScanCount = $"0 of {Books.Count:N0} scanned";
@@ -104,19 +107,20 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
for (int i = 0; i < Books.Count; i++) for (int i = 0; i < Books.Count; i++)
{ {
var b = Books[i]; var b = Books[i];
var url = GetUrl(b.LibraryBook);
try try
{ {
cts.Token.ThrowIfCancellationRequested();
var url = GetUrl(b.LibraryBook);
//Don't re-scan a file if we have already loaded existing audio codec and bitrate. //Don't re-scan a file if we have already loaded existing audio codec and bitrate.
if (b.Bitrate == 0 && b.Codec == null) if (b.Bitrate == 0 && b.Codec == null)
{ {
var (file, bestformat) = FindHighestExistingFormat(b.LibraryBook); var (file, bestFormat) = FindHighestExistingFormat(b.LibraryBook);
if (file is not null) if (file is not null)
{ {
b.FoundFile = Configuration.Instance.Books?.Path is string booksDir ? Path.GetRelativePath(booksDir, file) : file; b.FoundFile = Configuration.Instance.Books?.Path is string booksDir ? Path.GetRelativePath(booksDir, file) : file;
b.Bitrate = bestformat.BitRate; b.Bitrate = bestFormat.BitRate;
b.Codec = bestformat.CodecString; b.Codec = bestFormat.CodecString;
} }
else if (b.LibraryBook.Book.UserDefinedItem.LastDownloadedFormat is not null) else if (b.LibraryBook.Book.UserDefinedItem.LastDownloadedFormat is not null)
{ {
@@ -127,7 +131,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
else else
{ {
b.FoundFile = "File not found and no 'Last Downloaded' format found."; b.FoundFile = "File not found and no 'Last Downloaded' format found.";
b.ScanStatus = BookScanStatus.Error; b.ScanStatus = ProcessBookStatus.Failed;
continue; continue;
} }
} }
@@ -137,18 +141,18 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
b.AvailableCodec = codecString; b.AvailableCodec = codecString;
b.AvailableBitrate = bitrate; b.AvailableBitrate = bitrate;
b.ScanStatus = BookScanStatus.Completed; b.ScanStatus = ProcessBookStatus.Completed;
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
b.ScanStatus = BookScanStatus.Cancelled; b.ScanStatus = ProcessBookStatus.Cancelled;
break; break;
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", b.Asin); Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", b.Asin);
b.FoundFile = $"Error: {ex.Message}"; b.FoundFile = $"Error: {ex.Message}";
b.ScanStatus = BookScanStatus.Error; b.ScanStatus = ProcessBookStatus.Failed;
} }
finally finally
{ {
@@ -168,20 +172,20 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
private static (string? file, AudioFormat format) FindHighestExistingFormat(LibraryBook libraryBook) private static (string? file, AudioFormat format) FindHighestExistingFormat(LibraryBook libraryBook)
{ {
var largestfile var largestFile
= AudibleFileStorage.Audio = AudibleFileStorage.Audio
.GetPaths(libraryBook.Book.AudibleProductId) .GetPaths(libraryBook.Book.AudibleProductId)
.Select(p => new FileInfo(p)) .Select(p => new FileInfo(p))
.Where(f => f.Exists && f.Extension.EqualsInsensitive(".m4b")) .Where(f => f.Exists && f.Extension.EqualsInsensitive(".m4b"))
.OrderByDescending(f => f.Length) .OrderByDescending(f => f.Length)
.FirstOrDefault(); .FirstOrDefault()
?.FullName;
if (largestfile is null) return largestFile is null ? (null, AudioFormat.Default)
return (null, AudioFormat.Default); : (largestFile, AudioFormatDecoder.FromMpeg4(largestFile));
return (largestfile.FullName, AudioFormatDecoder.FromMpeg4(largestfile.FullName));
} }
static async Task<(string codec, int bitrate)> ReadAudioInfoAsync(HttpResponseMessage response) private static async Task<(string codec, int bitrate)> ReadAudioInfoAsync(HttpResponseMessage response)
{ {
var data = await response.Content.ReadAsJObjectAsync(); var data = await response.Content.ReadAsJObjectAsync();
var totalLengthMs = data["content_metadata"]?["chapter_info"]?.Value<long>("runtime_length_ms") ?? throw new InvalidDataException("Missing runtime length"); var totalLengthMs = data["content_metadata"]?["chapter_info"]?.Value<long>("runtime_length_ms") ?? throw new InvalidDataException("Missing runtime length");
@@ -202,12 +206,12 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
return (codecString, bitrate); return (codecString, bitrate);
} }
string GetUrl(LibraryBook libraryBook) private string GetUrl(LibraryBook libraryBook)
{ {
var drm_type = ScanWidevine ? "Widevine" : "Adrm"; var drm_type = ScanWidevine ? "Widevine" : "Adrm";
var locale = AudibleApi.Localization.Get(libraryBook.Book.Locale); var locale = AudibleApi.Localization.Get(libraryBook.Book.Locale);
return string.Format(BaseUrl, locale.TopDomain, libraryBook.Book.AudibleProductId, drm_type); return string.Format(BaseUrl, locale.TopDomain, libraryBook.Book.AudibleProductId, drm_type);
} }
const string BaseUrl = "ht" + "tps://api.audible.{0}/1.0/content/{1}/metadata?response_groups=chapter_info,content_reference&quality=High&drm_type={2}"; private const string BaseUrl = "ht" + "tps://api.audible.{0}/1.0/content/{1}/metadata?response_groups=chapter_info,content_reference&quality=High&drm_type={2}";
} }

View File

@@ -64,7 +64,6 @@ partial class FindBetterQualityBooksDialog
dataGridView1.RowHeadersVisible = false; dataGridView1.RowHeadersVisible = false;
dataGridView1.Size = new System.Drawing.Size(897, 397); dataGridView1.Size = new System.Drawing.Size(897, 397);
dataGridView1.TabIndex = 0; dataGridView1.TabIndex = 0;
dataGridView1.CellFormatting += dataGridView1_CellFormatting;
// //
// asinDataGridViewTextBoxColumn // asinDataGridViewTextBoxColumn
// //

View File

@@ -1,8 +1,10 @@
using ApplicationServices; using ApplicationServices;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System; using System;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
@@ -34,12 +36,23 @@ public partial class FindBetterQualityBooksDialog : Form
Shown += Shown_LoadLibrary; Shown += Shown_LoadLibrary;
Shown += Shown_ShowInitialMessage; Shown += Shown_ShowInitialMessage;
FormClosing += FindBetterQualityBooksDialog_FormClosing; FormClosing += FindBetterQualityBooksDialog_FormClosing;
SetDoubleBuffer(dataGridView1, true);
} }
static void SetDoubleBuffer(Control control, bool DoubleBuffered)
{
typeof(Control).InvokeMember("DoubleBuffered", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, control, [DoubleBuffered]);
}
private void Shown_ShowInitialMessage(object? sender, EventArgs e) private void Shown_ShowInitialMessage(object? sender, EventArgs e)
{ {
MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information); if (!VM.ShowFindBetterQualityBooksHelp)
return;
var result = MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Text, MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
if (result == DialogResult.No)
{
VM.ShowFindBetterQualityBooksHelp = false;
}
} }
private async void Shown_LoadLibrary(object? sender, EventArgs e) private async void Shown_LoadLibrary(object? sender, EventArgs e)
@@ -51,7 +64,13 @@ public partial class FindBetterQualityBooksDialog : Form
Invoke(() => Invoke(() =>
{ {
bookDataViewModelBindingSource.DataSource = VM.Books; bookDataViewModelBindingSource.DataSource = VM.Books;
foreach (DataGridViewRow r in dataGridView1.Rows)
{
//Force creation of DefaultCellStyle to speed up later coloring
//_ = r.DefaultCellStyle;
}
btnScan.Enabled = true; btnScan.Enabled = true;
dataGridView1.CellFormatting += dataGridView1_CellFormatting;
}); });
} }
@@ -87,6 +106,8 @@ public partial class FindBetterQualityBooksDialog : Form
{ {
await scanTask; await scanTask;
scanTask = null; scanTask = null;
//give the UI a moment to update after cancelling the first close
await Task.Delay(100);
Invoke(Close); Invoke(Close);
} }
} }
@@ -148,7 +169,7 @@ public partial class FindBetterQualityBooksDialog : Form
} }
} }
private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) private void dataGridView1_CellFormatting(object? sender, DataGridViewCellFormattingEventArgs e)
{ {
if (e.RowIndex < 0 || e.RowIndex >= dataGridView1.Rows.Count) if (e.RowIndex < 0 || e.RowIndex >= dataGridView1.Rows.Count)
return; return;
@@ -156,18 +177,7 @@ public partial class FindBetterQualityBooksDialog : Form
var row = dataGridView1.Rows[e.RowIndex]; var row = dataGridView1.Rows[e.RowIndex];
if (row.DataBoundItem is BookDataViewModel bvm) if (row.DataBoundItem is BookDataViewModel bvm)
{ {
///yes, this is tight coupling and bad practice. row.DefaultCellStyle.BackColor = bvm.ScanStatus.GetColor();
///If we ever need tese colors in a third place,
///consider moving them to a shared location like
///App.axaml in LibationAvalonia
var color = bvm.ScanStatus switch
{
BookScanStatus.Completed => ProcessQueue.ProcessBookControl.SuccessColor,
BookScanStatus.Cancelled => ProcessQueue.ProcessBookControl.CancelledColor,
BookScanStatus.Error => ProcessQueue.ProcessBookControl.FailedColor,
_ => ProcessQueue.ProcessBookControl.QueuedColor,
};
row.DefaultCellStyle.BackColor = color;
} }
} }
} }

View File

@@ -12,11 +12,6 @@ namespace LibationWinForms.ProcessQueue
private readonly int ProgressBarDistanceFromEdge; private readonly int ProgressBarDistanceFromEdge;
private object? m_OldContext; private object? m_OldContext;
public static Color FailedColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x50, 0x27, 0x27) : Color.LightCoral;
public static Color CancelledColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x4e, 0x4b, 0x15) : Color.Khaki;
public static Color QueuedColor { get; } = SystemColors.Control;
public static Color SuccessColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x1c, 0x3e, 0x20) : Color.PaleGreen;
public ProcessBookControl() public ProcessBookControl()
{ {
InitializeComponent(); InitializeComponent();
@@ -82,15 +77,6 @@ namespace LibationWinForms.ProcessQueue
private void SetStatus(ProcessBookStatus status, string statusText) private void SetStatus(ProcessBookStatus status, string statusText)
{ {
Color backColor = status switch
{
ProcessBookStatus.Completed => SuccessColor,
ProcessBookStatus.Cancelled => CancelledColor,
ProcessBookStatus.Queued => QueuedColor,
ProcessBookStatus.Working => QueuedColor,
_ => FailedColor
};
cancelBtn.Visible = status is ProcessBookStatus.Queued or ProcessBookStatus.Working; cancelBtn.Visible = status is ProcessBookStatus.Queued or ProcessBookStatus.Working;
moveLastBtn.Visible = status == ProcessBookStatus.Queued; moveLastBtn.Visible = status == ProcessBookStatus.Queued;
moveDownBtn.Visible = status == ProcessBookStatus.Queued; moveDownBtn.Visible = status == ProcessBookStatus.Queued;
@@ -101,7 +87,7 @@ namespace LibationWinForms.ProcessQueue
etaLbl.Visible = status == ProcessBookStatus.Working; etaLbl.Visible = status == ProcessBookStatus.Working;
statusLbl.Visible = status != ProcessBookStatus.Working; statusLbl.Visible = status != ProcessBookStatus.Working;
statusLbl.Text = statusText; statusLbl.Text = statusText;
BackColor = backColor; BackColor = status.GetColor();
int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge; int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge;

View File

@@ -1,4 +1,5 @@
using System.Drawing; using LibationUiBase.ProcessQueue;
using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms; namespace LibationWinForms;
@@ -9,6 +10,13 @@ internal static class ThemeExtensions
private static readonly Color LinkLabelVisited = Color.FromKnownColor(KnownColor.Purple); private static readonly Color LinkLabelVisited = Color.FromKnownColor(KnownColor.Purple);
private static readonly Color LinkLabelNew_Dark = Color.FromKnownColor(KnownColor.CornflowerBlue); private static readonly Color LinkLabelNew_Dark = Color.FromKnownColor(KnownColor.CornflowerBlue);
private static readonly Color LinkLabelVisited_Dark = Color.FromKnownColor(KnownColor.Orchid); private static readonly Color LinkLabelVisited_Dark = Color.FromKnownColor(KnownColor.Orchid);
private static readonly Color FailedColor = Color.LightCoral;
private static readonly Color FailedColor_Dark = Color.FromArgb(0x50, 0x27, 0x27);
private static readonly Color CancelledColor = Color.Khaki;
private static readonly Color CancelledColor_Dark = Color.FromArgb(0x4e, 0x4b, 0x15);
private static readonly Color SuccessColor = Color.PaleGreen;
private static readonly Color SuccessColor_Dark = Color.FromArgb(0x1c, 0x3e, 0x20);
public static Color LinkColor => Application.IsDarkModeEnabled ? LinkLabelNew_Dark : LinkLabelNew; public static Color LinkColor => Application.IsDarkModeEnabled ? LinkLabelNew_Dark : LinkLabelNew;
public static Color VisitedLinkColor => Application.IsDarkModeEnabled ? LinkLabelVisited_Dark : LinkLabelVisited; public static Color VisitedLinkColor => Application.IsDarkModeEnabled ? LinkLabelVisited_Dark : LinkLabelVisited;
extension(LinkLabel ll) extension(LinkLabel ll)
@@ -19,4 +27,16 @@ internal static class ThemeExtensions
ll.LinkColor = LinkColor; ll.LinkColor = LinkColor;
} }
} }
extension(ProcessBookStatus status)
{
public Color GetColor() => status switch
{
ProcessBookStatus.Completed => Application.IsDarkModeEnabled ? SuccessColor_Dark : SuccessColor,
ProcessBookStatus.Cancelled => Application.IsDarkModeEnabled ? CancelledColor_Dark : CancelledColor,
ProcessBookStatus.Queued => SystemColors.Control,
ProcessBookStatus.Working => SystemColors.Control,
_ => Application.IsDarkModeEnabled ? FailedColor_Dark : FailedColor
};
}
} }