// -------------------------------------------------------------------------------------------------------------------- // // 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.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Windows.Media.Imaging; using HandBrake.Interop.Interop.Json.Queue; using HandBrake.Interop.Model; using HandBrake.Interop.Utilities; using HandBrakeWPF.Factories; using HandBrakeWPF.Helpers; using HandBrakeWPF.Properties; using HandBrakeWPF.Services.Encode.Factories; using HandBrakeWPF.Services.Encode.Model; using HandBrakeWPF.Services.Interfaces; using HandBrakeWPF.Services.Queue.Model; using HandBrakeWPF.Utilities; using Newtonsoft.Json; 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 LogLevel = HandBrakeWPF.Services.Logging.Model.LogLevel; using LogMessageType = HandBrakeWPF.Services.Logging.Model.LogMessageType; using LogService = HandBrakeWPF.Services.Logging.LogService; using QueueCompletedEventArgs = HandBrakeWPF.EventArgs.QueueCompletedEventArgs; using QueueProgressEventArgs = HandBrakeWPF.EventArgs.QueueProgressEventArgs; public class QueueService : Interfaces.IQueueService { private static readonly object QueueLock = new object(); private readonly IUserSettingService userSettingService; private readonly ObservableCollection queue = new ObservableCollection(); private readonly string queueFile; private bool clearCompleted; public QueueService(IEncode encodeService, IUserSettingService userSettingService) { this.userSettingService = userSettingService; 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("{0}{1}.json", QueueRecoveryHelper.QueueFileName, GeneralUtilities.ProcessId); } public delegate void QueueProgressStatus(object sender, QueueProgressEventArgs e); public delegate void QueueCompletedEventDelegate(object sender, QueueCompletedEventArgs e); public event QueueProgressStatus JobProcessingStarted; public event EventHandler QueueChanged; public event QueueCompletedEventDelegate QueueCompleted; public event EventHandler QueuePaused; public int Count { get { return this.queue.Count(item => item.Status == QueueItemStatus.Waiting); } } public int ErrorCount { get { return this.queue.Count(item => item.Status == QueueItemStatus.Error); } } public IEncode EncodeService { get; private set; } public bool IsProcessing { get; private set; } public QueueTask LastProcessedJob { get; set; } public ObservableCollection Queue { get { return this.queue; } } public void Add(QueueTask job) { lock (QueueLock) { this.queue.Add(job); this.InvokeQueueChanged(EventArgs.Empty); } } public void BackupQueue(string exportPath) { Stopwatch watch = Stopwatch.StartNew(); string appDataPath = DirectoryUtilities.GetUserStoragePath(VersionHelper.IsNightly()); string tempPath = !string.IsNullOrEmpty(exportPath) ? exportPath : Path.Combine(appDataPath, string.Format(this.queueFile, string.Empty)); // Make a copy of the file before we replace it. This way, if we crash we can recover. if (File.Exists(tempPath)) { File.Copy(tempPath, tempPath + ".last"); } using (StreamWriter writer = new StreamWriter(tempPath)) { List tasks = this.queue.Where(item => item.Status != QueueItemStatus.Completed).ToList(); string queueJson = JsonConvert.SerializeObject(tasks, Formatting.Indented); writer.Write(queueJson); } if (File.Exists(tempPath + ".last")) { File.Delete(tempPath + ".last"); } watch.Stop(); Debug.WriteLine("Queue Save (ms): " + watch.ElapsedMilliseconds); } public void ExportCliJson(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(); } } public void ExportJson(string exportPath) { List jobs = this.queue.Where(item => item.Status != QueueItemStatus.Completed).ToList(); JsonSerializerSettings settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; string json = JsonConvert.SerializeObject(jobs, Formatting.Indented, settings); using (var strm = new StreamWriter(exportPath, false)) { strm.Write(json); strm.Close(); strm.Dispose(); } } public void ImportJson(string path) { List tasks; using (StreamReader reader = new StreamReader(path)) { string fileContent = reader.ReadToEnd(); if (string.IsNullOrEmpty(fileContent)) { return; } List reloadedQueue = JsonConvert.DeserializeObject>(fileContent); if (reloadedQueue == null) { return; } foreach (QueueTask task in reloadedQueue) { this.queue.Add(task); } if (reloadedQueue.Count > 0) { this.InvokeQueueChanged(EventArgs.Empty); } } } 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 || job.Status == QueueItemStatus.InProgress)) { return true; } } return false; } public void Clear() { List deleteList = this.queue.ToList(); foreach (QueueTask item in deleteList) { this.queue.Remove(item); } this.InvokeQueueChanged(EventArgs.Empty); } 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); }); } public QueueTask GetNextJobForProcessing() { if (this.queue.Count > 0) { return this.queue.FirstOrDefault(q => q.Status == QueueItemStatus.Waiting); } return null; } 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); } 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); } public void Remove(QueueTask job) { lock (QueueLock) { this.queue.Remove(job); this.InvokeQueueChanged(EventArgs.Empty); } } public void ResetJobStatusToWaiting(QueueTask job) { if (job.Status != QueueItemStatus.Error && job.Status != QueueItemStatus.Completed) { throw new GeneralApplicationException( Resources.Error, Resources.Queue_UnableToResetJob, null); } job.Status = QueueItemStatus.Waiting; } public void RestoreQueue(string importPath) { string appDataPath = DirectoryUtilities.GetUserStoragePath(VersionHelper.IsNightly()); string tempPath = !string.IsNullOrEmpty(importPath) ? importPath : (appDataPath + string.Format(this.queueFile, string.Empty)); if (File.Exists(tempPath)) { bool invokeUpdate = false; using (StreamReader stream = new StreamReader(!string.IsNullOrEmpty(importPath) ? importPath : tempPath)) { string queueJson = stream.ReadToEnd(); List list; try { list = JsonConvert.DeserializeObject>(queueJson); } catch (Exception exc) { throw new GeneralApplicationException(Resources.Queue_UnableToRestoreFile, Resources.Queue_UnableToRestoreFileExtended, 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); } } } public void Pause() { this.IsProcessing = false; this.InvokeQueuePaused(EventArgs.Empty); } public void PauseEncode() { if (this.EncodeService.IsEncoding && !this.EncodeService.IsPasued) { this.EncodeService.Pause(); this.LastProcessedJob.Statistics.SetPaused(true); } this.Pause(); } 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.IsPasued) { this.EncodeService.Resume(); this.IsProcessing = true; this.InvokeJobProcessingStarted(new QueueProgressEventArgs(this.LastProcessedJob)); this.LastProcessedJob.Statistics.SetPaused(false); } if (!this.EncodeService.IsEncoding) { this.ProcessNextJob(); } } public void Stop() { if (this.EncodeService.IsEncoding) { this.EncodeService.Stop(); } this.IsProcessing = false; this.InvokeQueuePaused(EventArgs.Empty); } protected virtual void OnQueueCompleted(QueueCompletedEventArgs e) { QueueCompletedEventDelegate handler = this.QueueCompleted; if (handler != null) { handler(this, e); } this.IsProcessing = false; } private void EncodeServiceEncodeCompleted(object sender, EncodeCompletedEventArgs e) { this.LastProcessedJob.Status = QueueItemStatus.Completed; this.LastProcessedJob.Statistics.EndTime = DateTime.Now; this.LastProcessedJob.Statistics.CompletedActivityLogPath = e.ActivityLogPath; this.LastProcessedJob.Statistics.FinalFileSize = e.FinalFilesizeInBytes; // 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.BackupQueue(string.Empty); this.OnQueueCompleted(new QueueCompletedEventArgs(true)); } } private void InvokeJobProcessingStarted(QueueProgressEventArgs e) { QueueProgressStatus handler = this.JobProcessingStarted; if (handler != null) { handler(this, 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); } } private void InvokeQueuePaused(EventArgs e) { this.IsProcessing = false; EventHandler handler = this.QueuePaused; if (handler != null) { handler(this, e); } } private void ProcessNextJob() { QueueTask job = this.GetNextJobForProcessing(); if (job != null) { if (this.userSettingService.GetUserSetting(UserSettingConstants.PauseOnLowDiskspace) && !DriveUtilities.HasMinimumDiskSpace(job.Task.Destination, this.userSettingService.GetUserSetting(UserSettingConstants.PauseQueueOnLowDiskspaceLevel))) { LogService.GetLogger().LogMessage(Resources.PauseOnLowDiskspace, LogMessageType.ScanOrEncode, LogLevel.Info); job.Status = QueueItemStatus.Waiting; this.Pause(); this.BackupQueue(string.Empty); return; // Don't start the next job. } job.Status = QueueItemStatus.InProgress; job.Statistics.StartTime = DateTime.Now; this.LastProcessedJob = job; this.IsProcessing = true; this.InvokeQueueChanged(EventArgs.Empty); this.InvokeJobProcessingStarted(new QueueProgressEventArgs(job)); if (!Directory.Exists(Path.GetDirectoryName(job.Task.Destination))) { this.EncodeServiceEncodeCompleted(null, new EncodeCompletedEventArgs(false, null, "Destination Directory Missing", null, null, 0)); this.BackupQueue(string.Empty); return; } this.EncodeService.Start(job.Task, job.Configuration, job.SelectedPresetKey); this.BackupQueue(string.Empty); } else { // No more jobs to process, so unsubscribe the event this.EncodeService.EncodeCompleted -= this.EncodeServiceEncodeCompleted; this.BackupQueue(string.Empty); // Fire the event to tell connected services. this.OnQueueCompleted(new QueueCompletedEventArgs(false)); } } } }