// --------------------------------------------------------------------------------------------------------------------
//
// 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 HandBrake.ApplicationServices.Services
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using Caliburn.Micro;
using HandBrake.ApplicationServices.EventArgs;
using HandBrake.ApplicationServices.Exceptions;
using HandBrake.ApplicationServices.Model;
using HandBrake.ApplicationServices.Services.Encode.EventArgs;
using HandBrake.ApplicationServices.Services.Encode.Interfaces;
using HandBrake.ApplicationServices.Services.Interfaces;
using HandBrake.ApplicationServices.Utilities;
///
/// The HandBrake Queue
///
public class QueueProcessor : IQueueProcessor
{
#region Constants and Fields
///
/// A Lock object to maintain thread safety
///
private static readonly object QueueLock = new object();
///
/// The Queue of Job objects
///
private readonly BindingList queue = new BindingList();
///
/// HandBrakes Queue file with a place holder for an extra string.
///
private readonly string queueFile;
///
/// The clear completed.
///
private bool clearCompleted;
#endregion
#region Constructors and Destructors
///
/// Initializes a new instance of the class.
///
///
/// The encode Service.
///
///
/// Services are not setup
///
public QueueProcessor(IEncodeServiceWrapper encodeService)
{
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;
#endregion
#region Properties
///
/// Gets the number of jobs in the queue;
///
public int Count
{
get
{
return this.queue.Count(item => item.Status == QueueItemStatus.Waiting);
}
}
///
/// Gets the IEncodeService instance.
///
public IEncodeServiceWrapper 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();
}
}
///
/// 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)
{
return this.queue.Any(job => job.Task != null && job.Status == QueueItemStatus.Waiting && job.Task.Destination != null && job.Task.Destination.Contains(destination.Replace("\\\\", "\\")));
}
///
/// 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.Waiting;
}
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)
{
throw new Exception("Already Processing the Queue");
}
clearCompleted = isClearCompleted;
this.EncodeService.EncodeCompleted -= this.EncodeServiceEncodeCompleted;
this.EncodeService.EncodeCompleted += this.EncodeServiceEncodeCompleted;
if (this.EncodeService.CanPause && 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 (clearCompleted)
{
this.ClearCompleted();
}
if (!e.Successful)
{
this.LastProcessedJob.Status = QueueItemStatus.Error;
this.Pause();
}
// Handling Log Data
this.EncodeService.ProcessLogs(this.LastProcessedJob.Task.Destination, this.LastProcessedJob.Configuration);
// 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;
}
///
/// Run through all the jobs on the queue.
///
private void ProcessNextJob()
{
QueueTask job = this.GetNextJobForProcessing();
if (job != null)
{
this.InvokeJobProcessingStarted(new QueueProgressEventArgs(job));
this.EncodeService.Start(job);
}
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
}
}