From cbe4be814c165a2f4ea4290d6220613e6dfc8f29 Mon Sep 17 00:00:00 2001
From: "kay.one" <kay.one@gmail.com>
Date: Sun, 12 May 2013 17:36:23 -0700
Subject: [PATCH] fixed diskscan

removed all stored status fields from episode
---
 NzbDrone.Api/Commands/CommandModule.cs        | 13 +--
 NzbDrone.Common/Composition/Container.cs      | 15 +++-
 .../Composition/ContainerBuilderBase.cs       | 14 +--
 NzbDrone.Common/Composition/IContainer.cs     |  2 +
 .../UpgradeDiskSpecificationFixture.cs        | 11 ++-
 NzbDrone.Core.Test/EpisodeStatusTest.cs       | 90 +------------------
 ...=> MediaFileTableCleanupServiceFixture.cs} | 34 ++++++-
 NzbDrone.Core.Test/NzbDrone.Core.Test.csproj  |  3 +-
 .../GetVideoFilesFixture.cs                   |  1 +
 .../ImportFileFixture.cs                      | 32 +++++--
 .../EpisodesBetweenDatesFixture.cs            |  3 -
 .../EpisodesRepositoryReadFixture.cs          | 45 ++++++++++
 .../EpisodesWithoutFilesFixture.cs            |  7 +-
 .../Datastore/Migration/Migration20130324.cs  |  2 -
 .../Datastore/RelationshipExtensions.cs       |  2 +-
 NzbDrone.Core/Datastore/TableMapping.cs       |  7 +-
 .../UpgradeDiskSpecification.cs               |  2 +-
 NzbDrone.Core/Jobs/TaskManager.cs             |  8 +-
 .../MediaFiles/Commands/CleanMediaFileDb.cs   | 14 +++
 .../MediaFiles/Commands/DiskScanCommand.cs    | 17 ++++
 .../DiskScanService.cs}                       | 49 +++++++---
 NzbDrone.Core/MediaFiles/EpisodeFile.cs       | 14 ---
 .../MediaFiles/GhostFileCleanupService.cs     | 48 ----------
 .../MediaFileTableCleanupService.cs           | 55 ++++++++++++
 NzbDrone.Core/NzbDrone.Core.csproj            |  6 +-
 .../Providers/Converting/HandbrakeProvider.cs |  2 +-
 NzbDrone.Core/Providers/MisnamedProvider.cs   |  4 +-
 NzbDrone.Core/Tv/Episode.cs                   | 24 +----
 NzbDrone.Core/Tv/EpisodeRepository.cs         | 16 +---
 NzbDrone.Core/Tv/EpisodeService.cs            | 37 +-------
 NzbDrone/NLog.config                          |  2 +-
 UI/Series/Index/SeriesIndexLayout.js          |  2 +-
 32 files changed, 295 insertions(+), 286 deletions(-)
 rename NzbDrone.Core.Test/MediaFileTests/{GhostFileCleanupFixture.cs => MediaFileTableCleanupServiceFixture.cs} (57%)
 create mode 100644 NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs
 create mode 100644 NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs
 create mode 100644 NzbDrone.Core/MediaFiles/Commands/DiskScanCommand.cs
 rename NzbDrone.Core/{Providers/IDiskScanService.cs => MediaFiles/DiskScanService.cs} (71%)
 delete mode 100644 NzbDrone.Core/MediaFiles/GhostFileCleanupService.cs
 create mode 100644 NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs

diff --git a/NzbDrone.Api/Commands/CommandModule.cs b/NzbDrone.Api/Commands/CommandModule.cs
index ae99bb893..4db3b0f69 100644
--- a/NzbDrone.Api/Commands/CommandModule.cs
+++ b/NzbDrone.Api/Commands/CommandModule.cs
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using NzbDrone.Api.Extensions;
+using NzbDrone.Common.Composition;
 using NzbDrone.Common.Messaging;
 
 namespace NzbDrone.Api.Commands
@@ -9,20 +10,22 @@ namespace NzbDrone.Api.Commands
     public class CommandModule : NzbDroneRestModule<CommandResource>
     {
         private readonly IMessageAggregator _messageAggregator;
-        private readonly IEnumerable<ICommand> _commands;
+        private readonly IContainer _container;
 
-        public CommandModule(IMessageAggregator messageAggregator, IEnumerable<ICommand> commands)
+        public CommandModule(IMessageAggregator messageAggregator, IContainer container)
         {
             _messageAggregator = messageAggregator;
-            _commands = commands;
+            _container = container;
 
             CreateResource = RunCommand;
         }
 
         private CommandResource RunCommand(CommandResource resource)
         {
-            var commandType = _commands.Single(c => c.GetType().Name.Replace("Command", "").Equals(resource.Command, StringComparison.InvariantCultureIgnoreCase))
-                                       .GetType();
+            var commandType =
+                _container.GetImplementations(typeof(ICommand))
+                          .Single(c => c.Name.Replace("Command", "")
+                               .Equals(resource.Command, StringComparison.InvariantCultureIgnoreCase));
 
 
             var command = Request.Body.FromJson<ICommand>(commandType);
diff --git a/NzbDrone.Common/Composition/Container.cs b/NzbDrone.Common/Composition/Container.cs
index 2033d3405..1afa9d3b6 100644
--- a/NzbDrone.Common/Composition/Container.cs
+++ b/NzbDrone.Common/Composition/Container.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using TinyIoC;
 
 namespace NzbDrone.Common.Composition
@@ -7,10 +8,12 @@ namespace NzbDrone.Common.Composition
     public class Container : IContainer
     {
         private readonly TinyIoCContainer _container;
+        private readonly List<Type> _loadedTypes;
 
-        public Container(TinyIoCContainer container)
+        public Container(TinyIoCContainer container, List<Type> loadedTypes)
         {
             _container = container;
+            _loadedTypes = loadedTypes;
             _container.Register<IContainer>(this);
         }
 
@@ -92,5 +95,15 @@ namespace NzbDrone.Common.Composition
         {
             return _container.CanResolve(type);
         }
+
+        public IEnumerable<Type> GetImplementations(Type contractType)
+        {
+            return _loadedTypes
+                .Where(implementation =>
+                       contractType.IsAssignableFrom(implementation) &&
+                       !implementation.IsInterface &&
+                       !implementation.IsAbstract
+                );
+        }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/NzbDrone.Common/Composition/ContainerBuilderBase.cs
index f48cc677a..14b6f6400 100644
--- a/NzbDrone.Common/Composition/ContainerBuilderBase.cs
+++ b/NzbDrone.Common/Composition/ContainerBuilderBase.cs
@@ -18,8 +18,6 @@ namespace NzbDrone.Common.Composition
 
         protected ContainerBuilderBase(params string[] assemblies)
         {
-            Container = new Container(new TinyIoCContainer());
-
             _loadedTypes = new List<Type>();
 
             foreach (var assembly in assemblies)
@@ -27,6 +25,7 @@ namespace NzbDrone.Common.Composition
                 _loadedTypes.AddRange(Assembly.Load(assembly).GetTypes());
             }
 
+            Container = new Container(new TinyIoCContainer(), _loadedTypes);
             AutoRegisterInterfaces();
         }
 
@@ -52,7 +51,7 @@ namespace NzbDrone.Common.Composition
 
         private void AutoRegisterImplementations(Type contractType)
         {
-            var implementations = GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList();
+            var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList();
 
 
 
@@ -84,14 +83,5 @@ namespace NzbDrone.Common.Composition
             }
         }
 
-        private IEnumerable<Type> GetImplementations(Type contractType)
-        {
-            return _loadedTypes
-                .Where(implementation =>
-                       contractType.IsAssignableFrom(implementation) &&
-                       !implementation.IsInterface &&
-                       !implementation.IsAbstract
-                );
-        }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Common/Composition/IContainer.cs b/NzbDrone.Common/Composition/IContainer.cs
index 7fc5dea09..f1b3c0001 100644
--- a/NzbDrone.Common/Composition/IContainer.cs
+++ b/NzbDrone.Common/Composition/IContainer.cs
@@ -26,5 +26,7 @@ namespace NzbDrone.Common.Composition
         void Register(Type registrationType, object instance);
         void RegisterAll(Type registrationType, IEnumerable<Type> implementationList);
         bool IsTypeRegistered(Type type);
+
+        IEnumerable<Type> GetImplementations(Type contractType);
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
index f5d881db2..53d8dba97 100644
--- a/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
+++ b/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
@@ -34,8 +34,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
             firstFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p, true), DateAdded = DateTime.Now };
             secondFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p, true), DateAdded = DateTime.Now };
 
-            var singleEpisodeList = new List<Episode> { new Episode { EpisodeFile = firstFile }, new Episode { EpisodeFile = null } };
-            var doubleEpisodeList = new List<Episode> { new Episode { EpisodeFile = firstFile }, new Episode { EpisodeFile = secondFile }, new Episode { EpisodeFile = null } };
+            var singleEpisodeList = new List<Episode> { new Episode { EpisodeFile = firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } };
+            var doubleEpisodeList = new List<Episode> { new Episode { EpisodeFile = firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } };
 
             var fakeSeries = Builder<Series>.CreateNew()
                          .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p })
@@ -66,6 +66,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
             secondFile.Quality = new QualityModel(Quality.SDTV);
         }
 
+        [Test]
+        public void should_return_true_if_episode_has_no_existing_file()
+        {
+            parseResultSingle.Episodes.ForEach(c => c.EpisodeFileId = 0);
+            _upgradeDisk.IsSatisfiedBy(parseResultSingle).Should().BeTrue();
+        }
+
         [Test]
         public void should_return_true_if_single_episode_doesnt_exist_on_disk()
         {
diff --git a/NzbDrone.Core.Test/EpisodeStatusTest.cs b/NzbDrone.Core.Test/EpisodeStatusTest.cs
index 8d266fe98..2b4d589f2 100644
--- a/NzbDrone.Core.Test/EpisodeStatusTest.cs
+++ b/NzbDrone.Core.Test/EpisodeStatusTest.cs
@@ -11,79 +11,15 @@ using NzbDrone.Core.Test.Framework;
 namespace NzbDrone.Core.Test
 {
     [TestFixture]
-    
+
     public class EpisodeStatusTest : CoreTest
     {
-        [TestCase(1, false, false, EpisodeStatuses.NotAired)]
-        [TestCase(-2, false, false, EpisodeStatuses.Missing)]
-        [TestCase(0, false, false, EpisodeStatuses.AirsToday)]
-        [TestCase(1, true, false, EpisodeStatuses.Ready)]
-        public void no_grab_date(int offsetDays, bool hasEpisodes, bool ignored, EpisodeStatuses status)
-        {
-            Episode episode = Builder<Episode>.CreateNew()
-                .With(e => e.AirDate = DateTime.Now.AddDays(offsetDays))
-                .With(e => e.Ignored = ignored)
-                .With(e => e.GrabDate = null)
-                .Build();
-
-            if (hasEpisodes)
-            {
-                episode.EpisodeFile = new EpisodeFile();
-            }
-
-            episode.Status.Should().Be(status);
-        }
-
-        [TestCase(1, false, false, EpisodeStatuses.Missing)]
-        [TestCase(-2, false, false, EpisodeStatuses.Missing)]
-        [TestCase(1, true, false, EpisodeStatuses.Ready)]
-        public void old_grab_date(int offsetDays, bool hasEpisodes, bool ignored,
-                                                 EpisodeStatuses status)
-        {
-            Episode episode = Builder<Episode>.CreateNew()
-               .With(e => e.Ignored = ignored)
-                .With(e => e.GrabDate = DateTime.Now.AddDays(-2).AddHours(-1))
-                .With(e => e.AirDate = DateTime.Today.AddDays(-2))
-                .Build();
-
-            if (hasEpisodes)
-            {
-                episode.EpisodeFile = new EpisodeFile();
-            }
-
-            episode.Status.Should().Be(status);
-        }
-
-        [TestCase(1, false, false, EpisodeStatuses.Downloading)]
-        [TestCase(-2, false, false, EpisodeStatuses.Downloading)]
-        [TestCase(1, true, false, EpisodeStatuses.Ready)]
-        [TestCase(1, true, true, EpisodeStatuses.Ready)]
-        [TestCase(1, false, true, EpisodeStatuses.Downloading)]
-        public void recent_grab_date(int offsetDays, bool hasEpisodes, bool ignored,
-                                                    EpisodeStatuses status)
-        {
-            Episode episode = Builder<Episode>.CreateNew()
-            .With(e => e.AirDate = DateTime.Now.AddDays(offsetDays))
-                .With(e => e.Ignored = ignored)
-                .With(e => e.GrabDate = DateTime.Now.AddHours(22))
-                .Build();
-
-            if (hasEpisodes)
-            {
-                episode.EpisodeFile = new EpisodeFile();
-            }
-
-            episode.Status.Should().Be(status);
-
-        }
-
         [TestCase(1, true, true, EpisodeStatuses.Ready)]
         public void ignored_episode(int offsetDays, bool ignored, bool hasEpisodes, EpisodeStatuses status)
         {
             Episode episode = Builder<Episode>.CreateNew()
                 .With(e => e.AirDate = DateTime.Now.AddDays(offsetDays))
                 .With(e => e.Ignored = ignored)
-                .With(e => e.GrabDate = null)
                 .Build();
 
             if (hasEpisodes)
@@ -101,33 +37,11 @@ namespace NzbDrone.Core.Test
             Episode episode = Builder<Episode>.CreateNew()
                 .With(e => e.AirDate = DateTime.Now.AddDays(20))
                 .With(e => e.Ignored = false)
-                .With(e => e.GrabDate = null)
+                .With(e => e.EpisodeFileId = 0)
                 .Build();
 
 
             episode.Status.Should().Be(EpisodeStatuses.NotAired);
         }
-
-        [TestCase(false, false, EpisodeStatuses.Failed, PostDownloadStatusType.Failed)]
-        [TestCase(false, false, EpisodeStatuses.Unpacking, PostDownloadStatusType.Unpacking)]
-        [TestCase(true, false, EpisodeStatuses.Ready, PostDownloadStatusType.Failed)]
-        [TestCase(true, true, EpisodeStatuses.Ready, PostDownloadStatusType.Unpacking)]
-        public void episode_downloaded_post_download_status_is_used(bool hasEpisodes, bool ignored,
-                                                    EpisodeStatuses status, PostDownloadStatusType postDownloadStatus)
-        {
-            Episode episode = Builder<Episode>.CreateNew()
-                .With(e => e.Ignored = ignored)
-                .With(e => e.GrabDate = DateTime.Now.AddHours(22))
-                .With(e => e.PostDownloadStatus = postDownloadStatus)
-                .Build();
-
-            if (hasEpisodes)
-            {
-                episode.EpisodeFile = new EpisodeFile();
-            }
-
-            episode.Status.Should().Be(status);
-
-        }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/MediaFileTests/GhostFileCleanupFixture.cs b/NzbDrone.Core.Test/MediaFileTests/MediaFileTableCleanupServiceFixture.cs
similarity index 57%
rename from NzbDrone.Core.Test/MediaFileTests/GhostFileCleanupFixture.cs
rename to NzbDrone.Core.Test/MediaFileTests/MediaFileTableCleanupServiceFixture.cs
index b2617d87c..771f67540 100644
--- a/NzbDrone.Core.Test/MediaFileTests/GhostFileCleanupFixture.cs
+++ b/NzbDrone.Core.Test/MediaFileTests/MediaFileTableCleanupServiceFixture.cs
@@ -5,13 +5,14 @@ using Moq;
 using NUnit.Framework;
 using NzbDrone.Common;
 using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.MediaFiles.Commands;
 using NzbDrone.Core.Test.Framework;
 using NzbDrone.Core.Tv;
 using System.Linq;
 
 namespace NzbDrone.Core.Test.MediaFileTests
 {
-    public class GhostFileCleanupFixture : CoreTest<GhostFileCleanupService>
+    public class MediaFileTableCleanupServiceFixture : CoreTest<MediaFileTableCleanupService>
     {
 
         private void GiveEpisodeFiles(IEnumerable<EpisodeFile> episodeFiles)
@@ -30,6 +31,10 @@ namespace NzbDrone.Core.Test.MediaFileTests
             Mocker.GetMock<IDiskProvider>()
              .Setup(e => e.FileExists(It.Is<String>(c => c != DeletedPath)))
              .Returns(true);
+
+            Mocker.GetMock<IEpisodeService>()
+            .Setup(c => c.GetEpisodesByFileId(It.IsAny<int>()))
+            .Returns(new List<Episode> { new Episode() });
         }
 
         [Test]
@@ -40,7 +45,7 @@ namespace NzbDrone.Core.Test.MediaFileTests
 
             GiveEpisodeFiles(episodeFiles);
 
-            Subject.RemoveNonExistingFiles(0);
+            Subject.Execute(new CleanMediaFileDb(0));
 
             Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
         }
@@ -55,10 +60,33 @@ namespace NzbDrone.Core.Test.MediaFileTests
 
             GiveEpisodeFiles(episodeFiles);
 
-            Subject.RemoveNonExistingFiles(0);
+            Subject.Execute(new CleanMediaFileDb(0));
 
             Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.Is<EpisodeFile>(e => e.Path == DeletedPath)), Times.Exactly(2));
 
         }
