diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index cffb3817..7fce064b 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -418,16 +418,24 @@ public partial class Configuration [Description("Auto-scroll the download queue to keep active downloads in view.")] public bool AutoScrollQueue { get => GetNonString(defaultValue: true); set => SetNonString(value); } - [Description("Maximum number of books to download and decrypt simultaneously. Defaults to the number of logical processors.")] + /// The lowest can go: one book at a time. + public const int MinConcurrentDownloads = 1; + + /// + /// The highest can go. Audible throttles license + /// requests, so more concurrency stops helping well before this and starts producing + /// license denials instead. + /// + public const int MaxAllowedConcurrentDownloads = 10; + + /// Deliberately conservative. See . + public const int DefaultConcurrentDownloads = 3; + + [Description("Maximum number of books to download and decrypt simultaneously. Set to 1 to download one book at a time.")] public int MaxConcurrentDownloads { - get - { - var value = GetNonString(defaultValue: 0); - // Treat 0 or 1 as "use default" — 1 may have been written by an earlier bug. - return value <= 1 ? Environment.ProcessorCount : value; - } - set => SetNonString(Math.Max(2, value)); + get => Math.Clamp(GetNonString(defaultValue: DefaultConcurrentDownloads), MinConcurrentDownloads, MaxAllowedConcurrentDownloads); + set => SetNonString(Math.Clamp(value, MinConcurrentDownloads, MaxAllowedConcurrentDownloads)); } [Description("Global download speed limit in bytes per second.")] diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 1984db4f..f149790f 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -43,17 +43,31 @@ public class ProcessQueueViewModel : ReactiveObject SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; MaxConcurrentDownloads = Configuration.Instance.MaxConcurrentDownloads; AutoScrollQueue = Configuration.Instance.AutoScrollQueue; - MultiThreadEnabled = true; } public int CompletedCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyCompleted)); } } public int QueuedCount { get => field; private set { this.RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyQueued)); } } public int ErrorCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyErrors)); } } - public int MaxConcurrentDownloads { get => field; set { RaiseAndSetIfChanged(ref field, value); Configuration.Instance.MaxConcurrentDownloads = value; } } + /// + /// How many books download and decrypt at once. + /// means one at a time, which is how Libation behaved before parallel downloads existed - so this + /// single value is both the limit and the off switch, and the two can never disagree. + /// + public int MaxConcurrentDownloads + { + get => field; + set + { + var clamped = Math.Clamp(value, Configuration.MinConcurrentDownloads, Configuration.MaxAllowedConcurrentDownloads); + RaiseAndSetIfChanged(ref field, clamped); + Configuration.Instance.MaxConcurrentDownloads = clamped; + } + } public bool AutoScrollQueue { get => field; set { RaiseAndSetIfChanged(ref field, value); Configuration.Instance.AutoScrollQueue = value; } } - public bool MultiThreadEnabled { get => field; set => RaiseAndSetIfChanged(ref field, value); } - private int EffectiveConcurrentDownloads => MultiThreadEnabled ? Math.Max(2, MaxConcurrentDownloads) : 1; + /// Exposed so UI controls can bind their spinner bounds rather than hardcoding them. + public static int MinConcurrentDownloads => Configuration.MinConcurrentDownloads; + public static int MaxAllowedConcurrentDownloads => Configuration.MaxAllowedConcurrentDownloads; public string? RunningTime { get => field; set => RaiseAndSetIfChanged(ref field, value); } public bool ProgressBarVisible { get => field; set => RaiseAndSetIfChanged(ref field, value); } public bool AnyCompleted => CompletedCount > 0; @@ -662,7 +676,7 @@ public class ProcessQueueViewModel : ReactiveObject } // If at capacity, wait for a slot to open before trying to dequeue more - if (activeTasks.Count >= EffectiveConcurrentDownloads) + if (activeTasks.Count >= MaxConcurrentDownloads) { await Task.WhenAny(activeTasks); continue; diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs index c72a37ae..788f1305 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs @@ -43,7 +43,8 @@ this.virtualFlowControl2 = new LibationWinForms.ProcessQueue.VirtualFlowControl(); this.panel1 = new System.Windows.Forms.Panel(); this.autoScrollChk = new System.Windows.Forms.CheckBox(); - this.multiThreadChk = new System.Windows.Forms.CheckBox(); + this.concurrencyLbl = new System.Windows.Forms.Label(); + this.concurrencyNum = new System.Windows.Forms.NumericUpDown(); this.label1 = new System.Windows.Forms.Label(); this.numericUpDown1 = new LibationWinForms.ProcessQueue.NumericUpDownSuffix(); this.btnCleanFinished = new System.Windows.Forms.Button(); @@ -61,6 +62,7 @@ this.tabPage1.SuspendLayout(); this.panel1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.concurrencyNum)).BeginInit(); this.tabPage2.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.logDGV)).BeginInit(); this.panel2.SuspendLayout(); @@ -160,7 +162,8 @@ this.panel1.BackColor = System.Drawing.SystemColors.Control; this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.panel1.Controls.Add(this.autoScrollChk); - this.panel1.Controls.Add(this.multiThreadChk); + this.panel1.Controls.Add(this.concurrencyLbl); + this.panel1.Controls.Add(this.concurrencyNum); this.panel1.Controls.Add(this.label1); this.panel1.Controls.Add(this.numericUpDown1); this.panel1.Controls.Add(this.btnCleanFinished); @@ -183,17 +186,23 @@ this.autoScrollChk.Text = "Auto-scroll"; this.autoScrollChk.UseVisualStyleBackColor = true; // - // multiThreadChk + // concurrencyLbl // - this.multiThreadChk.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Top; - this.multiThreadChk.AutoSize = true; - this.multiThreadChk.Checked = true; - this.multiThreadChk.CheckState = System.Windows.Forms.CheckState.Checked; - this.multiThreadChk.Location = new System.Drawing.Point(82, 28); - this.multiThreadChk.Name = "multiThreadChk"; - this.multiThreadChk.TabIndex = 7; - this.multiThreadChk.Text = "Parallel downloads"; - this.multiThreadChk.UseVisualStyleBackColor = true; + this.concurrencyLbl.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Top; + this.concurrencyLbl.AutoSize = true; + this.concurrencyLbl.Location = new System.Drawing.Point(82, 30); + this.concurrencyLbl.Name = "concurrencyLbl"; + this.concurrencyLbl.Size = new System.Drawing.Size(54, 15); + this.concurrencyLbl.TabIndex = 7; + this.concurrencyLbl.Text = "At once:"; + // + // concurrencyNum + // + this.concurrencyNum.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Top; + this.concurrencyNum.Location = new System.Drawing.Point(136, 27); + this.concurrencyNum.Name = "concurrencyNum"; + this.concurrencyNum.Size = new System.Drawing.Size(45, 23); + this.concurrencyNum.TabIndex = 8; // // label1 // @@ -361,6 +370,7 @@ this.panel1.ResumeLayout(false); this.panel1.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.concurrencyNum)).EndInit(); this.tabPage2.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.logDGV)).EndInit(); this.panel2.ResumeLayout(false); @@ -393,6 +403,7 @@ private NumericUpDownSuffix numericUpDown1; private System.Windows.Forms.Label label1; private System.Windows.Forms.CheckBox autoScrollChk; - private System.Windows.Forms.CheckBox multiThreadChk; + private System.Windows.Forms.Label concurrencyLbl; + private System.Windows.Forms.NumericUpDown concurrencyNum; } } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index 63ad0be1..0b717190 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -39,7 +39,10 @@ internal partial class ProcessQueueControl : UserControl ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged; ViewModel.ProcessStart += Book_ProcessStart; autoScrollChk.CheckedChanged += (s, e) => ViewModel.AutoScrollQueue = autoScrollChk.Checked; - multiThreadChk.CheckedChanged += (s, e) => ViewModel.MultiThreadEnabled = multiThreadChk.Checked; + + concurrencyNum.Minimum = ProcessQueueViewModel.MinConcurrentDownloads; + concurrencyNum.Maximum = ProcessQueueViewModel.MaxAllowedConcurrentDownloads; + concurrencyNum.ValueChanged += (s, e) => ViewModel.MaxConcurrentDownloads = (int)concurrencyNum.Value; ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null)); } @@ -131,8 +134,8 @@ internal partial class ProcessQueueControl : UserControl } if (e.PropertyName is null or nameof(ViewModel.AutoScrollQueue)) autoScrollChk.Checked = ViewModel.AutoScrollQueue; - if (e.PropertyName is null or nameof(ViewModel.MultiThreadEnabled)) - multiThreadChk.Checked = ViewModel.MultiThreadEnabled; + if (e.PropertyName is null or nameof(ViewModel.MaxConcurrentDownloads)) + concurrencyNum.Value = ViewModel.MaxConcurrentDownloads; } ///