diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs
index 334357f6..093bd1ab 100644
--- a/Source/AaxDecrypter/NetworkFileStream.cs
+++ b/Source/AaxDecrypter/NetworkFileStream.cs
@@ -41,6 +41,10 @@ namespace AaxDecrypter
[JsonIgnore]
public bool IsCancelled => _cancellationSource.IsCancellationRequested;
+ private static long _globalSpeedLimit = 0;
+ /// bytes per second
+ public static long GlobalSpeedLimit { get => _globalSpeedLimit; set => _globalSpeedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); }
+
#endregion
#region Private Properties
@@ -61,6 +65,13 @@ namespace AaxDecrypter
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
+ //Number of times per second the download rate is checkd and throttled
+ private const int THROTTLE_FREQUENCY = 8;
+
+ //Minimum throttle rate. The minimum amount of data that can be throttled
+ //on each iteration of the download loop is DOWNLOAD_BUFF_SZ.
+ private const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY;
+
#endregion
#region Constructor
@@ -168,6 +179,8 @@ namespace AaxDecrypter
try
{
+ DateTime startTime = DateTime.Now;
+ long bytesReadSinceThrottle = 0;
int bytesRead;
do
{
@@ -185,6 +198,22 @@ namespace AaxDecrypter
_downloadedPiece.Set();
}
+ #region throttle
+
+ bytesReadSinceThrottle += bytesRead;
+
+ if (GlobalSpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > GlobalSpeedLimit / THROTTLE_FREQUENCY)
+ {
+ var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.Now).TotalMilliseconds;
+ if (delayMS > 0)
+ await Task.Delay(delayMS, _cancellationSource.Token);
+
+ startTime = DateTime.Now;
+ bytesReadSinceThrottle = 0;
+ }
+
+ #endregion
+
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
WritePosition = downloadPosition;
@@ -195,9 +224,9 @@ namespace AaxDecrypter
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
- catch (Exception ex)
+ catch (TaskCanceledException)
{
- Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
+ Serilog.Log.Information("Download was cancelled");
}
finally
{
diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index 05d5a400..a687dd2c 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -176,6 +176,9 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
config.AutoDownloadEpisodes = false;
+
+ if (!config.Exists(nameof(config.DownloadSpeedLimit)))
+ config.DownloadSpeedLimit = 0;
}
/// Initialize logging. Wire-up events. Run after migration
diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
index 45631b66..2c483c3e 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
+using LibationFileManager;
using ReactiveUI;
using System;
using System.Collections.Generic;
@@ -25,9 +26,14 @@ namespace LibationAvalonia.ViewModels
public ProcessQueueViewModel()
{
+ Logger = LogMe.RegisterForm(this);
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
- Logger = LogMe.RegisterForm(this);
+
+ if (Design.IsDesignMode)
+ AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
+
+ SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private int _completedCount;
@@ -35,6 +41,7 @@ namespace LibationAvalonia.ViewModels
private int _queuedCount;
private string _runningTime;
private bool _progressBarVisible;
+ private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); } }
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); } }
@@ -46,6 +53,37 @@ namespace LibationAvalonia.ViewModels
public bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
+ public decimal SpeedLimit
+ {
+ get
+ {
+ return _speedLimit;
+ }
+ set
+ {
+ var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
+ var config = Configuration.Instance;
+ config.DownloadSpeedLimit = newValue;
+
+ _speedLimit
+ = config.DownloadSpeedLimit <= newValue ? value
+ : value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
+ : 0;
+
+ config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
+
+ SpeedLimitIncrement = _speedLimit > 100 ? 10
+ : _speedLimit > 10 ? 1
+ : _speedLimit > 1 ? 0.1m
+ : 0.01m;
+
+ this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
+ this.RaisePropertyChanged();
+ }
+ }
+
+ public decimal SpeedLimitIncrement { get; private set; }
+
private void Queue_CompletedCountChanged(object sender, int e)
{
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml b/Source/LibationAvalonia/Views/MainWindow.axaml
index 9640b41b..12445243 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml
@@ -172,7 +172,7 @@
-
+
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
index e090119c..d1f6d1ff 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml
@@ -4,10 +4,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LibationAvalonia.Views"
+ xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="850"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850"
x:Class="LibationAvalonia.Views.ProcessQueueControl">
+
+
+
+
@@ -32,25 +37,43 @@
Process Queue
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
index 7a4c3aef..840a90f0 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
@@ -1,11 +1,13 @@
-using ApplicationServices;
+using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using DataLayer;
using LibationAvalonia.ViewModels;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
namespace LibationAvalonia.Views
@@ -86,6 +88,11 @@ namespace LibationAvalonia.Views
#endregion
}
+ public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
+ {
+ if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus();
+ }
+
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
@@ -148,4 +155,41 @@ namespace LibationAvalonia.Views
#endregion
}
+
+ public class DecimalConverter : IValueConverter
+ {
+ public static readonly DecimalConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
+ {
+ if (sourceText == "∞") return 0;
+
+ for (int i = sourceText.Length; i > 0; i--)
+ {
+ if (decimal.TryParse(sourceText[..i], out var val))
+ return val;
+ }
+
+ return 0;
+ }
+ return 0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is decimal val)
+ {
+ return
+ val == 0 ? "∞"
+ : (
+ val >= 10 ? ((long)val).ToString()
+ : val >= 1 ? val.ToString("F1")
+ : val.ToString("F2")
+ ) + " MB/s";
+ }
+ return value.ToString();
+ }
+ }
}
diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs
index 4b48d05e..430e5cea 100644
--- a/Source/LibationFileManager/Configuration.PersistentSettings.cs
+++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs
@@ -274,9 +274,24 @@ namespace LibationFileManager
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value);
}
- #region templates: custom file naming
+ [Description("Global download speed limit in bytes per second.")]
+ public long DownloadSpeedLimit
+ {
+ get
+ {
+ AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = persistentDictionary.GetNonString(nameof(DownloadSpeedLimit));
+ return AaxDecrypter.NetworkFileStream.GlobalSpeedLimit;
+ }
+ set
+ {
+ AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = value;
+ persistentDictionary.SetNonString(nameof(DownloadSpeedLimit), AaxDecrypter.NetworkFileStream.GlobalSpeedLimit);
+ }
+ }
- [Description("Edit how filename characters are replaced")]
+ #region templates: custom file naming
+
+ [Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters
{
get => persistentDictionary.GetNonString(nameof(ReplacementCharacters));
diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs
index 86767aa9..031702ae 100644
--- a/Source/LibationWinForms/Form1.ProcessQueue.cs
+++ b/Source/LibationWinForms/Form1.ProcessQueue.cs
@@ -15,6 +15,7 @@ namespace LibationWinForms
private void Configure_ProcessQueue()
{
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
+ splitContainer1.Panel2MinSize = 350;
var coppalseState = Configuration.Instance.GetNonString(nameof(splitContainer1.Panel2Collapsed));
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width;
diff --git a/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs b/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs
index 56893822..f9aecde5 100644
--- a/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs
+++ b/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs
@@ -31,6 +31,8 @@ namespace LibationWinForms.GridView
public override Type EditType => typeof(MyRatingCellEditor);
public override Type ValueType => typeof(Rating);
+ public MyRatingGridViewCell() { ToolTipText = "Click to change ratings"; }
+
public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
@@ -43,7 +45,7 @@ namespace LibationWinForms.GridView
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
if (value is Rating rating)
- {
+ {
ToolTipText = "Click to change ratings";
var starString = rating.ToStarString();
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
index f600fd5d..7739bd3e 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.Designer.cs
@@ -43,6 +43,8 @@
this.panel3 = new System.Windows.Forms.Panel();
this.virtualFlowControl2 = new LibationWinForms.ProcessQueue.VirtualFlowControl();
this.panel1 = new System.Windows.Forms.Panel();
+ this.label1 = new System.Windows.Forms.Label();
+ this.numericUpDown1 = new LibationWinForms.ProcessQueue.NumericUpDownSuffix();
this.btnCleanFinished = new System.Windows.Forms.Button();
this.cancelAllBtn = new System.Windows.Forms.Button();
this.tabPage2 = new System.Windows.Forms.TabPage();
@@ -58,6 +60,7 @@
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
this.panel1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
this.tabPage2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.logDGV)).BeginInit();
this.panel2.SuspendLayout();
@@ -166,6 +169,8 @@
//
this.panel1.BackColor = System.Drawing.SystemColors.Control;
this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.panel1.Controls.Add(this.label1);
+ this.panel1.Controls.Add(this.numericUpDown1);
this.panel1.Controls.Add(this.btnCleanFinished);
this.panel1.Controls.Add(this.cancelAllBtn);
this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
@@ -174,6 +179,44 @@
this.panel1.Size = new System.Drawing.Size(390, 25);
this.panel1.TabIndex = 2;
//
+ // label1
+ //
+ this.label1.Anchor = System.Windows.Forms.AnchorStyles.Right;
+ this.label1.AutoSize = true;
+ this.label1.Location = new System.Drawing.Point(148, 4);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(54, 15);
+ this.label1.TabIndex = 5;
+ this.label1.Text = "DL Limit:";
+ //
+ // numericUpDown1
+ //
+ this.numericUpDown1.Anchor = System.Windows.Forms.AnchorStyles.Right;
+ this.numericUpDown1.DecimalPlaces = 1;
+ this.numericUpDown1.Increment = new decimal(new int[] {
+ 1,
+ 0,
+ 0,
+ 65536});
+ this.numericUpDown1.Location = new System.Drawing.Point(208, 0);
+ this.numericUpDown1.Maximum = new decimal(new int[] {
+ 999,
+ 0,
+ 0,
+ 0});
+ this.numericUpDown1.Name = "numericUpDown1";
+ this.numericUpDown1.Size = new System.Drawing.Size(84, 23);
+ this.numericUpDown1.Suffix = " MB/s";
+ this.numericUpDown1.TabIndex = 4;
+ this.numericUpDown1.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.numericUpDown1.ThousandsSeparator = true;
+ this.numericUpDown1.Value = new decimal(new int[] {
+ 999,
+ 0,
+ 0,
+ 0});
+ this.numericUpDown1.ValueChanged += new System.EventHandler(this.numericUpDown1_ValueChanged);
+ //
// btnCleanFinished
//
this.btnCleanFinished.Dock = System.Windows.Forms.DockStyle.Right;
@@ -305,6 +348,8 @@
this.tabControl1.ResumeLayout(false);
this.tabPage1.ResumeLayout(false);
this.panel1.ResumeLayout(false);
+ this.panel1.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
this.tabPage2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.logDGV)).EndInit();
this.panel2.ResumeLayout(false);
@@ -337,5 +382,7 @@
private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;
private System.Windows.Forms.Button logCopyBtn;
+ private NumericUpDownSuffix numericUpDown1;
+ private System.Windows.Forms.Label label1;
}
}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
index baf7a8ea..53e8f819 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
+using LibationFileManager;
namespace LibationWinForms.ProcessQueue
{
@@ -46,6 +48,9 @@ namespace LibationWinForms.ProcessQueue
{
InitializeComponent();
+ var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
+ numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
+
popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
popoutBtn.Name = "popoutBtn";
popoutBtn.Text = "Pop Out";
@@ -424,5 +429,57 @@ This error appears to be caused by a temporary interruption of service that some
}
#endregion
+
+ private void numericUpDown1_ValueChanged(object sender, EventArgs e)
+ {
+ var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
+
+ var config = Configuration.Instance;
+ config.DownloadSpeedLimit = newValue;
+ if (config.DownloadSpeedLimit > newValue)
+ numericUpDown1.Value =
+ numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
+ : 0;
+
+ numericUpDown1.Increment =
+ numericUpDown1.Value > 100 ? 10
+ : numericUpDown1.Value > 10 ? 1
+ : numericUpDown1.Value > 1 ? 0.1m
+ : 0.01m;
+
+ numericUpDown1.DecimalPlaces =
+ numericUpDown1.Value >= 10 ? 0
+ : numericUpDown1.Value >= 1 ? 1
+ : 2;
+ }
+ }
+ public class NumericUpDownSuffix : NumericUpDown
+ {
+ [Description("Suffix displayed after numeric value."), Category("Data")]
+ [Browsable(true)]
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ [DisallowNull]
+ public string Suffix
+ {
+ get => _suffix;
+ set
+ {
+ base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value);
+ _suffix = value;
+ ChangingText = true;
+ }
+ }
+ private string _suffix = string.Empty;
+ public override string Text
+ {
+ get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
+ set
+ {
+ if (Value == Minimum)
+ base.Text = "∞";
+ else
+ base.Text = value + Suffix;
+ }
+ }
}
}