+
+        [Test]
+        public void should_delete_files_that_dont_belong_to_any_episodes()
+        {
+            var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(10)
+                                .Random(10)
+                                .With(c => c.Path = "ExistingPath")
+                                .Build();
+
+            GiveEpisodeFiles(episodeFiles);
+            GivenFilesAreNotAttachedToEpisode();
+
+            Subject.Execute(new CleanMediaFileDb(0));
+
+            Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.IsAny<EpisodeFile>()), Times.Exactly(10));
+        }
+
+        private void GivenFilesAreNotAttachedToEpisode()
+        {
+            Mocker.GetMock<IEpisodeService>()
+                  .Setup(c => c.GetEpisodesByFileId(It.IsAny<int>()))
+                  .Returns(new List<Episode>());
+        }
     }
 }
diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index c6d206185..ada85977a 100644
--- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -142,7 +142,7 @@
     <Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
     <Compile Include="JobTests\TestJobs.cs" />
     <Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" />
-    <Compile Include="MediaFileTests\GhostFileCleanupFixture.cs" />
+    <Compile Include="MediaFileTests\MediaFileTableCleanupServiceFixture.cs" />
     <Compile Include="MediaFileTests\MediaFileRepositoryFixture.cs" />
     <Compile Include="MediaFileTests\EpisodeFileMoverFixture.cs" />
     <Compile Include="MetadataSourceTests\TracktProxyFixture.cs" />
@@ -160,6 +160,7 @@
     <Compile Include="ProviderTests\RecycleBinProviderTests\DeleteFileFixture.cs" />
     <Compile Include="ProviderTests\RecycleBinProviderTests\DeleteDirectoryFixture.cs" />
     <Compile Include="ProviderTests\PlexProviderTest.cs" />
+    <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesRepositoryReadFixture.cs" />
     <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithoutFilesFixture.cs" />
     <Compile Include="TvTests\EpisodeRepositoryTests\EpisodesBetweenDatesFixture.cs" />
     <Compile Include="TvTests\SeasonProviderTest.cs" />
diff --git a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetVideoFilesFixture.cs b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetVideoFilesFixture.cs
index 976562756..5f9d1478c 100644
--- a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetVideoFilesFixture.cs
+++ b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetVideoFilesFixture.cs
@@ -4,6 +4,7 @@ using FluentAssertions;
 using Moq;
 using NUnit.Framework;
 using NzbDrone.Common;
+using NzbDrone.Core.MediaFiles;
 using NzbDrone.Core.Providers;
 
 using NzbDrone.Core.Test.Framework;
diff --git a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs
index ba45d0349..e3c9f513d 100644
--- a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs
+++ b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs
@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using FizzWare.NBuilder;
 using FluentAssertions;
