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; + } + } } }