// -------------------------------------------------------------------------------------------------------------------- // // This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License. // // // The HandBrake Queue // // -------------------------------------------------------------------------------------------------------------------- namespace HandBrakeWPF.Services.Queue { using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Xml.Serialization; using HandBrake.ApplicationServices.Model; using HandBrakeWPF.Factories; using HandBrakeWPF.Services.Encode.Factories; using HandBrakeWPF.Services.Encode.Model; using HandBrakeWPF.Services.Interfaces; using HandBrakeWPF.Services.Queue.Model; using HandBrakeWPF.Utilities; using EncodeCompletedEventArgs = HandBrakeWPF.Services.Encode.EventArgs.EncodeCompletedEventArgs; using Execute = Caliburn.Micro.Execute; using GeneralApplicationException = HandBrakeWPF.Exceptions.GeneralApplicationException; using IEncode = HandBrakeWPF.Services.Encode.Interfaces.IEncode; using QueueCompletedEventArgs = HandBrakeWPF.EventArgs.QueueCompletedEventArgs; using QueueProgressEventArgs = HandBrakeWPF.EventArgs.QueueProgressEventArgs; /// /// The HandBrake Queue /// public class QueueProcessor : Interfaces.IQueueProcessor { #region Constants and Fields /// /// A Lock object to maintain thread safety /// private static readonly object QueueLock = new object(); private readonly IUserSettingService userSettingService; private readonly IErrorService errorService; private readonly BindingList queue = new BindingList(); private readonly string queueFile; private bool clearCompleted; #endregion #region Constructors and Destructors /// /// Initializes a new instance of the class. /// /// /// The encode Service. /// /// /// The user settings service. /// /// /// The Error Service. /// /// /// Services are not setup /// public QueueProcessor(IEncode encodeService, IUserSettingService userSettingService, IErrorService errorService) { this.userSettingService = userSettingService; this.errorService = errorService; this.EncodeService = encodeService; // If this is the first instance, just use the main queue file, otherwise add the instance id to the filename. this.queueFile = string.Format("hb_queue_recovery{0}.xml", GeneralUtilities.ProcessId); } #endregion #region Delegates /// /// Queue Progess Status /// /// /// The sender. /// /// /// The QueueProgressEventArgs. /// public delegate void QueueProgressStatus(object sender, QueueProgressEventArgs e); /// /// The queue completed. /// /// /// The sender. /// /// /// The e. /// public delegate void QueueCompletedEventDelegate(object sender, QueueCompletedEventArgs e); #endregion #region Events /// /// Fires when the Queue has started /// public event QueueProgressStatus JobProcessingStarted; /// /// Fires when a job is Added, Removed or Re-Ordered. /// Should be used for triggering an update of the Queue Window. /// public event EventHandler QueueChanged; /// /// Fires when the entire encode queue has completed. /// public event QueueCompletedEventDelegate QueueCompleted; /// /// Fires when a pause to the encode queue has been requested. /// public event EventHandler QueuePaused; /// /// The low diskspace detected. /// public event EventHandler LowDiskspaceDetected; #endregion #region Properties /// /// Gets the number of jobs in the queue; /// public int Count { get { return this.queue.Count(item => item.Status == QueueItemStatus.Waiting); } } /// /// The number of errors detected. /// public int ErrorCount { get { return this.queue.Count(item => item.Status == QueueItemStatus.Error); } } /// /// Gets the IEncodeService instance. /// public IEncode EncodeService { get; private set; } /// /// Gets a value indicating whether IsProcessing. /// public bool IsProcessing { get; private set; } /// /// Gets or sets Last Processed Job. /// This is set when the job is poped of the queue by GetNextJobForProcessing(); /// public QueueTask LastProcessedJob { get; set; } /// /// Gets The current queue. /// public BindingList Queue { get { return this.queue; } } #endregion #region Public Methods /// /// Add a job to the Queue. /// This method is Thread Safe. /// /// /// The encode Job object. /// public void Add(QueueTask job) { lock (QueueLock) { this.queue.Add(job); this.InvokeQueueChanged(EventArgs.Empty); } } /// /// Backup any changes to the queue file /// /// /// If this is not null or empty, this will be used instead of the standard backup location. /// public void BackupQueue(string exportPath) { string appDataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"HandBrake\"); string tempPath = !string.IsNullOrEmpty(exportPath) ? exportPath : appDataPath + string.Format(this.queueFile, string.Empty); using (var strm = new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { List tasks = this.queue.Where(item => item.Status != QueueItemStatus.Completed).ToList(); var serializer = new XmlSerializer(typeof(List)); serializer.Serialize(strm, tasks); strm.Close(); strm.Dispose(); } } /// /// Export the Queue the standardised JSON format. /// /// /// The export Path. /// public void ExportJson(string exportPath) { List jobs = this.queue.Where(item => item.Status != QueueItemStatus.Completed).ToList(); List workUnits = jobs.Select(job => job.Task).ToList(); HBConfiguration config = HBConfigurationFactory.Create(); // Default to current settings for now. These will hopefully go away in the future. string json = QueueFactory.GetQueueJson(workUnits, config); using (var strm = new StreamWriter(exportPath, false)) { strm.Write(json); strm.Close(); strm.Dispose(); } } /// /// Checks the current queue for an existing instance of the specified destination. /// /// /// The destination of the encode. /// /// /// Whether or not the supplied destination is already in the queue. /// public bool CheckForDestinationPathDuplicates(string destination) { foreach (QueueTask job in this.queue) { if (string.Equals( job.Task.Destination, destination.Replace("\\\\", "\\"), StringComparison.OrdinalIgnoreCase) && job.Status == QueueItemStatus.Waiting) { return true; } } return false; } /// /// Clear down all Queue Items /// public void Clear() { List deleteList = this.queue.ToList(); foreach (QueueTask item in deleteList) { this.queue.Remove(item); } this.InvokeQueueChanged(EventArgs.Empty); } /// /// Clear down the Queue´s completed items /// public void ClearCompleted() { Execute.OnUIThread( () => { List deleteList = this.queue.Where(task => task.Status == QueueItemStatus.Completed).ToList(); foreach (QueueTask item in deleteList) { this.queue.Remove(item); } this.InvokeQueueChanged(EventArgs.Empty); }); } /// /// Get the first job on the queue for processing. /// This also removes the job from the Queue and sets the LastProcessedJob /// /// /// An encode Job object. /// public QueueTask GetNextJobForProcessing() { if (this.queue.Count > 0) { QueueTask job = this.queue.FirstOrDefault(q => q.Status == QueueItemStatus.Waiting); if (job != null) { job.Status = QueueItemStatus.InProgress; this.LastProcessedJob = job; this.InvokeQueueChanged(EventArgs.Empty); } this.BackupQueue(string.Empty); return job; } this.BackupQueue(string.Empty); return null; } /// /// Moves an item down one position in the queue. /// /// /// The zero-based location of the job in the queue. /// public void MoveDown(int index) { if (index < this.queue.Count - 1) { QueueTask item = this.queue[index]; this.queue.RemoveAt(index); this.queue.Insert((index + 1), item); } this.InvokeQueueChanged(EventArgs.Empty); } /// /// Moves an item up one position in the queue. /// /// /// The zero-based location of the job in the queue. /// public void MoveUp(int index) { if (index > 0) { QueueTask item = this.queue[index]; this.queue.RemoveAt(index); this.queue.Insert((index - 1), item); } this.InvokeQueueChanged(EventArgs.Empty); } /// /// Remove a job from the Queue. /// This method is Thread Safe /// /// /// The job. /// public void Remove(QueueTask job) { lock (QueueLock) { this.queue.Remove(job); this.InvokeQueueChanged(EventArgs.Empty); } } /// /// Reset a Queued Item from Error or Completed to Waiting /// /// /// The job. /// public void ResetJobStatusToWaiting(QueueTask job) { if (job.Status != QueueItemStatus.Error && job.Status != QueueItemStatus.Completed) { throw new GeneralApplicationException( "Job Error", "Unable to reset job status as it is not in an Error or Completed state", null); } job.Status = QueueItemStatus.Waiting; } /// /// Restore a Queue from file or from the queue backup file. /// /// /// The import path. String.Empty or null will result in the default file being loaded. /// public void RestoreQueue(string importPath) { string appDataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"HandBrake\"); string tempPath = !string.IsNullOrEmpty(importPath) ? importPath : (appDataPath + string.Format(this.queueFile, string.Empty)); if (File.Exists(tempPath)) { bool invokeUpdate = false; using ( var strm = new FileStream( (!string.IsNullOrEmpty(importPath) ? importPath : tempPath), FileMode.Open, FileAccess.Read)) { if (strm.Length != 0) { var serializer = new XmlSerializer(typeof(List)); List list; try { list = serializer.Deserialize(strm) as List; } catch (Exception exc) { throw new GeneralApplicationException( "Unable to restore queue file.", "The file may be corrupted or from an older incompatible version of HandBrake", exc); } if (list != null) { foreach (QueueTask item in list) { if (item.Status != QueueItemStatus.Completed) { // Reset InProgress/Error to Waiting so it can be processed if (item.Status == QueueItemStatus.InProgress) { item.Status = QueueItemStatus.Error; } this.queue.Add(item); } } } invokeUpdate = true; } } if (invokeUpdate) { this.InvokeQueueChanged(EventArgs.Empty); } } } /// /// Requests a pause of the encode queue. /// public void Pause() { this.InvokeQueuePaused(EventArgs.Empty); this.IsProcessing = false; } /// /// Starts encoding the first job in the queue and continues encoding until all jobs /// have been encoded. /// /// /// The is Clear Completed. /// public void Start(bool isClearCompleted) { if (this.IsProcessing) { return; } this.clearCompleted = isClearCompleted; this.EncodeService.EncodeCompleted -= this.EncodeServiceEncodeCompleted; this.EncodeService.EncodeCompleted += this.EncodeServiceEncodeCompleted; if (this.EncodeService.IsEncoding) { this.EncodeService.Resume(); } if (!this.EncodeService.IsEncoding) { this.ProcessNextJob(); } this.IsProcessing = true; } #endregion #region Methods /// /// After an encode is complete, move onto the next job. /// /// /// The sender. /// /// /// The EncodeCompletedEventArgs. /// private void EncodeServiceEncodeCompleted(object sender, EncodeCompletedEventArgs e) { this.LastProcessedJob.Status = QueueItemStatus.Completed; // Clear the completed item of the queue if the setting is set. if (this.clearCompleted) { this.ClearCompleted(); } if (!e.Successful) { this.LastProcessedJob.Status = QueueItemStatus.Error; } // Move onto the next job. if (this.IsProcessing) { this.ProcessNextJob(); } else { this.EncodeService.EncodeCompleted -= this.EncodeServiceEncodeCompleted; this.OnQueueCompleted(new QueueCompletedEventArgs(true)); this.BackupQueue(string.Empty); } } /// /// Invoke the JobProcessingStarted event /// /// /// The QueueProgressEventArgs. /// private void InvokeJobProcessingStarted(QueueProgressEventArgs e) { QueueProgressStatus handler = this.JobProcessingStarted; if (handler != null) { handler(this, e); } } /// /// Invoke the Queue Changed Event /// /// /// The e. /// private void InvokeQueueChanged(EventArgs e) { try { this.BackupQueue(string.Empty); } catch (Exception) { // Do Nothing. } EventHandler handler = this.QueueChanged; if (handler != null) { handler(this, e); } } /// /// Invoke the QueuePaused event /// /// /// The EventArgs. /// private void InvokeQueuePaused(EventArgs e) { this.IsProcessing = false; EventHandler handler = this.QueuePaused; if (handler != null) { handler(this, e); } } /// /// The on queue completed. /// /// /// The e. /// protected virtual void OnQueueCompleted(QueueCompletedEventArgs e) { QueueCompletedEventDelegate handler = this.QueueCompleted; if (handler != null) { handler(this, e); } this.IsProcessing = false; } /// /// The on low diskspace detected. /// protected virtual void OnLowDiskspaceDetected() { this.LowDiskspaceDetected?.Invoke(this, EventArgs.Empty); } /// /// Run through all the jobs on the queue. /// private void ProcessNextJob() { QueueTask job = this.GetNextJobForProcessing(); if (job != null) { if (this.userSettingService.GetUserSetting(UserSettingConstants.PauseOnLowDiskspace)) { string drive = Path.GetPathRoot(job.Task.Destination); if (!string.IsNullOrEmpty(drive) && !drive.StartsWith("\\")) { DriveInfo c = new DriveInfo(drive); if (c.AvailableFreeSpace < this.userSettingService.GetUserSetting(UserSettingConstants.PauseOnLowDiskspaceLevel)) { job.Status = QueueItemStatus.Waiting; this.InvokeQueueChanged(EventArgs.Empty); this.OnLowDiskspaceDetected(); return; // Don't start the next job. } } } this.InvokeJobProcessingStarted(new QueueProgressEventArgs(job)); this.EncodeService.Start(job.Task, job.Configuration); } else { // No more jobs to process, so unsubscribe the event this.EncodeService.EncodeCompleted -= this.EncodeServiceEncodeCompleted; // Fire the event to tell connected services. this.OnQueueCompleted(new QueueCompletedEventArgs(false)); } } #endregion } }