@@ -23,7 +22,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
 
         private long _fileSize = 80.Megabytes();
         private Series _fakeSeries;
-        private List<Episode> _fakeEpisodes;
+        private Episode[] _fakeEpisodes;
         private Episode _fakeEpisode;
 
         [SetUp]
@@ -35,14 +34,16 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
 
             _fakeEpisode = Builder<Episode>
                     .CreateNew()
+                    .With(c => c.EpisodeFileId = 0)
                     .Build();
 
 
             _fakeEpisodes = Builder<Episode>.CreateListOfSize(2)
                                         .All()
                                         .With(c => c.SeasonNumber = 3)
+                                        .With(c => c.EpisodeFileId = 1)
                                         .With(e => e.EpisodeFile = new EpisodeFile())
-                                        .BuildList();
+                                        .BuildList().ToArray();
 
             GivenNewFile();
 
@@ -69,8 +70,20 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         }
 
 
-        private void GivenEpisodes(IEnumerable<Episode> episodes, QualityModel quality)
+        private void GivenEpisodes(Episode[] episodes, QualityModel quality)
         {
+            foreach (var episode in episodes)
+            {
+                if (episode.EpisodeFile == null)
+                {
+                    episode.EpisodeFileId = 0;
+                }
+                else
+                {
+                    episode.EpisodeFileId = episode.EpisodeFile.Value.Id;
+                }
+            }
+
             Mocker.GetMock<IParsingService>()
                   .Setup(c => c.GetEpisodes(It.IsAny<string>(), It.IsAny<Series>()))
                   .Returns(new LocalEpisode
@@ -102,6 +115,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         public void import_new_file_with_same_quality_should_succeed()
         {
             _fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
+
             GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.SDTV));
 
             var result = Subject.ImportFile(_fakeSeries, "file.ext");
@@ -112,6 +126,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         public void import_new_file_with_better_quality_should_succeed()
         {
             _fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
+
             GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
 
             var result = Subject.ImportFile(_fakeSeries, "file.ext");
@@ -121,7 +136,8 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         [Test]
         public void import_new_file_episode_has_better_quality_should_skip()
         {
-            _fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p) };
+            _fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
+
             GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.SDTV));
 
             var result = Subject.ImportFile(_fakeSeries, "file.ext");
@@ -158,7 +174,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         [Test]
         public void import_file_with_no_episode_in_db_should_skip()
         {
-            GivenEpisodes(new List<Episode>(), new QualityModel());
+            GivenEpisodes(new Episode[0], new QualityModel());
 
             var result = Subject.ImportFile(_fakeSeries, "file.ext");
 
@@ -185,8 +201,8 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
         [Test]
         public void skip_import_new_multi_part_file_episode_existing_has_better_quality()
         {
-            _fakeEpisodes[0].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p) };
-            _fakeEpisodes[1].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p) };
+            _fakeEpisodes[0].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
+            _fakeEpisodes[1].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
 
             GivenEpisodes(_fakeEpisodes, new QualityModel(Quality.SDTV));
 
diff --git a/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs
index 5230fb6e5..b21b731d2 100644
--- a/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs
+++ b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs
@@ -1,7 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
 using FizzWare.NBuilder;
 using FluentAssertions;
 using NUnit.Framework;
diff --git a/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs
new file mode 100644
index 000000000..41e691e34
--- /dev/null
+++ b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesRepositoryReadFixture.cs
@@ -0,0 +1,45 @@
+using System;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
+{
+    [TestFixture]
+    public class EpisodesRepositoryReadFixture : DbTest<EpisodeRepository, Episode>
+    {
+        private Series series;
+
+        [SetUp]
+        public void Setup()
+        {
+            series = Builder<Series>.CreateNew()
+                                        .With(s => s.Runtime = 30)
+                                        .BuildNew();
+
+            Db.Insert(series);
+        }
+
+        [Test]
+        public void should_get_episodes_by_file()
+        {
+            var episodeFile = Builder<EpisodeFile>.CreateNew().BuildNew();
+
+            Db.Insert(episodeFile);
+
+            var episode = Builder<Episode>.CreateListOfSize(2)
+                                        .All()
+                                        .With(e => e.SeriesId = series.Id)
+                                        .With(e => e.EpisodeFileId = episodeFile.Id)
+                                        .BuildListOfNew();
+
+            Db.InsertMany(episode);
+
+            var episodes = Subject.GetEpisodeByFileId(episodeFile.Id);
+            episodes.Should().HaveCount(2);
+        }
+    }
+}
diff --git a/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
index 082b9c4ff..12513c980 100644
--- a/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
+++ b/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using FizzWare.NBuilder;
+using FizzWare.NBuilder;
 using FluentAssertions;
 using NUnit.Framework;
 using NzbDrone.Core.Datastore;
diff --git a/NzbDrone.Core/Datastore/Migration/Migration20130324.cs b/NzbDrone.Core/Datastore/Migration/Migration20130324.cs
index e5462aaa4..c0b5685bf 100644
--- a/NzbDrone.Core/Datastore/Migration/Migration20130324.cs
+++ b/NzbDrone.Core/Datastore/Migration/Migration20130324.cs
@@ -59,8 +59,6 @@ namespace NzbDrone.Core.Datastore.Migration
                 .WithColumn("Ignored").AsBoolean().Nullable()
                 .WithColumn("EpisodeFileId").AsInt32().Nullable()
                 .WithColumn("AirDate").AsDateTime().Nullable()
-                .WithColumn("GrabDate").AsDateTime().Nullable()
-                .WithColumn("PostDownloadStatus").AsInt32().Nullable()
                 .WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable()
                 .WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable()
                 .WithColumn("SceneSeasonNumber").AsInt32().Nullable()
diff --git a/NzbDrone.Core/Datastore/RelationshipExtensions.cs b/NzbDrone.Core/Datastore/RelationshipExtensions.cs
index 7c8767966..1f32aee74 100644
--- a/NzbDrone.Core/Datastore/RelationshipExtensions.cs
+++ b/NzbDrone.Core/Datastore/RelationshipExtensions.cs
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Datastore
         {
             return relationshipBuilder.For(portalExpression.GetMemberName())
                                 .LazyLoad((db, parent) => db.Query<TChild>()
-                                .Single(c => c.Id == childIdSelector(parent)));
+                                .SingleOrDefault(c => c.Id == childIdSelector(parent)));
 
         }
 
diff --git a/NzbDrone.Core/Datastore/TableMapping.cs b/NzbDrone.Core/Datastore/TableMapping.cs
index fd05a11e6..239268e01 100644
--- a/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/NzbDrone.Core/Datastore/TableMapping.cs
@@ -56,9 +56,12 @@ namespace NzbDrone.Core.Datastore
 
             Mapper.Entity<Episode>().RegisterModel("Episodes")
                   .Ignore(e => e.SeriesTitle)
