diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj
index 4305f2e02..030ee6c9f 100644
--- a/src/NzbDrone.Common/NzbDrone.Common.csproj
+++ b/src/NzbDrone.Common/NzbDrone.Common.csproj
@@ -215,9 +215,11 @@
+
+
diff --git a/src/NzbDrone.Common/Timeline/ProgressReporter.cs b/src/NzbDrone.Common/Timeline/ProgressReporter.cs
new file mode 100644
index 000000000..6bf4ac6bb
--- /dev/null
+++ b/src/NzbDrone.Common/Timeline/ProgressReporter.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Common.Timeline
+{
+ public interface IProgressReporter
+ {
+ long Raw { get; }
+ long Total { get; }
+ double Progress { get; }
+
+ void UpdateProgress(long currentProgress, long maxProgress);
+ void FinishProgress();
+ }
+
+ public class ProgressReporter : IProgressReporter
+ {
+ private readonly int _maxSteps;
+
+ public long Raw { get; protected set; }
+ public long Total { get; private set; }
+
+ public double Progress => Total == 0 ? 1.0 : Math.Min(Raw, Total) / Total;
+
+ //public TimeSpan? EstimatedDurationRemaining { get; private set; }
+
+ public ProgressReporter(long initialProgress, long maxProgress, int maxSteps = 100)
+ {
+ _maxSteps = maxSteps;
+
+ Raw = initialProgress;
+ Total = maxProgress;
+ }
+
+ public void UpdateProgress(long currentProgress, long maxProgress)
+ {
+ bool shouldRaiseEvent;
+
+ lock (this)
+ {
+ var oldRaw = Raw;
+ var oldTotal = Total;
+
+ Raw = currentProgress;
+ Total = Total;
+
+ var oldStep = oldTotal <= 0 ? _maxSteps : oldRaw * _maxSteps / oldTotal;
+ var newStep = Total <= 0 ? _maxSteps : Raw * _maxSteps / Total;
+
+ shouldRaiseEvent = (oldStep != newStep);
+ }
+
+ if (shouldRaiseEvent)
+ {
+ RaiseEvent();
+ }
+ }
+
+ public void FinishProgress()
+ {
+ lock (this)
+ {
+ Raw = Total;
+ }
+
+ RaiseEvent();
+ }
+
+ protected virtual void RaiseEvent()
+ {
+ // TODO
+ }
+
+ }
+}
diff --git a/src/NzbDrone.Common/Timeline/TimelineContext.cs b/src/NzbDrone.Common/Timeline/TimelineContext.cs
new file mode 100644
index 000000000..b140a4e6e
--- /dev/null
+++ b/src/NzbDrone.Common/Timeline/TimelineContext.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Common.Timeline
+{
+ public enum TimelineState
+ {
+ Pending,
+ Started,
+ Completed,
+ Failed
+ }
+
+ public interface ITimelineContext : IProgressReporter
+ {
+ string Name { get; }
+ TimelineState State { get; }
+
+ void UpdateState(TimelineState state);
+ ITimelineContext AppendTimeline(string name, long initialProgress = 0, long maxProgress = 1);
+ void AppendTimeline(ITimelineContext timeline);
+ }
+
+ public class TimelineContext : ProgressReporter, ITimelineContext
+ {
+ private List _timelines = new List();
+
+ public string Name { get; private set; }
+ public TimelineState State { get; private set; }
+
+ public IEnumerable Timelines
+ {
+ get
+ {
+ lock (this)
+ {
+ return _timelines.ToArray();
+ }
+ }
+ }
+
+ public TimelineContext(string name, long initialProgress, long maxProgress)
+ : base(initialProgress, maxProgress)
+ {
+ Name = name;
+ }
+
+ public void UpdateState(TimelineState state)
+ {
+ lock (this)
+ {
+ State = state;
+
+ if (State == TimelineState.Completed || State == TimelineState.Failed)
+ {
+ Raw = Total;
+ }
+ }
+
+ RaiseEvent();
+ }
+
+ public ITimelineContext AppendTimeline(string name, long initialProgress = 0, long maxProgress = 1)
+ {
+ lock (this)
+ {
+ var timeline = new TimelineContext(name, initialProgress, maxProgress);
+ _timelines.Add(timeline);
+ return timeline;
+ }
+ }
+
+ public void AppendTimeline(ITimelineContext timeline)
+ {
+ lock (this)
+ {
+ _timelines.Add(timeline);
+ }
+ }
+
+ protected override void RaiseEvent()
+ {
+ lock (this)
+ {
+ if (Raw == Total)
+ {
+ State = TimelineState.Completed;
+ }
+ else
+ {
+ State = TimelineState.Started;
+ }
+ }
+
+ base.RaiseEvent();
+ }
+ }
+}