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 AppScaffolding; using LibationFileManager; namespace LibationWinForms.ProcessQueue { internal partial class ProcessQueueControl : UserControl, ILogForm { private TrackedQueue Queue = new(); private readonly LogMe Logger; private int QueuedCount { set { queueNumberLbl.Text = value.ToString(); queueNumberLbl.Visible = value > 0; } } private int ErrorCount { set { errorNumberLbl.Text = value.ToString(); errorNumberLbl.Visible = value > 0; } } private int CompletedCount { set { completedNumberLbl.Text = value.ToString(); completedNumberLbl.Visible = value > 0; } } public Task QueueRunner { get; private set; } public bool Running => !QueueRunner?.IsCompleted ?? false; public ToolStripButton popoutBtn = new(); public ProcessQueueControl() { 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"; popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; popoutBtn.Alignment = ToolStripItemAlignment.Right; popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; statusStrip1.Items.Add(popoutBtn); Logger = LogMe.RegisterForm(this); virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData; virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; Queue.QueuededCountChanged += Queue_QueuededCountChanged; Queue.CompletedCountChanged += Queue_CompletedCountChanged; Load += ProcessQueueControl_Load; } private void ProcessQueueControl_Load(object sender, EventArgs e) { if (DesignMode) return; runningTimeLbl.Text = string.Empty; QueuedCount = 0; ErrorCount = 0; CompletedCount = 0; } private bool isBookInQueue(DataLayer.LibraryBook libraryBook) => Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); public void AddDownloadPdf(DataLayer.LibraryBook libraryBook) => AddDownloadPdf(new List() { libraryBook }); public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook) => AddDownloadDecrypt(new List() { libraryBook }); public void AddConvertMp3(DataLayer.LibraryBook libraryBook) => AddConvertMp3(new List() { libraryBook }); public void AddDownloadPdf(IEnumerable entries) { List procs = new(); foreach (var entry in entries) { if (isBookInQueue(entry)) continue; ProcessBook pbook = new(entry, Logger); pbook.PropertyChanged += Pbook_DataAvailable; pbook.AddDownloadPdf(); procs.Add(pbook); } Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } public void AddDownloadDecrypt(IEnumerable entries) { List procs = new(); foreach (var entry in entries) { if (isBookInQueue(entry)) continue; ProcessBook pbook = new(entry, Logger); pbook.PropertyChanged += Pbook_DataAvailable; pbook.AddDownloadDecryptBook(); pbook.AddDownloadPdf(); procs.Add(pbook); } Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } public void AddConvertMp3(IEnumerable entries) { List procs = new(); foreach (var entry in entries) { if (isBookInQueue(entry)) continue; ProcessBook pbook = new(entry, Logger); pbook.PropertyChanged += Pbook_DataAvailable; pbook.AddConvertToMp3(); procs.Add(pbook); } Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); AddToQueue(procs); } private void AddToQueue(IEnumerable pbook) { BeginInvoke(() => { Queue.Enqueue(pbook); if (!Running) QueueRunner = QueueLoop(); }); } DateTime StartingTime; private async Task QueueLoop() { try { Serilog.Log.Logger.Information("Begin processing queue"); StartingTime = DateTime.Now; counterTimer.Start(); bool shownServiceOutageMessage = false; while (Queue.MoveNext()) { var nextBook = Queue.Current; Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook); var result = await nextBook.ProcessOneAsync(); Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result); if (result == ProcessBookResult.ValidationFail) Queue.ClearCurrent(); else if (result == ProcessBookResult.FailedAbort) Queue.ClearQueue(); else if (result == ProcessBookResult.FailedSkip) nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error); else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) { MessageBox.Show(@$" You were denied a content license for {nextBook.LibraryBook.Book.Title} This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app. ", "Possible Interruption of Service", MessageBoxButtons.OK, MessageBoxIcon.Asterisk); shownServiceOutageMessage = true; } } Serilog.Log.Logger.Information("Completed processing queue"); Queue_CompletedCountChanged(this, 0); counterTimer.Stop(); virtualFlowControl2.VirtualControlCount = Queue.Count; UpdateAllControls(); } catch (Exception ex) { Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items"); } } public void WriteLine(string text) { if (IsDisposed) return; var timeStamp = DateTime.Now; Invoke(() => logDGV.Rows.Add(timeStamp, text.Trim())); } #region Control event handlers 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); int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success); ErrorCount = errCount; CompletedCount = completeCount; UpdateProgressBar(); } private void Queue_QueuededCountChanged(object sender, int cueCount) { QueuedCount = cueCount; virtualFlowControl2.VirtualControlCount = Queue.Count; UpdateProgressBar(); } private void UpdateProgressBar() { toolStripProgressBar1.Maximum = Queue.Count; toolStripProgressBar1.Value = Queue.Completed.Count; } private async void cancelAllBtn_Click(object sender, EventArgs e) { Queue.ClearQueue(); if (Queue.Current is not null) await Queue.Current.CancelAsync(); virtualFlowControl2.VirtualControlCount = Queue.Count; UpdateAllControls(); } private void btnClearFinished_Click(object sender, EventArgs e) { Queue.ClearCompleted(); virtualFlowControl2.VirtualControlCount = Queue.Count; UpdateAllControls(); if (!Running) runningTimeLbl.Text = string.Empty; } private void CounterTimer_Tick(object sender, EventArgs e) { string timeToStr(TimeSpan time) { string minsSecs = $"{time:mm\\:ss}"; if (time.TotalHours >= 1) return $"{time.TotalHours:F0}:{minsSecs}"; return minsSecs; } if (Running) runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime); } private void clearLogBtn_Click(object sender, EventArgs e) { logDGV.Rows.Clear(); } private void LogCopyBtn_Click(object sender, EventArgs e) { string logText = string.Join("\r\n", logDGV.Rows.Cast().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}")); Clipboard.SetDataObject(logText, false, 5, 150); } private void LogDGV_Resize(object sender, EventArgs e) { logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width; } #endregion #region View-Model update event handling /// /// Index of the first visible in the /// private int FirstVisible = 0; /// /// Number of visible in the /// private int NumVisible = 0; /// /// Controls displaying the state, starting with /// private IReadOnlyList Panels; /// /// Updates the display of a single at within /// /// index of the within the /// The nme of the property that needs updating. If null, all properties are updated. private void UpdateControl(int queueIndex, string propertyName = null) { try { int i = queueIndex - FirstVisible; if (i > NumVisible || i < 0) return; var proc = Queue[queueIndex]; Invoke(() => { Panels[i].SuspendLayout(); if (propertyName is null or nameof(proc.Cover)) Panels[i].SetCover(proc.Cover); if (propertyName is null or nameof(proc.BookText)) Panels[i].SetBookInfo(proc.BookText); if (proc.Result != ProcessBookResult.None) { Panels[i].SetResult(proc.Result); return; } if (propertyName is null or nameof(proc.Status)) Panels[i].SetStatus(proc.Status); if (propertyName is null or nameof(proc.Progress)) Panels[i].SetProgrss(proc.Progress); if (propertyName is null or nameof(proc.TimeRemaining)) Panels[i].SetRemainingTime(proc.TimeRemaining); Panels[i].ResumeLayout(); }); } catch (Exception ex) { Serilog.Log.Logger.Error(ex, "Error updating the queued item's display."); } } private void UpdateAllControls() { int numToShow = Math.Min(NumVisible, Queue.Count - FirstVisible); for (int i = 0; i < numToShow; i++) UpdateControl(FirstVisible + i); } /// /// View notified the model that a botton was clicked /// /// index of the within /// The clicked control to update private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked) { try { ProcessBook item = Queue[queueIndex]; if (buttonName == nameof(panelClicked.cancelBtn)) { if (item is not null) await item.CancelAsync(); Queue.RemoveQueued(item); virtualFlowControl2.VirtualControlCount = Queue.Count; } else if (buttonName == nameof(panelClicked.moveFirstBtn)) { Queue.MoveQueuePosition(item, QueuePosition.Fisrt); UpdateAllControls(); } else if (buttonName == nameof(panelClicked.moveUpBtn)) { Queue.MoveQueuePosition(item, QueuePosition.OneUp); UpdateControl(queueIndex); if (queueIndex > 0) UpdateControl(queueIndex - 1); } else if (buttonName == nameof(panelClicked.moveDownBtn)) { Queue.MoveQueuePosition(item, QueuePosition.OneDown); UpdateControl(queueIndex); if (queueIndex + 1 < Queue.Count) UpdateControl(queueIndex + 1); } else if (buttonName == nameof(panelClicked.moveLastBtn)) { Queue.MoveQueuePosition(item, QueuePosition.Last); UpdateAllControls(); } } catch(Exception ex) { Serilog.Log.Logger.Error(ex, "Error handling button click from queued item"); } } /// /// View needs updating /// private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList panelsToFill) { FirstVisible = firstIndex; NumVisible = numVisible; Panels = panelsToFill; UpdateAllControls(); } /// /// Model updates the view /// private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e) { int index = Queue.IndexOf((ProcessBook)sender); UpdateControl(index, e.PropertyName); } #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; } } } }