-                  .Relationships.AutoMapICollectionOrComplexProperties();
+                  .Relationship()
+                  .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId);
+            //.Relationships.AutoMapICollectionOrComplexProperties();
 
-            Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles");
+            Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
+                  .Relationships.AutoMapICollectionOrComplexProperties();
 
             Mapper.Entity<QualityProfile>().RegisterModel("QualityProfiles");
 
diff --git a/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs
index db792e010..1134cedc9 100644
--- a/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs
+++ b/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
 
         public virtual bool IsSatisfiedBy(RemoteEpisode subject)
         {
-            foreach (var file in subject.Episodes.Select(c => c.EpisodeFile).Where(c => c != null))
+            foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
             {
                 _logger.Trace("Comparing file quality with report. Existing file is {0}", file.Quality);
 
diff --git a/NzbDrone.Core/Jobs/TaskManager.cs b/NzbDrone.Core/Jobs/TaskManager.cs
index fe82b7f21..16b06c9ba 100644
--- a/NzbDrone.Core/Jobs/TaskManager.cs
+++ b/NzbDrone.Core/Jobs/TaskManager.cs
@@ -69,8 +69,12 @@ namespace NzbDrone.Core.Jobs
 
         public void HandleAsync(CommandExecutedEvent message)
         {
-            var commandId = _scheduledTaskRepository.GetDefinition(message.Command.GetType()).Id;
-            _scheduledTaskRepository.SetLastExecutionTime(commandId, DateTime.UtcNow);
+            var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.GetType().FullName);
+
+            if (scheduledTask != null)
+            {
+                _scheduledTaskRepository.SetLastExecutionTime(scheduledTask.Id, DateTime.UtcNow);
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs b/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs
new file mode 100644
index 000000000..b873dcc5f
--- /dev/null
+++ b/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs
@@ -0,0 +1,14 @@
+using NzbDrone.Common.Messaging;
+
+namespace NzbDrone.Core.MediaFiles.Commands
+{
+    public class CleanMediaFileDb : ICommand
+    {
+        public int SeriesId { get; private set; }
+
+        public CleanMediaFileDb(int seriesId)
+        {
+            SeriesId = seriesId;
+        }
+    }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/MediaFiles/Commands/DiskScanCommand.cs b/NzbDrone.Core/MediaFiles/Commands/DiskScanCommand.cs
new file mode 100644
index 000000000..db7dd7a04
--- /dev/null
+++ b/NzbDrone.Core/MediaFiles/Commands/DiskScanCommand.cs
@@ -0,0 +1,17 @@
+using NzbDrone.Common.Messaging;
+
+namespace NzbDrone.Core.MediaFiles.Commands
+{
+    public class DiskScanCommand : ICommand
+    {
+        public int? SeriesId { get; private set; }
+
+        public DiskScanCommand(int seriesId = 0)
+        {
+            if (seriesId != 0)
+            {
+                SeriesId = seriesId;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/Providers/IDiskScanService.cs b/NzbDrone.Core/MediaFiles/DiskScanService.cs
similarity index 71%
rename from NzbDrone.Core/Providers/IDiskScanService.cs
rename to NzbDrone.Core/MediaFiles/DiskScanService.cs
index 2d7ab8364..2ea1f5c4c 100644
--- a/NzbDrone.Core/Providers/IDiskScanService.cs
+++ b/NzbDrone.Core/MediaFiles/DiskScanService.cs
@@ -4,40 +4,43 @@ using System.IO;
 using System.Linq;
 using NLog;
 using NzbDrone.Common;
-using NzbDrone.Core.MediaFiles;
+using NzbDrone.Common.Messaging;
+using NzbDrone.Core.MediaFiles.Commands;
 using NzbDrone.Core.Parser;
+using NzbDrone.Core.Providers;
 using NzbDrone.Core.Tv;
 
-namespace NzbDrone.Core.Providers
+namespace NzbDrone.Core.MediaFiles
 {
     public interface IDiskScanService
     {
-        void Scan(Series series);
         EpisodeFile ImportFile(Series series, string filePath);
         string[] GetVideoFiles(string path, bool allDirectories = true);
     }
 
-    public class DiskScanService : IDiskScanService
+    public class DiskScanService : IDiskScanService, IExecute<DiskScanCommand>
     {
         private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
         private static readonly string[] MediaExtensions = new[] { ".mkv", ".avi", ".wmv", ".mp4", ".mpg", ".mpeg", ".xvid", ".flv", ".mov", ".rm", ".rmvb", ".divx", ".dvr-ms", ".ts", ".ogm", ".m4v", ".strm" };
         private readonly IDiskProvider _diskProvider;
-        private readonly ICleanGhostFiles _ghostFileCleaner;
+        private readonly ISeriesService _seriesService;
         private readonly IMediaFileService _mediaFileService;
         private readonly IVideoFileInfoReader _videoFileInfoReader;
         private readonly IParsingService _parsingService;
+        private readonly IMessageAggregator _messageAggregator;
 
-        public DiskScanService(IDiskProvider diskProvider, ICleanGhostFiles ghostFileCleaner, IMediaFileService mediaFileService, IVideoFileInfoReader videoFileInfoReader,
-            IParsingService parsingService)
+        public DiskScanService(IDiskProvider diskProvider, ISeriesService seriesService, IMediaFileService mediaFileService, IVideoFileInfoReader videoFileInfoReader,
+            IParsingService parsingService, IMessageAggregator messageAggregator)
         {
             _diskProvider = diskProvider;
-            _ghostFileCleaner = ghostFileCleaner;
+            _seriesService = seriesService;
             _mediaFileService = mediaFileService;
             _videoFileInfoReader = videoFileInfoReader;
             _parsingService = parsingService;
+            _messageAggregator = messageAggregator;
         }
 
-        public virtual void Scan(Series series)
+        private void Scan(Series series)
         {
             if (!_diskProvider.FolderExists(series.Path))
             {
@@ -45,7 +48,7 @@ namespace NzbDrone.Core.Providers
                 return;
             }
 
-            _ghostFileCleaner.RemoveNonExistingFiles(series.Id);
+            _messageAggregator.PublishCommand(new CleanMediaFileDb(series.Id));
 
             var mediaFileList = GetVideoFiles(series.Path);
 
@@ -58,7 +61,7 @@ namespace NzbDrone.Core.Providers
             //Todo: Move the episode linking to here, instead of import (or rename import)
         }
 
-        public virtual EpisodeFile ImportFile(Series series, string filePath)
+        public EpisodeFile ImportFile(Series series, string filePath)
         {
             Logger.Trace("Importing file to database [{0}]", filePath);
 
@@ -87,7 +90,7 @@ namespace NzbDrone.Core.Providers
                 }
             }
 
-            if (parsedEpisode.Episodes.Any(e => e.EpisodeFile != null && e.EpisodeFile.Quality > parsedEpisode.Quality))
+            if (parsedEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && e.EpisodeFile.Value.Quality > parsedEpisode.Quality))
             {
                 Logger.Trace("This file isn't an upgrade for all episodes. Skipping {0}", filePath);
                 return null;
@@ -101,6 +104,7 @@ namespace NzbDrone.Core.Providers
             episodeFile.Quality = parsedEpisode.Quality;
             episodeFile.SeasonNumber = parsedEpisode.SeasonNumber;
             episodeFile.SceneName = Path.GetFileNameWithoutExtension(filePath.CleanPath());
+            episodeFile.Episodes = parsedEpisode.Episodes;
 
             //Todo: We shouldn't actually import the file until we confirm its the only one we want.
             //Todo: Separate episodeFile creation from importing (pass file to import to import)
@@ -108,7 +112,7 @@ namespace NzbDrone.Core.Providers
             return episodeFile;
         }
 
-        public virtual string[] GetVideoFiles(string path, bool allDirectories = true)
+        public string[] GetVideoFiles(string path, bool allDirectories = true)
         {
             Logger.Debug("Scanning '{0}' for video files", path);
 
@@ -120,5 +124,24 @@ namespace NzbDrone.Core.Providers
             Logger.Trace("{0} video files were found in {1}", mediaFileList.Count, path);
             return mediaFileList.ToArray();
         }
+
+        public void Execute(DiskScanCommand message)
+        {
+            var seriesToScan = new List<Series>();
+
+            if (message.SeriesId.HasValue)
+            {
+                seriesToScan.Add(_seriesService.GetSeries(message.SeriesId.Value));
+            }
+            else
+            {
+                seriesToScan.AddRange(_seriesService.GetAllSeries());
+            }
+
+            foreach (var series in seriesToScan)
+            {
+                Scan(series);
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/NzbDrone.Core/MediaFiles/EpisodeFile.cs
index 72c7d1598..b95b3e1e5 100644
--- a/NzbDrone.Core/MediaFiles/EpisodeFile.cs
+++ b/NzbDrone.Core/MediaFiles/EpisodeFile.cs
@@ -6,20 +6,6 @@ namespace NzbDrone.Core.MediaFiles
 {
     public class EpisodeFile : ModelBase
     {
-        public EpisodeFile()
-        {
-
-        }
-
-        public EpisodeFile(EpisodeFile source)
-        {
-            Id = source.Id;
-            SeriesId = source.SeriesId;
-            SeasonNumber = source.SeasonNumber;
-            Path = source.Path;
-            Size = source.Size;
-        }
-
         public int SeriesId { get; set; }
         public int SeasonNumber { get; set; }
         public string Path { get; set; }
diff --git a/NzbDrone.Core/MediaFiles/GhostFileCleanupService.cs b/NzbDrone.Core/MediaFiles/GhostFileCleanupService.cs
deleted file mode 100644
index efbca4182..000000000
--- a/NzbDrone.Core/MediaFiles/GhostFileCleanupService.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System;
-using NLog;
-using NzbDrone.Common;
-
-namespace NzbDrone.Core.MediaFiles
-{
-    public interface ICleanGhostFiles
-    {
-        void RemoveNonExistingFiles(int seriesId);
-    }
-
-
-    public class GhostFileCleanupService : ICleanGhostFiles
-    {
-        private readonly IMediaFileService _mediaFileService;
-        private readonly IDiskProvider _diskProvider;
-        private readonly Logger _logger;
-
-        public GhostFileCleanupService(IMediaFileService mediaFileService, IDiskProvider diskProvider, Logger logger)
-        {
-            _mediaFileService = mediaFileService;
-            _diskProvider = diskProvider;
-            _logger = logger;
-        }
-
-        public void RemoveNonExistingFiles(int seriesId)
-        {
-            var seriesFile = _mediaFileService.GetFilesBySeries(seriesId);
-
-            foreach (var episodeFile in seriesFile)
-            {
-                try
-                {
-                    if (!_diskProvider.FileExists(episodeFile.Path))
-                    {
-                        _logger.Trace("File [{0}] no longer exists on disk. removing from db", episodeFile.Path);
-                        _mediaFileService.Delete(episodeFile);
-                    }
-                }
-                catch (Exception ex)
-                {
-                    var message = String.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id);
-                    _logger.ErrorException(message, ex);
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs
new file mode 100644
index 000000000..949f88679
--- /dev/null
+++ b/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Linq;
+using NLog;
+using NzbDrone.Common;
+using NzbDrone.Common.Messaging;
+using NzbDrone.Core.MediaFiles.Commands;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Core.MediaFiles
+{
+
+    public class MediaFileTableCleanupService : IExecute<CleanMediaFileDb>
+    {
+        private readonly IMediaFileService _mediaFileService;
+        private readonly IDiskProvider _diskProvider;
+        private readonly IEpisodeService _episodeService;
+        private readonly Logger _logger;
+
+        public MediaFileTableCleanupService(IMediaFileService mediaFileService, IDiskProvider diskProvider, IEpisodeService episodeService, Logger logger)
+        {
+            _mediaFileService = mediaFileService;
+            _diskProvider = diskProvider;
+            _episodeService = episodeService;
+            _logger = logger;
+        }
+
+        public void Execute(CleanMediaFileDb message)
+        {
+            var seriesFile = _mediaFileService.GetFilesBySeries(message.SeriesId);
+
+            foreach (var episodeFile in seriesFile)
+            {
+                try
+                {
+                    if (!_diskProvider.FileExists(episodeFile.Path))
+                    {
+                        _logger.Trace("File [{0}] no longer exists on disk. removing from db", episodeFile.Path);
+                        _mediaFileService.Delete(episodeFile);
+                    }
+
+                    if (!_episodeService.GetEpisodesByFileId(episodeFile.Id).Any())
+                    {
+                        _logger.Trace("File [{0}] is not assigned to any episodes. removing from db", episodeFile.Path);
+                        _mediaFileService.Delete(episodeFile);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    var errorMessage = String.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id);
+                    _logger.ErrorException(errorMessage, ex);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj
index 0de65433b..08de40cd8 100644
--- a/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/NzbDrone.Core/NzbDrone.Core.csproj
@@ -247,7 +247,9 @@
     <Compile Include="Indexers\RssSyncCommand.cs" />
     <Compile Include="Jobs\TaskManager.cs" />
     <Compile Include="Lifecycle\ApplicationShutdownRequested.cs" />
+    <Compile Include="MediaFiles\Commands\CleanMediaFileDb.cs" />
     <Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
+    <Compile Include="MediaFiles\Commands\DiskScanCommand.cs" />
     <Compile Include="MediaFiles\Events\EpisodeDownloadedEvent.cs" />
     <Compile Include="Download\EpisodeGrabbedEvent.cs" />
     <Compile Include="Download\SeriesRenamedEvent.cs" />
@@ -296,7 +298,7 @@
     <Compile Include="MediaFiles\EpisodeFileMovingService.cs" />
     <Compile Include="MediaFiles\Events\EpisodeFileAddedEvent.cs" />
     <Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
-    <Compile Include="MediaFiles\GhostFileCleanupService.cs" />
+    <Compile Include="MediaFiles\MediaFileTableCleanupService.cs" />
     <Compile Include="MediaFiles\MediaFileRepository.cs" />
     <Compile Include="MetadataSource\IProvideEpisodeInfo.cs" />
     <Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
@@ -404,7 +406,7 @@
     <Compile Include="DecisionEngine\Specifications\RetentionSpecification.cs">
       <SubType>Code</SubType>
     </Compile>
-    <Compile Include="Providers\IDiskScanService.cs">
+    <Compile Include="MediaFiles\DiskScanService.cs">
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="Download\Clients\BlackholeProvider.cs">
diff --git a/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs b/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs
index d2209c324..6fd7eca30 100644
--- a/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs
+++ b/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Providers.Converting
             var outputFile = _configService.GetValue("iPodConvertDir", "");
 
             var handBrakePreset = _configService.GetValue("HandBrakePreset", "iPhone & iPod Touch");
-            var handBrakeCommand = String.Format("-i \"{0}\" -o \"{1}\" --preset=\"{2}\"", episode.EpisodeFile.Path, outputFile, handBrakePreset);
+            var handBrakeCommand = String.Format("-i \"{0}\" -o \"{1}\" --preset=\"{2}\"", episode.EpisodeFile.Value.Path, outputFile, handBrakePreset);
             var handBrakeFile = @"C:\Program Files (x86)\Handbrake\HandBrakeCLI.exe";
 
             try
diff --git a/NzbDrone.Core/Providers/MisnamedProvider.cs b/NzbDrone.Core/Providers/MisnamedProvider.cs
index bb80443a4..fb20010c1 100644
--- a/NzbDrone.Core/Providers/MisnamedProvider.cs
+++ b/NzbDrone.Core/Providers/MisnamedProvider.cs
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Providers
 
             var misnamedFilesSelect = episodesWithFiles.AsParallel().Where(
                 w =>
-                w.First().EpisodeFile.Path !=
+                w.First().EpisodeFile.Value.Path !=
                 _buildFileNames.BuildFilename(w.Select(e => e).ToList(), w.First().Series, w.First().EpisodeFile)).Skip(Math.Max(pageSize * (pageNumber - 1), 0)).Take(pageSize);
 
             //Process the episodes
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Providers
                     var properName = _buildFileNames.BuildFilename(episodes, firstEpisode.Series,
                                                                    firstEpisode.EpisodeFile);
 
-                    var currentName = Path.GetFileNameWithoutExtension(firstEpisode.EpisodeFile.Path);
+                    var currentName = Path.GetFileNameWithoutExtension(firstEpisode.EpisodeFile.Value.Path);
 
                     if (properName != currentName)
                     {
diff --git a/NzbDrone.Core/Tv/Episode.cs b/NzbDrone.Core/Tv/Episode.cs
index cce3045b7..cb70473bb 100644
--- a/NzbDrone.Core/Tv/Episode.cs
+++ b/NzbDrone.Core/Tv/Episode.cs
@@ -1,4 +1,5 @@
 using System;
+using Marr.Data;
 using NzbDrone.Core.Datastore;
 using NzbDrone.Core.MediaFiles;
 using NzbDrone.Core.Model;
@@ -18,38 +19,21 @@ namespace NzbDrone.Core.Tv
 
         public string Overview { get; set; }
         public Boolean Ignored { get; set; }
-        public PostDownloadStatusType PostDownloadStatus { get; set; }
         public Nullable<Int32> AbsoluteEpisodeNumber { get; set; }
         public int SceneSeasonNumber { get; set; }
         public int SceneEpisodeNumber { get; set; }
-        public DateTime? GrabDate { get; set; }
 
         public bool HasFile
         {
-            get { return EpisodeFile != null; }
+            get { return EpisodeFileId != 0; }
         }
-        
+
         public EpisodeStatuses Status
         {
             get
             {
                 if (HasFile) return EpisodeStatuses.Ready;
 
-                if (GrabDate != null)
-                {
-                    if (PostDownloadStatus == PostDownloadStatusType.Unpacking)
-                        return EpisodeStatuses.Unpacking;
-
-                    if (PostDownloadStatus == PostDownloadStatusType.Failed)
-                        return EpisodeStatuses.Failed;
-
-                    if (GrabDate.Value.AddDays(1) >= DateTime.Now)
-                        return EpisodeStatuses.Downloading;
-                }
-
-                if (GrabDate != null && GrabDate.Value.AddDays(1) >= DateTime.Now)
-                    return EpisodeStatuses.Downloading;
-
                 if (AirDate != null && AirDate.Value.Date == DateTime.Today)
                     return EpisodeStatuses.AirsToday;
 
@@ -75,7 +59,7 @@ namespace NzbDrone.Core.Tv
 
         public Series Series { get; set; }
 
-        public EpisodeFile EpisodeFile { get; set; }
+        public LazyLoaded<EpisodeFile> EpisodeFile { get; set; }
 
         public override string ToString()
         {
diff --git a/NzbDrone.Core/Tv/EpisodeRepository.cs b/NzbDrone.Core/Tv/EpisodeRepository.cs
index 9d5727caf..3372960a4 100644
--- a/NzbDrone.Core/Tv/EpisodeRepository.cs
+++ b/NzbDrone.Core/Tv/EpisodeRepository.cs
@@ -1,10 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Diagnostics;
 using System.Linq;
-using FluentMigrator.Runner;
 using Marr.Data;
 using Marr.Data.QGen;
 using NzbDrone.Common.Messaging;
@@ -26,7 +22,6 @@ namespace NzbDrone.Core.Tv
         List<Episode> EpisodesWithFiles();
         List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate);
         void SetIgnoreFlat(Episode episode, bool ignoreFlag);
-        void SetPostDownloadStatus(int episodeId, PostDownloadStatusType status);
         void SetFileId(int episodeId, int fileId);
     }
 
@@ -62,7 +57,7 @@ namespace NzbDrone.Core.Tv
 
         public List<Episode> GetEpisodeByFileId(int fileId)
         {
-            return Query.Where(s => s.EpisodeFile != null && s.EpisodeFile.Id == fileId).ToList();
+            return Query.Where(e => e.EpisodeFileId == fileId).ToList();
         }
 
         public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials)
@@ -74,7 +69,7 @@ namespace NzbDrone.Core.Tv
             {
                 startingSeasonNumber = 0;
             }
-            
+
             var pagingQuery = Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
                                       .Where(e => e.EpisodeFileId == 0)
                                       .AndWhere(e => e.SeasonNumber >= startingSeasonNumber)
@@ -98,7 +93,7 @@ namespace NzbDrone.Core.Tv
 
         public List<Episode> EpisodesWithFiles()
         {
-            return Query.Where(s => s.EpisodeFile != null).ToList();
+            return Query.Where(s => s.EpisodeFileId != 0).ToList();
         }
 
         public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate)
@@ -113,11 +108,6 @@ namespace NzbDrone.Core.Tv
             SetFields(episode, p => p.Ignored);
         }
 
-        public void SetPostDownloadStatus(int episodeId, PostDownloadStatusType status)
-        {
-            SetFields(new Episode { Id = episodeId, PostDownloadStatus = status }, episode => episode.PostDownloadStatus);
-        }
-
         public void SetFileId(int episodeId, int fileId)
         {
             SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId);
diff --git a/NzbDrone.Core/Tv/EpisodeService.cs b/NzbDrone.Core/Tv/EpisodeService.cs
index ca69afbde..c3e9f0549 100644
--- a/NzbDrone.Core/Tv/EpisodeService.cs
+++ b/NzbDrone.Core/Tv/EpisodeService.cs
@@ -29,16 +29,14 @@ namespace NzbDrone.Core.Tv
         List<int> GetEpisodeNumbersBySeason(int seriesId, int seasonNumber);
         void SetEpisodeIgnore(int episodeId, bool isIgnored);
         bool IsFirstOrLastEpisodeOfSeason(int episodeId);
-        void SetPostDownloadStatus(List<int> episodeIds, PostDownloadStatusType postDownloadStatus);
         void UpdateEpisodes(List<Episode> episodes);
         List<Episode> EpisodesBetweenDates(DateTime start, DateTime end);
     }
 
     public class EpisodeService : IEpisodeService,
-        IHandle<EpisodeGrabbedEvent>,
         IHandle<EpisodeFileDeletedEvent>,
         IHandle<EpisodeFileAddedEvent>,
-    IHandleAsync<SeriesDeletedEvent>,
+        IHandleAsync<SeriesDeletedEvent>,
         IHandleAsync<SeriesAddedEvent>
     {
 
@@ -166,7 +164,7 @@ namespace NzbDrone.Core.Tv
                         episodeToUpdate.EpisodeFileId > 0)
                     {
                         logger.Info("Unlinking episode file because TheTVDB changed the episode number...");
-                        episodeToUpdate.EpisodeFile = null;
+                        episodeToUpdate.EpisodeFileId = 0;
                     }
 
                     episodeToUpdate.SeriesId = series.Id;
@@ -268,22 +266,6 @@ namespace NzbDrone.Core.Tv
             return false;
         }
 
-        public void SetPostDownloadStatus(List<int> episodeIds, PostDownloadStatusType postDownloadStatus)
-        {
-            if (episodeIds.Count == 0) throw new ArgumentException("episodeIds should contain one or more episode ids.");
-
-
-            foreach (var episodeId in episodeIds)
-            {
-                var episode = _episodeRepository.Get(episodeId);
-                episode.PostDownloadStatus = postDownloadStatus;
-                _episodeRepository.Update(episode);
-            }
-
-
-            logger.Trace("Updating PostDownloadStatus for {0} episode(s) to {1}", episodeIds.Count, postDownloadStatus);
-        }
-
         public void UpdateEpisodes(List<Episode> episodes)
         {
             _episodeRepository.UpdateMany(episodes);
@@ -296,16 +278,6 @@ namespace NzbDrone.Core.Tv
             return LinkSeriesToEpisodes(episodes);
         }
 
-        public void Handle(EpisodeGrabbedEvent message)
-        {
-            foreach (var episode in message.Episode.Episodes)
-            {
-                logger.Trace("Marking episode {0} as fetched.", episode.Id);
-                episode.GrabDate = DateTime.UtcNow;
-                _episodeRepository.Update(episode);
-            }
-        }
-
         public void HandleAsync(SeriesDeletedEvent message)
         {
             var episodes = GetEpisodeBySeries(message.Series.Id);
@@ -317,10 +289,8 @@ namespace NzbDrone.Core.Tv
             foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id))
             {
                 _logger.Trace("Detaching episode {0} from file.", episode.Id);
-                episode.EpisodeFile = null;
+                episode.EpisodeFileId = 0;
                 episode.Ignored = _configService.AutoIgnorePreviouslyDownloadedEpisodes;
-                episode.GrabDate = null;
-                episode.PostDownloadStatus = PostDownloadStatusType.Unknown;
                 UpdateEpisode(episode);
             }
         }
@@ -335,7 +305,6 @@ namespace NzbDrone.Core.Tv
             foreach (var episode in message.EpisodeFile.Episodes.Value)
             {
                 _episodeRepository.SetFileId(episode.Id, message.EpisodeFile.Id);
-                _episodeRepository.SetPostDownloadStatus(episode.Id, PostDownloadStatusType.NoError);
                 _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.Path, episode);
             }
         }
diff --git a/NzbDrone/NLog.config b/NzbDrone/NLog.config
index 23f2d521e..6ed2d7f88 100644
--- a/NzbDrone/NLog.config
+++ b/NzbDrone/NLog.config
@@ -25,7 +25,7 @@
             layout="${date:format=yy-M-d HH\:mm\:ss.f}|${logger}}|${level}|${message}|${exception:format=ToString}"/>
   </targets>
   <rules>
-    <logger name="*" minlevel="Debug" writeTo="consoleLogger"/>
+    <logger name="*" minlevel="Trace" writeTo="consoleLogger"/>
     <logger name="*" minlevel="Off" writeTo="udpTarget"/>
     <logger name="*" minlevel="Warn" writeTo="rollingFileLogger"/>
   </rules>
diff --git a/UI/Series/Index/SeriesIndexLayout.js b/UI/Series/Index/SeriesIndexLayout.js
index 7f0c19700..018aadabc 100644
--- a/UI/Series/Index/SeriesIndexLayout.js
+++ b/UI/Series/Index/SeriesIndexLayout.js
@@ -156,7 +156,7 @@ define([
                         {
                             title         : 'Update Library',
                             icon          : 'icon-refresh',
-                            command       : 'updatelibrary',
+                            command       : 'diskscan',
                             successMessage: 'Library was updated!',
                             errorMessage  : 'Library update failed!'
                         },