aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Common/ScheduledTasks
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Common/ScheduledTasks')
-rw-r--r--MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs503
-rw-r--r--MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs64
-rw-r--r--MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs71
-rw-r--r--MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs86
-rw-r--r--MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs66
-rw-r--r--MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs153
-rw-r--r--MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs50
-rw-r--r--MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs55
-rw-r--r--MediaBrowser.Common/ScheduledTasks/TaskManager.cs108
-rw-r--r--MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs109
-rw-r--r--MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs97
-rw-r--r--MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs61
-rw-r--r--MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs108
-rw-r--r--MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs101
14 files changed, 1632 insertions, 0 deletions
diff --git a/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs b/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs
new file mode 100644
index 0000000000..61ed649e2c
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs
@@ -0,0 +1,503 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task that can be executed at a scheduled time
+ /// </summary>
+ /// <typeparam name="TKernelType">The type of the T kernel type.</typeparam>
+ public abstract class BaseScheduledTask<TKernelType> : IScheduledTask
+ where TKernelType : IKernel
+ {
+ /// <summary>
+ /// Gets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ protected TKernelType Kernel { get; private set; }
+
+ /// <summary>
+ /// The _last execution result
+ /// </summary>
+ private TaskResult _lastExecutionResult;
+ /// <summary>
+ /// The _last execution resultinitialized
+ /// </summary>
+ private bool _lastExecutionResultinitialized;
+ /// <summary>
+ /// The _last execution result sync lock
+ /// </summary>
+ private object _lastExecutionResultSyncLock = new object();
+ /// <summary>
+ /// Gets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ public TaskResult LastExecutionResult
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () =>
+ {
+ try
+ {
+ return JsonSerializer.DeserializeFromFile<TaskResult>(HistoryFilePath);
+ }
+ catch (IOException)
+ {
+ // File doesn't exist. No biggie
+ return null;
+ }
+ });
+
+ return _lastExecutionResult;
+ }
+ private set
+ {
+ _lastExecutionResult = value;
+
+ _lastExecutionResultinitialized = value != null;
+ }
+ }
+
+ /// <summary>
+ /// The _scheduled tasks data directory
+ /// </summary>
+ private string _scheduledTasksDataDirectory;
+ /// <summary>
+ /// Gets the scheduled tasks data directory.
+ /// </summary>
+ /// <value>The scheduled tasks data directory.</value>
+ private string ScheduledTasksDataDirectory
+ {
+ get
+ {
+ if (_scheduledTasksDataDirectory == null)
+ {
+ _scheduledTasksDataDirectory = Path.Combine(Kernel.ApplicationPaths.DataPath, "ScheduledTasks");
+
+ if (!Directory.Exists(_scheduledTasksDataDirectory))
+ {
+ Directory.CreateDirectory(_scheduledTasksDataDirectory);
+ }
+ }
+ return _scheduledTasksDataDirectory;
+ }
+ }
+
+ /// <summary>
+ /// The _scheduled tasks configuration directory
+ /// </summary>
+ private string _scheduledTasksConfigurationDirectory;
+ /// <summary>
+ /// Gets the scheduled tasks configuration directory.
+ /// </summary>
+ /// <value>The scheduled tasks configuration directory.</value>
+ private string ScheduledTasksConfigurationDirectory
+ {
+ get
+ {
+ if (_scheduledTasksConfigurationDirectory == null)
+ {
+ _scheduledTasksConfigurationDirectory = Path.Combine(Kernel.ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+
+ if (!Directory.Exists(_scheduledTasksConfigurationDirectory))
+ {
+ Directory.CreateDirectory(_scheduledTasksConfigurationDirectory);
+ }
+ }
+ return _scheduledTasksConfigurationDirectory;
+ }
+ }
+
+ /// <summary>
+ /// Gets the configuration file path.
+ /// </summary>
+ /// <value>The configuration file path.</value>
+ private string ConfigurationFilePath
+ {
+ get { return Path.Combine(ScheduledTasksConfigurationDirectory, Id + ".js"); }
+ }
+
+ /// <summary>
+ /// Gets the history file path.
+ /// </summary>
+ /// <value>The history file path.</value>
+ private string HistoryFilePath
+ {
+ get { return Path.Combine(ScheduledTasksDataDirectory, Id + ".js"); }
+ }
+
+ /// <summary>
+ /// Gets the current cancellation token
+ /// </summary>
+ /// <value>The current cancellation token source.</value>
+ private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current execution start time.
+ /// </summary>
+ /// <value>The current execution start time.</value>
+ private DateTime CurrentExecutionStartTime { get; set; }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public TaskState State
+ {
+ get
+ {
+ if (CurrentCancellationTokenSource != null)
+ {
+ return CurrentCancellationTokenSource.IsCancellationRequested
+ ? TaskState.Cancelling
+ : TaskState.Running;
+ }
+
+ return TaskState.Idle;
+ }
+ }
+
+ /// <summary>
+ /// Gets the current progress.
+ /// </summary>
+ /// <value>The current progress.</value>
+ public TaskProgress CurrentProgress { get; private set; }
+
+ /// <summary>
+ /// The _triggers
+ /// </summary>
+ private IEnumerable<BaseTaskTrigger> _triggers;
+ /// <summary>
+ /// The _triggers initialized
+ /// </summary>
+ private bool _triggersInitialized;
+ /// <summary>
+ /// The _triggers sync lock
+ /// </summary>
+ private object _triggersSyncLock = new object();
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ /// <exception cref="System.ArgumentNullException">value</exception>
+ public IEnumerable<BaseTaskTrigger> Triggers
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, () =>
+ {
+ try
+ {
+ return JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(ConfigurationFilePath)
+ .Select(t => ScheduledTaskHelpers.GetTrigger(t, Kernel))
+ .ToList();
+ }
+ catch (IOException)
+ {
+ // File doesn't exist. No biggie. Return defaults.
+ return GetDefaultTriggers();
+ }
+ });
+
+ return _triggers;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ // Cleanup current triggers
+ if (_triggers != null)
+ {
+ DisposeTriggers();
+ }
+
+ _triggers = value.ToList();
+
+ _triggersInitialized = true;
+
+ ReloadTriggerEvents();
+
+ JsonSerializer.SerializeToFile(_triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), ConfigurationFilePath);
+ }
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected abstract IEnumerable<BaseTaskTrigger> GetDefaultTriggers();
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected abstract Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress);
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public abstract string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public abstract string Description { get; }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public virtual string Category
+ {
+ get { return "Application"; }
+ }
+
+ /// <summary>
+ /// The _id
+ /// </summary>
+ private Guid? _id;
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ public Guid Id
+ {
+ get
+ {
+ if (!_id.HasValue)
+ {
+ _id = GetType().FullName.GetMD5();
+ }
+
+ return _id.Value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+
+ /// <summary>
+ /// Initializes the specified kernel.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public void Initialize(IKernel kernel)
+ {
+ Logger = LogManager.GetLogger(GetType().Name);
+
+ Kernel = (TKernelType)kernel;
+ ReloadTriggerEvents();
+ }
+
+ /// <summary>
+ /// Reloads the trigger events.
+ /// </summary>
+ private void ReloadTriggerEvents()
+ {
+ foreach (var trigger in Triggers)
+ {
+ trigger.Stop();
+
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Triggered += trigger_Triggered;
+ trigger.Start();
+ }
+ }
+
+ /// <summary>
+ /// Handles the Triggered event of the trigger control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ void trigger_Triggered(object sender, EventArgs e)
+ {
+ var trigger = (BaseTaskTrigger)sender;
+
+ Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
+
+ Kernel.TaskManager.QueueScheduledTask(this);
+ }
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
+ public async Task Execute()
+ {
+ // Cancel the current execution, if any
+ if (CurrentCancellationTokenSource != null)
+ {
+ throw new InvalidOperationException("Cannot execute a Task that is already running");
+ }
+
+ CurrentCancellationTokenSource = new CancellationTokenSource();
+
+ Logger.Info("Executing {0}", Name);
+
+ var progress = new Progress<TaskProgress>();
+
+ progress.ProgressChanged += progress_ProgressChanged;
+
+ TaskCompletionStatus status;
+ CurrentExecutionStartTime = DateTime.UtcNow;
+
+ Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskBeginExecute", Name);
+
+ try
+ {
+ await Task.Run(async () => await ExecuteInternal(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false)).ConfigureAwait(false);
+
+ status = TaskCompletionStatus.Completed;
+ }
+ catch (OperationCanceledException)
+ {
+ status = TaskCompletionStatus.Cancelled;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error", ex);
+
+ status = TaskCompletionStatus.Failed;
+ }
+
+ var endTime = DateTime.UtcNow;
+
+ LogResult(endTime, status);
+
+ Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskEndExecute", LastExecutionResult);
+
+ progress.ProgressChanged -= progress_ProgressChanged;
+ CurrentCancellationTokenSource.Dispose();
+ CurrentCancellationTokenSource = null;
+ CurrentProgress = null;
+
+ Kernel.TaskManager.OnTaskCompleted(this);
+ }
+
+ /// <summary>
+ /// Logs the result.
+ /// </summary>
+ /// <param name="endTime">The end time.</param>
+ /// <param name="status">The status.</param>
+ private void LogResult(DateTime endTime, TaskCompletionStatus status)
+ {
+ var startTime = CurrentExecutionStartTime;
+ var elapsedTime = endTime - startTime;
+
+ Logger.Info("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
+
+ var result = new TaskResult
+ {
+ StartTimeUtc = startTime,
+ EndTimeUtc = endTime,
+ Status = status,
+ Name = Name,
+ Id = Id
+ };
+
+ JsonSerializer.SerializeToFile(result, HistoryFilePath);
+
+ LastExecutionResult = result;
+ }
+
+ /// <summary>
+ /// Progress_s the progress changed.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The e.</param>
+ void progress_ProgressChanged(object sender, TaskProgress e)
+ {
+ CurrentProgress = e;
+ }
+
+ /// <summary>
+ /// Stops the task if it is currently executing
+ /// </summary>
+ /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
+ public void Cancel()
+ {
+ if (State != TaskState.Running)
+ {
+ throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
+ }
+
+ CancelIfRunning();
+ }
+
+ /// <summary>
+ /// Cancels if running.
+ /// </summary>
+ public void CancelIfRunning()
+ {
+ if (State == TaskState.Running)
+ {
+ Logger.Info("Attempting to cancel Scheduled Task {0}", Name);
+ CurrentCancellationTokenSource.Cancel();
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeTriggers();
+
+ if (State == TaskState.Running)
+ {
+ LogResult(DateTime.UtcNow, TaskCompletionStatus.Aborted);
+ }
+
+ if (CurrentCancellationTokenSource != null)
+ {
+ CurrentCancellationTokenSource.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Disposes each trigger
+ /// </summary>
+ private void DisposeTriggers()
+ {
+ foreach (var trigger in Triggers)
+ {
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs b/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs
new file mode 100644
index 0000000000..5e60bb718c
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Use to indicate that a scheduled task should run
+ /// </summary>
+ public abstract class BaseTaskTrigger : IDisposable
+ {
+ /// <summary>
+ /// Fires when the trigger condition is satisfied and the task should run
+ /// </summary>
+ internal event EventHandler<EventArgs> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ protected async void OnTriggered()
+ {
+ Stop();
+
+ if (Triggered != null)
+ {
+ Triggered(this, EventArgs.Empty);
+ }
+
+ await Task.Delay(1000).ConfigureAwait(false);
+
+ Start();
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal abstract void Start();
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal abstract void Stop();
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ Stop();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs b/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs
new file mode 100644
index 0000000000..c1cf1a9a33
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires everyday
+ /// </summary>
+ public class DailyTrigger : BaseTaskTrigger
+ {
+ /// <summary>
+ /// Get the time of day to trigger the task to run
+ /// </summary>
+ /// <value>The time of day.</value>
+ public TimeSpan TimeOfDay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal override void Start()
+ {
+ DisposeTimer();
+
+ var now = DateTime.Now;
+
+ var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(TimeOfDay);
+
+ Timer = new Timer(state => OnTriggered(), null, triggerDate - now, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal override void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes this instance.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeTimer();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
new file mode 100644
index 0000000000..9220116b2f
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Interface IScheduledTask
+ /// </summary>
+ public interface IScheduledTask : IDisposable
+ {
+ /// <summary>
+ /// Gets the triggers.
+ /// </summary>
+ /// <value>The triggers.</value>
+ IEnumerable<BaseTaskTrigger> Triggers { get; set; }
+
+ /// <summary>
+ /// Gets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ TaskResult LastExecutionResult { get; }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ TaskState State { get; }
+
+ /// <summary>
+ /// Gets the current progress.
+ /// </summary>
+ /// <value>The current progress.</value>
+ TaskProgress CurrentProgress { get; }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ string Description { get; }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ string Category { get; }
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ Guid Id { get; }
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
+ Task Execute();
+
+ /// <summary>
+ /// Stops the task if it is currently executing
+ /// </summary>
+ /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
+ void Cancel();
+
+ /// <summary>
+ /// Initializes the specified kernel.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ void Initialize(IKernel kernel);
+
+ /// <summary>
+ /// Cancels if running.
+ /// </summary>
+ void CancelIfRunning();
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs b/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs
new file mode 100644
index 0000000000..1ead484c84
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that runs repeatedly on an interval
+ /// </summary>
+ public class IntervalTrigger : BaseTaskTrigger
+ {
+ /// <summary>
+ /// Gets or sets the interval.
+ /// </summary>
+ /// <value>The interval.</value>
+ public TimeSpan Interval { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal override void Start()
+ {
+ DisposeTimer();
+
+ Timer = new Timer(state => OnTriggered(), null, Interval, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal override void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes this instance.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeTimer();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs b/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs
new file mode 100644
index 0000000000..34421ca1fa
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs
@@ -0,0 +1,153 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Class ScheduledTaskHelpers
+ /// </summary>
+ public static class ScheduledTaskHelpers
+ {
+ /// <summary>
+ /// Gets the task info.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <returns>TaskInfo.</returns>
+ public static TaskInfo GetTaskInfo(IScheduledTask task)
+ {
+ return new TaskInfo
+ {
+ Name = task.Name,
+ CurrentProgress = task.CurrentProgress,
+ State = task.State,
+ Id = task.Id,
+ LastExecutionResult = task.LastExecutionResult,
+ Triggers = task.Triggers.Select(GetTriggerInfo).ToArray(),
+ Description = task.Description,
+ Category = task.Category
+ };
+ }
+
+ /// <summary>
+ /// Gets the trigger info.
+ /// </summary>
+ /// <param name="trigger">The trigger.</param>
+ /// <returns>TaskTriggerInfo.</returns>
+ public static TaskTriggerInfo GetTriggerInfo(BaseTaskTrigger trigger)
+ {
+ var info = new TaskTriggerInfo
+ {
+ Type = trigger.GetType().Name
+ };
+
+ var dailyTrigger = trigger as DailyTrigger;
+
+ if (dailyTrigger != null)
+ {
+ info.TimeOfDayTicks = dailyTrigger.TimeOfDay.Ticks;
+ }
+
+ var weeklyTaskTrigger = trigger as WeeklyTrigger;
+
+ if (weeklyTaskTrigger != null)
+ {
+ info.TimeOfDayTicks = weeklyTaskTrigger.TimeOfDay.Ticks;
+ info.DayOfWeek = weeklyTaskTrigger.DayOfWeek;
+ }
+
+ var intervalTaskTrigger = trigger as IntervalTrigger;
+
+ if (intervalTaskTrigger != null)
+ {
+ info.IntervalTicks = intervalTaskTrigger.Interval.Ticks;
+ }
+
+ var systemEventTrigger = trigger as SystemEventTrigger;
+
+ if (systemEventTrigger != null)
+ {
+ info.SystemEvent = systemEventTrigger.SystemEvent;
+ }
+
+ return info;
+ }
+
+ /// <summary>
+ /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <param name="kernel">The kernel.</param>
+ /// <returns>BaseTaskTrigger.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception>
+ public static BaseTaskTrigger GetTrigger(TaskTriggerInfo info, IKernel kernel)
+ {
+ if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.TimeOfDayTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new DailyTrigger
+ {
+ TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value)
+ };
+ }
+
+ if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.TimeOfDayTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (!info.DayOfWeek.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new WeeklyTrigger
+ {
+ TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
+ DayOfWeek = info.DayOfWeek.Value
+ };
+ }
+
+ if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.IntervalTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new IntervalTrigger
+ {
+ Interval = TimeSpan.FromTicks(info.IntervalTicks.Value)
+ };
+ }
+
+ if (info.Type.Equals(typeof(SystemEventTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.SystemEvent.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new SystemEventTrigger
+ {
+ SystemEvent = info.SystemEvent.Value
+ };
+ }
+
+ if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return new StartupTrigger(kernel);
+ }
+
+ throw new ArgumentException("Unrecognized trigger type: " + info.Type);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs b/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs
new file mode 100644
index 0000000000..84775924ff
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Common.Kernel;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Class StartupTaskTrigger
+ /// </summary>
+ public class StartupTrigger : BaseTaskTrigger
+ {
+ /// <summary>
+ /// Gets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ protected IKernel Kernel { get; private set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StartupTrigger" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public StartupTrigger(IKernel kernel)
+ {
+ Kernel = kernel;
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal override void Start()
+ {
+ Kernel.ReloadCompleted += Kernel_ReloadCompleted;
+ }
+
+ async void Kernel_ReloadCompleted(object sender, EventArgs e)
+ {
+ await Task.Delay(2000).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal override void Stop()
+ {
+ Kernel.ReloadCompleted -= Kernel_ReloadCompleted;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs b/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs
new file mode 100644
index 0000000000..3075a8587b
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs
@@ -0,0 +1,55 @@
+using MediaBrowser.Model.Tasks;
+using Microsoft.Win32;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Class SystemEventTrigger
+ /// </summary>
+ public class SystemEventTrigger : BaseTaskTrigger
+ {
+ /// <summary>
+ /// Gets or sets the system event.
+ /// </summary>
+ /// <value>The system event.</value>
+ public SystemEvent SystemEvent { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal override void Start()
+ {
+ switch (SystemEvent)
+ {
+ case SystemEvent.WakeFromSleep:
+ SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal override void Stop()
+ {
+ SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
+ }
+
+ /// <summary>
+ /// Handles the PowerModeChanged event of the SystemEvents control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="PowerModeChangedEventArgs" /> instance containing the event data.</param>
+ async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
+ {
+ if (e.Mode == PowerModes.Resume && SystemEvent == SystemEvent.WakeFromSleep)
+ {
+ // This value is a bit arbitrary, but add a delay to help ensure network connections have been restored before running the task
+ await Task.Delay(5000).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/TaskManager.cs b/MediaBrowser.Common/ScheduledTasks/TaskManager.cs
new file mode 100644
index 0000000000..4e6407e7c6
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/TaskManager.cs
@@ -0,0 +1,108 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Class TaskManager
+ /// </summary>
+ public class TaskManager : BaseManager<IKernel>
+ {
+ /// <summary>
+ /// The _task queue
+ /// </summary>
+ private readonly List<Type> _taskQueue = new List<Type>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public TaskManager(IKernel kernel)
+ : base(kernel)
+ {
+
+ }
+
+ /// <summary>
+ /// Cancels if running and queue.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void CancelIfRunningAndQueue<T>()
+ where T : IScheduledTask
+ {
+ Kernel.ScheduledTasks.OfType<T>().First().CancelIfRunning();
+ QueueScheduledTask<T>();
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void QueueScheduledTask<T>()
+ where T : IScheduledTask
+ {
+ var scheduledTask = Kernel.ScheduledTasks.OfType<T>().First();
+
+ QueueScheduledTask(scheduledTask);
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ public void QueueScheduledTask(IScheduledTask task)
+ {
+ var type = task.GetType();
+
+ var scheduledTask = Kernel.ScheduledTasks.First(t => t.GetType() == type);
+
+ lock (_taskQueue)
+ {
+ // If it's idle just execute immediately
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ scheduledTask.Execute();
+ return;
+ }
+
+ if (!_taskQueue.Contains(type))
+ {
+ Logger.Info("Queueing task {0}", type.Name);
+ _taskQueue.Add(type);
+ }
+ else
+ {
+ Logger.Info("Task already queued: {0}", type.Name);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ internal void OnTaskCompleted(IScheduledTask task)
+ {
+ // Execute queued tasks
+ lock (_taskQueue)
+ {
+ var copy = _taskQueue.ToList();
+
+ foreach (var type in copy)
+ {
+ var scheduledTask = Kernel.ScheduledTasks.First(t => t.GetType() == type);
+
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ scheduledTask.Execute();
+
+ _taskQueue.Remove(type);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
new file mode 100644
index 0000000000..98c6d672a5
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -0,0 +1,109 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old cache files
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class DeleteCacheFileTask : BaseScheduledTask<IKernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }; //2am
+
+ return new[] { trigger };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ return Task.Run(() =>
+ {
+ var minDateModified = DateTime.UtcNow.AddMonths(-2);
+
+ DeleteCacheFilesFromDirectory(cancellationToken, Kernel.ApplicationPaths.CachePath, minDateModified, progress);
+ });
+ }
+
+
+ /// <summary>
+ /// Deletes the cache files from directory with a last write time less than a given date
+ /// </summary>
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ /// <param name="directory">The directory.</param>
+ /// <param name="minDateModified">The min date modified.</param>
+ /// <param name="progress">The progress.</param>
+ private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<TaskProgress> progress)
+ {
+ var filesToDelete = new DirectoryInfo(directory).EnumerateFileSystemInfos("*", SearchOption.AllDirectories)
+ .Where(f => !f.Attributes.HasFlag(FileAttributes.Directory) && f.LastWriteTimeUtc < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(new TaskProgress { Description = file.FullName, PercentComplete = 100 * percent });
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ File.Delete(file.FullName);
+
+ index++;
+ }
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Cache file cleanup"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Deletes cache files no longer needed by the system"; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs
new file mode 100644
index 0000000000..afb21187ce
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -0,0 +1,97 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old log files
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class DeleteLogFileTask : BaseScheduledTask<IKernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }; //2am
+
+ return new[] { trigger };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ return Task.Run(() =>
+ {
+ // Delete log files more than n days old
+ var minDateModified = DateTime.UtcNow.AddDays(-(Kernel.Configuration.LogFileRetentionDays));
+
+ var filesToDelete = new DirectoryInfo(Kernel.ApplicationPaths.LogDirectoryPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories)
+ .Where(f => f.LastWriteTimeUtc < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(new TaskProgress { Description = file.FullName, PercentComplete = 100 * percent });
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ File.Delete(file.FullName);
+
+ index++;
+ }
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ });
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Log file cleanup"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return string.Format("Deletes log files that are more than {0} days old.", Kernel.Configuration.LogFileRetentionDays); }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public override string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs
new file mode 100644
index 0000000000..24aaad57f1
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs
@@ -0,0 +1,61 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Class ReloadLoggerFileTask
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class ReloadLoggerFileTask : BaseScheduledTask<IKernel>
+ {
+ /// <summary>
+ /// Gets the default triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(0) }; //12am
+
+ return new[] { trigger };
+ }
+
+ /// <summary>
+ /// Executes the internal.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(new TaskProgress { PercentComplete = 0 });
+
+ return Task.Run(() => Kernel.ReloadLogger());
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Start new log file"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Moves logging to a new file to help reduce log file sizes."; }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs
new file mode 100644
index 0000000000..1412be2a24
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs
@@ -0,0 +1,108 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Deployment.Application;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Plugin Update Task
+ /// </summary>
+ [Export(typeof(IScheduledTask))]
+ public class SystemUpdateTask : BaseScheduledTask<IKernel>
+ {
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers()
+ {
+ return new BaseTaskTrigger[] {
+
+ // 1am
+ new DailyTrigger { TimeOfDay = TimeSpan.FromHours(1) },
+
+ new IntervalTrigger { Interval = TimeSpan.FromHours(2)}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ protected override async Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
+ {
+ if (!ApplicationDeployment.IsNetworkDeployed) return;
+
+ EventHandler<TaskProgress> innerProgressHandler = (sender, e) => progress.Report(new TaskProgress { PercentComplete = e.PercentComplete * .1 });
+
+ // Create a progress object for the update check
+ var innerProgress = new Progress<TaskProgress>();
+ innerProgress.ProgressChanged += innerProgressHandler;
+
+ var updateInfo = await new ApplicationUpdateCheck().CheckForApplicationUpdate(cancellationToken, innerProgress).ConfigureAwait(false);
+
+ // Release the event handler
+ innerProgress.ProgressChanged -= innerProgressHandler;
+
+ progress.Report(new TaskProgress { PercentComplete = 10 });
+
+ if (!updateInfo.UpdateAvailable)
+ {
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ return;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Configuration.EnableAutoUpdate)
+ {
+ Logger.Info("Update Revision {0} available. Updating...", updateInfo.AvailableVersion);
+
+ innerProgressHandler = (sender, e) => progress.Report(new TaskProgress { PercentComplete = (e.PercentComplete * .9) + .1 });
+
+ innerProgress = new Progress<TaskProgress>();
+ innerProgress.ProgressChanged += innerProgressHandler;
+
+ await new ApplicationUpdater().UpdateApplication(cancellationToken, innerProgress).ConfigureAwait(false);
+
+ // Release the event handler
+ innerProgress.ProgressChanged -= innerProgressHandler;
+
+ Kernel.OnApplicationUpdated(updateInfo.AvailableVersion);
+ }
+ else
+ {
+ Logger.Info("A new version of Media Browser is available.");
+ }
+
+ progress.Report(new TaskProgress { PercentComplete = 100 });
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get { return "Check for application updates"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public override string Description
+ {
+ get { return "Downloads and installs application updates."; }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs b/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs
new file mode 100644
index 0000000000..136dc8b130
--- /dev/null
+++ b/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Common.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires on a weekly basis
+ /// </summary>
+ public class WeeklyTrigger : BaseTaskTrigger
+ {
+ /// <summary>
+ /// Get the time of day to trigger the task to run
+ /// </summary>
+ /// <value>The time of day.</value>
+ public TimeSpan TimeOfDay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ public DayOfWeek DayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ protected internal override void Start()
+ {
+ DisposeTimer();
+
+ var triggerDate = GetNextTriggerDateTime();
+
+ Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Gets the next trigger date time.
+ /// </summary>
+ /// <returns>DateTime.</returns>
+ private DateTime GetNextTriggerDateTime()
+ {
+ var now = DateTime.Now;
+
+ // If it's on the same day
+ if (now.DayOfWeek == DayOfWeek)
+ {
+ // It's either later today, or a week from now
+ return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ }
+
+ var triggerDate = now.Date;
+
+ // Walk the date forward until we get to the trigger day
+ while (triggerDate.DayOfWeek != DayOfWeek)
+ {
+ triggerDate = triggerDate.AddDays(1);
+ }
+
+ // Return the trigger date plus the time offset
+ return triggerDate.Add(TimeOfDay);
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ protected internal override void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes this instance.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeTimer();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+ }
+}