diff --git a/NzbDrone.Core/Helpers/EpisodeRenameHelper.cs b/NzbDrone.Core/Helpers/EpisodeRenameHelper.cs new file mode 100644 index 000000000..3879893ae --- /dev/null +++ b/NzbDrone.Core/Helpers/EpisodeRenameHelper.cs @@ -0,0 +1,37 @@ +using System; +using NzbDrone.Core.Model; + +namespace NzbDrone.Core.Helpers +{ + public static class EpisodeRenameHelper + { + public static string GetNewName(EpisodeRenameModel erm) + { + //Todo: Get the users preferred naming convention instead of hard-coding it + + if (erm.EpisodeFile.Episodes.Count == 1) + { + return String.Format("{0} - S{1:00}E{2:00} - {3}", erm.SeriesName, + erm.EpisodeFile.Episodes[0].SeasonNumber, erm.EpisodeFile.Episodes[0].EpisodeNumber, + erm.EpisodeFile.Episodes[0].Title); + } + + var epNumberString = String.Empty; + var epNameString = String.Empty; + + foreach (var episode in erm.EpisodeFile.Episodes) + { + epNumberString = epNumberString + String.Format("E{0:00}", episode.EpisodeNumber); + epNameString = epNameString + String.Format("+ {0}", episode.Title).Trim(' ', '+'); + } + + return String.Format("{0} - S{1:00}E{2} - {3}", erm.SeriesName, erm.EpisodeFile.Episodes[0].SeasonNumber, + epNumberString, epNameString); + } + + public static string GetSeasonFolder(int seasonNumber, string seasonFolderFormat) + { + return seasonFolderFormat.Replace("%s", seasonNumber.ToString()).Replace("%0s", seasonNumber.ToString("00")); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/EpisodeSortingHelper.cs b/NzbDrone.Core/Helpers/EpisodeSortingHelper.cs similarity index 97% rename from NzbDrone.Core/EpisodeSortingHelper.cs rename to NzbDrone.Core/Helpers/EpisodeSortingHelper.cs index 16b7f2c5a..9ccb95ae9 100644 --- a/NzbDrone.Core/EpisodeSortingHelper.cs +++ b/NzbDrone.Core/Helpers/EpisodeSortingHelper.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; using NzbDrone.Core.Model; -namespace NzbDrone.Core +namespace NzbDrone.Core.Helpers { public static class EpisodeSortingHelper { diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 48435cf3e..29c60a616 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -156,7 +156,8 @@ - + + diff --git a/NzbDrone.Core/Parser.cs b/NzbDrone.Core/Parser.cs index 3275f0556..42c07c880 100644 --- a/NzbDrone.Core/Parser.cs +++ b/NzbDrone.Core/Parser.cs @@ -71,6 +71,38 @@ namespace NzbDrone.Core return result; } + /// + /// Parses a post title to find the series that relates to it + /// + /// Title of the report + /// Normalized Series Name + internal static string ParseSeriesName(string title) + { + Logger.Trace("Parsing string '{0}'", title); + + foreach (var regex in ReportTitleRegex) + { + var match = regex.Matches(title); + + if (match.Count != 0) + { + var seriesName = NormalizeTitle(match[0].Groups["title"].Value); + var year = 0; + Int32.TryParse(match[0].Groups["year"].Value, out year); + + if (year < 1900 || year > DateTime.Now.Year + 1) + { + year = 0; + } + + Logger.Trace("Series Parsed. {0}", seriesName); + return seriesName; + } + } + + return String.Empty; + } + /// /// Parses proper status out of a report title /// diff --git a/NzbDrone.Core/Providers/DiskProvider.cs b/NzbDrone.Core/Providers/DiskProvider.cs index 55ff77fea..e87263262 100644 --- a/NzbDrone.Core/Providers/DiskProvider.cs +++ b/NzbDrone.Core/Providers/DiskProvider.cs @@ -29,7 +29,9 @@ namespace NzbDrone.Core.Providers public long GetSize(string path) { - return new FileInfo(path).Length; + var fi = new FileInfo(path); + return fi.Length; + //return new FileInfo(path).Length; } public String CreateDirectory(string path) diff --git a/NzbDrone.Core/Providers/IMediaFileProvider.cs b/NzbDrone.Core/Providers/IMediaFileProvider.cs index d7479c52f..608637d06 100644 --- a/NzbDrone.Core/Providers/IMediaFileProvider.cs +++ b/NzbDrone.Core/Providers/IMediaFileProvider.cs @@ -10,8 +10,8 @@ namespace NzbDrone.Core.Providers /// Scans the specified series folder for media files /// /// The series to be scanned - void Scan(Series series); - + List Scan(Series series); + List Scan(Series series, string path); EpisodeFile ImportFile(Series series, string filePath); string GenerateEpisodePath(EpisodeModel episode); void CleanUp(List files); diff --git a/NzbDrone.Core/Providers/IPostProcessingProvider.cs b/NzbDrone.Core/Providers/IPostProcessingProvider.cs new file mode 100644 index 000000000..68b869cb6 --- /dev/null +++ b/NzbDrone.Core/Providers/IPostProcessingProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Providers +{ + public interface IPostProcessingProvider + { + void ProcessEpisode(string dir, string nzbName); + } +} diff --git a/NzbDrone.Core/Providers/IRenameProvider.cs b/NzbDrone.Core/Providers/IRenameProvider.cs index b45280860..a3f691c7c 100644 --- a/NzbDrone.Core/Providers/IRenameProvider.cs +++ b/NzbDrone.Core/Providers/IRenameProvider.cs @@ -11,5 +11,6 @@ namespace NzbDrone.Core.Providers void RenameSeries(int seriesId); void RenameSeason(int seasonId); void RenameEpisode(int episodeId); + void RenameEpisodeFile(int episodeFileId); } } diff --git a/NzbDrone.Core/Providers/MediaFileProvider.cs b/NzbDrone.Core/Providers/MediaFileProvider.cs index 3e2d58bd4..592d96ba5 100644 --- a/NzbDrone.Core/Providers/MediaFileProvider.cs +++ b/NzbDrone.Core/Providers/MediaFileProvider.cs @@ -21,10 +21,10 @@ namespace NzbDrone.Core.Providers private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly string[] MediaExtentions = new[] { "*.mkv", "*.avi", "*.wmv" }; - public MediaFileProvider(IRepository repository, IConfigProvider _configProvider, IDiskProvider diskProvider, IEpisodeProvider episodeProvider) + public MediaFileProvider(IRepository repository, IConfigProvider configProvider, IDiskProvider diskProvider, IEpisodeProvider episodeProvider) { _repository = repository; - this._configProvider = _configProvider; + _configProvider = configProvider; _diskProvider = diskProvider; _episodeProvider = episodeProvider; } @@ -33,14 +33,36 @@ namespace NzbDrone.Core.Providers /// Scans the specified series folder for media files /// /// The series to be scanned - public void Scan(Series series) + public List Scan(Series series) { var mediaFileList = GetMediaFileList(series.Path); + var fileList = new List(); foreach (var filePath in mediaFileList) { - ImportFile(series, filePath); + var file = ImportFile(series, filePath); + if (file != null) + fileList.Add(file); } + return fileList; + } + + /// + /// Scans the specified series folder for media files + /// + /// The series to be scanned + public List Scan(Series series, string path) + { + var mediaFileList = GetMediaFileList(path); + var fileList = new List(); + + foreach (var filePath in mediaFileList) + { + var file = ImportFile(series, filePath); + if (file != null) + fileList.Add(file); + } + return fileList; } public EpisodeFile ImportFile(Series series, string filePath) @@ -72,11 +94,20 @@ namespace NzbDrone.Core.Providers if (episodes.Count < 1) return null; + var size = _diskProvider.GetSize(filePath); + + //If Size is less than 50MB and contains sample. Check for Size to ensure its not an episode with sample in the title + if (size < 50000000 && filePath.ToLower().Contains("sample")) + { + Logger.Trace("[{0}] appears to be a sample... skipping.", filePath); + return null; + } + var episodeFile = new EpisodeFile(); episodeFile.DateAdded = DateTime.Now; episodeFile.SeriesId = series.SeriesId; episodeFile.Path = Parser.NormalizePath(filePath); - episodeFile.Size = _diskProvider.GetSize(filePath); + episodeFile.Size = size; episodeFile.Quality = Parser.ParseQuality(filePath); episodeFile.Proper = Parser.ParseProper(filePath); var fileId = (int)_repository.Add(episodeFile); @@ -167,6 +198,5 @@ namespace NzbDrone.Core.Providers Logger.Trace("{0} media files were found in {1}", mediaFileList.Count, path); return mediaFileList; } - } } diff --git a/NzbDrone.Core/Providers/PostProcessingProvider.cs b/NzbDrone.Core/Providers/PostProcessingProvider.cs new file mode 100644 index 000000000..103303b8e --- /dev/null +++ b/NzbDrone.Core/Providers/PostProcessingProvider.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using NzbDrone.Core.Helpers; + +namespace NzbDrone.Core.Providers +{ + public class PostProcessingProvider : IPostProcessingProvider + { + private readonly ISeriesProvider _seriesProvider; + private readonly IConfigProvider _configProvider; + private readonly IMediaFileProvider _mediaFileProvider; + private readonly IRenameProvider _renameProvider; + + public PostProcessingProvider(ISeriesProvider seriesProvider, IConfigProvider configProvider, + IMediaFileProvider mediaFileProvider, IRenameProvider renameProvider) + { + _seriesProvider = seriesProvider; + _configProvider = configProvider; + _mediaFileProvider = mediaFileProvider; + _renameProvider = renameProvider; + } + + #region IPostProcessingProvider Members + + public void ProcessEpisode(string dir, string nzbName) + { + var parsedSeries = Parser.ParseSeriesName(nzbName); + var series = _seriesProvider.FindSeries(parsedSeries); + + if (series == null) + return; + + //Import the files, and then rename the newly added ones. + var fileList = _mediaFileProvider.Scan(series, dir); + + foreach (var file in fileList) + { + //Todo: Where should we handle XBMC notifying/library updating etc? RenameProvider seems like a likely place, since we want to update XBMC after renaming (might as well) + _renameProvider.RenameEpisodeFile(file.EpisodeFileId); + } + } + + #endregion + } +} diff --git a/NzbDrone.Core/Providers/RenameProvider.cs b/NzbDrone.Core/Providers/RenameProvider.cs index acf539d06..ec319b4ba 100644 --- a/NzbDrone.Core/Providers/RenameProvider.cs +++ b/NzbDrone.Core/Providers/RenameProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading; using NLog; +using NzbDrone.Core.Helpers; using NzbDrone.Core.Model; using NzbDrone.Core.Repository; @@ -48,7 +49,7 @@ namespace NzbDrone.Core.Providers erm.Folder = series.Path; if (series.SeasonFolder) - erm.Folder += Path.DirectorySeparatorChar + GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber); + erm.Folder += Path.DirectorySeparatorChar + EpisodeRenameHelper.GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber, _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true)); erm.EpisodeFile = episodeFile; _epsToRename.Add(erm); @@ -70,7 +71,7 @@ namespace NzbDrone.Core.Providers erm.Folder = series.Path; if (series.SeasonFolder) - erm.Folder += Path.DirectorySeparatorChar + GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber); + erm.Folder += Path.DirectorySeparatorChar + EpisodeRenameHelper.GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber, _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true)); erm.EpisodeFile = episodeFile; _epsToRename.Add(erm); @@ -92,7 +93,7 @@ namespace NzbDrone.Core.Providers erm.Folder = series.Path; if (series.SeasonFolder) - erm.Folder += Path.DirectorySeparatorChar + GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber); + erm.Folder += Path.DirectorySeparatorChar + EpisodeRenameHelper.GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber, _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true)); erm.EpisodeFile = episodeFile; _epsToRename.Add(erm); @@ -114,7 +115,26 @@ namespace NzbDrone.Core.Providers erm.Folder = series.Path; if (series.SeasonFolder) - erm.Folder += Path.DirectorySeparatorChar + GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber); + erm.Folder += Path.DirectorySeparatorChar + EpisodeRenameHelper.GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber, _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true)); + + erm.EpisodeFile = episodeFile; + _epsToRename.Add(erm); + StartRename(); + } + + public void RenameEpisodeFile(int episodeFileId) + { + //This will properly rename multi-episode files if asked to rename either of the episode + var episodeFile = _mediaFileProvider.GetEpisodeFile(episodeFileId); + var series = _seriesProvider.GetSeries(episodeFile.Series.SeriesId); + + var erm = new EpisodeRenameModel(); + erm.SeriesName = series.Title; + + erm.Folder = series.Path; + + if (series.SeasonFolder) + erm.Folder += Path.DirectorySeparatorChar + EpisodeRenameHelper.GetSeasonFolder(episodeFile.Episodes[0].SeasonNumber, _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true)); erm.EpisodeFile = episodeFile; _epsToRename.Add(erm); @@ -159,7 +179,7 @@ namespace NzbDrone.Core.Providers { //Update EpisodeFile if successful Logger.Debug("Renaming Episode: {0}", Path.GetFileName(erm.EpisodeFile.Path)); - var newName = GetNewName(erm); + var newName = EpisodeRenameHelper.GetNewName(erm); var ext = Path.GetExtension(erm.EpisodeFile.Path); var newFilename = erm.Folder + Path.DirectorySeparatorChar + newName + ext; @@ -180,36 +200,5 @@ namespace NzbDrone.Core.Providers Logger.Warn("Unable to Rename Episode: {0}", Path.GetFileName(erm.EpisodeFile.Path)); } } - - private string GetNewName(EpisodeRenameModel erm) - { - //Todo: Get the users preferred naming convention instead of hard-coding it - - if (erm.EpisodeFile.Episodes.Count == 1) - { - return String.Format("{0} - S{1:00}E{2:00} - {3}", erm.SeriesName, - erm.EpisodeFile.Episodes[0].SeasonNumber, erm.EpisodeFile.Episodes[0].EpisodeNumber, - erm.EpisodeFile.Episodes[0].Title); - } - - var epNumberString = String.Empty; - var epNameString = String.Empty; - - foreach (var episode in erm.EpisodeFile.Episodes) - { - epNumberString = epNumberString + String.Format("E{0:00}", episode.EpisodeNumber); - epNameString = epNameString + String.Format("+ {0}", episode.Title).Trim(' ', '+'); - } - - return String.Format("{0} - S{1:00}E{2} - {3}", erm.SeriesName, erm.EpisodeFile.Episodes[0].SeasonNumber, - epNumberString, epNameString); - } - - private string GetSeasonFolder(int seasonNumber) - { - return - _configProvider.GetValue("Sorting_SeasonFolderFormat", "Season %s", true).Replace("%s", seasonNumber.ToString()). - Replace("%0s", seasonNumber.ToString("00")); - } } } diff --git a/NzbDrone.PostProcessor/NzbDrone.PostProcessor.csproj b/NzbDrone.PostProcessor/NzbDrone.PostProcessor.csproj new file mode 100644 index 000000000..c2dd7425d --- /dev/null +++ b/NzbDrone.PostProcessor/NzbDrone.PostProcessor.csproj @@ -0,0 +1,52 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {0C679573-736D-4F77-B934-FD8931AC1AA1} + Exe + Properties + NzbDrone.PostProcessor + NzbDrone.PostProcessor + v2.0 + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + \ No newline at end of file diff --git a/NzbDrone.PostProcessor/Program.cs b/NzbDrone.PostProcessor/Program.cs new file mode 100644 index 000000000..ee194c402 --- /dev/null +++ b/NzbDrone.PostProcessor/Program.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NzbDrone.PostProcessor +{ + class Program + { + static void Main(string[] args) + { + } + } +} diff --git a/NzbDrone.PostProcessor/Properties/AssemblyInfo.cs b/NzbDrone.PostProcessor/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d9889c15e --- /dev/null +++ b/NzbDrone.PostProcessor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NzbDrone.PostProcessor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("TIO Networks Corp")] +[assembly: AssemblyProduct("NzbDrone.PostProcessor")] +[assembly: AssemblyCopyright("Copyright © TIO Networks Corp 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6521fcb0-15dc-4324-b08a-f18f87d78859")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/NzbDrone.Web/Controllers/ApiController.cs b/NzbDrone.Web/Controllers/ApiController.cs new file mode 100644 index 000000000..f49290087 --- /dev/null +++ b/NzbDrone.Web/Controllers/ApiController.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Xml.Linq; +using NzbDrone.Core; +using NzbDrone.Core.Providers; + +namespace NzbDrone.Web.Controllers +{ + public class ApiController : Controller + { + private readonly IPostProcessingProvider _postProcessingProvider; + + public ApiController(IPostProcessingProvider postProcessingProvider) + { + _postProcessingProvider = postProcessingProvider; + } + + public ActionResult ProcessEpisode(string dir, string nzbName) + { + _postProcessingProvider.ProcessEpisode(dir, nzbName); + return Content("ok"); + } + } +} diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index be14a241f..597343f16 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -6,6 +6,7 @@ using System.Web; using System.Web.Mvc; using NLog; using NzbDrone.Core; +using NzbDrone.Core.Helpers; using NzbDrone.Core.Model; using NzbDrone.Core.Providers; using NzbDrone.Core.Repository.Quality; diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index fde1b8dea..033ee295f 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -73,6 +73,7 @@ + diff --git a/NzbDrone.sln b/NzbDrone.sln index 0340f1f00..bac82bd7d 100644 --- a/NzbDrone.sln +++ b/NzbDrone.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Web.Tests", "NzbDr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.PostProcessor", "NzbDrone.PostProcessor\NzbDrone.PostProcessor.csproj", "{0C679573-736D-4F77-B934-FD8931AC1AA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,6 +94,18 @@ Global {99CDD5DC-698F-4624-B431-2D6381CE3A15}.Release|Mixed Platforms.Build.0 = Release|Any CPU {99CDD5DC-698F-4624-B431-2D6381CE3A15}.Release|x64.ActiveCfg = Release|Any CPU {99CDD5DC-698F-4624-B431-2D6381CE3A15}.Release|x86.ActiveCfg = Release|Any CPU + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|Any CPU.ActiveCfg = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|x64.ActiveCfg = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|x86.ActiveCfg = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Debug|x86.Build.0 = Debug|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|Any CPU.ActiveCfg = Release|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|Mixed Platforms.Build.0 = Release|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|x64.ActiveCfg = Release|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|x86.ActiveCfg = Release|x86 + {0C679573-736D-4F77-B934-FD8931AC1AA1}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE