diff --git a/NzbDrone.Core/Parser.cs b/NzbDrone.Core/Parser.cs
new file mode 100644
index 000000000..e260a56ca
--- /dev/null
+++ b/NzbDrone.Core/Parser.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using NLog;
+using NzbDrone.Core.Model;
+using NzbDrone.Core.Repository.Quality;
+
+namespace NzbDrone.Core
+{
+ public static class Parser
+ {
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private static readonly Regex[] ReportTitleRegex = new[]
+ {
+ new Regex(@"^(?
.+?)?\W?(?\d{4}?)?\W+(?\d{4})\W+(?\d{2})\W+(?\d{2})\W?(?!\\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ new Regex(@"^(?.*?)?(?:\W?S?(?\d{1,2}(?!\d+))(?:(?:\-|\.|[ex]|\s|to)+(?\d{1,2}(?!\d+)))+)+\W?(?!\\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ new Regex(@"^(?.+?)?\W?(?\d{4}?)?(?:\W(?\d+)(?\d{2}))+\W?(?!\\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ //Supports 103/113 naming
+ new Regex(@"^(?.*?)?(?:\W?S?(?\d{1,2}(?!\d+))(?:(?:\-|\.|[ex]|\s|to)+(?\d+))+)+\W?(?!\\)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled)
+ };
+
+ private static readonly Regex[] SeasonReportTitleRegex = new[]
+ {
+ new Regex(
+ @"(?.+?)?\W?(?\d{4}?)?\W(?:S|Season)?\W?(?\d+)(?!\\)",
+ RegexOptions.IgnoreCase |
+ RegexOptions.Compiled),
+ };
+
+ private static readonly Regex NormalizeRegex = new Regex(@"((^|\W)(a|an|the|and|or|of)($|\W))|\W|\b(?!(?:19\d{2}|20\d{2}))\d+\b",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ ///
+ /// Parses a post title into list of episodes it contains
+ ///
+ /// Title of the report
+ /// List of episodes contained to the post
+ internal static EpisodeParseResult ParseEpisodeInfo(string title)
+ {
+ Logger.Trace("Parsing string '{0}'", title);
+
+ foreach (var regex in ReportTitleRegex)
+ {
+ var simpleTitle = Regex.Replace(title, @"480[i|p]|720[i|p]|1080[i|p]|[x|h]264", String.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ var match = regex.Matches(simpleTitle);
+
+ if (match.Count != 0)
+ {
+ var seriesName = NormalizeTitle(match[0].Groups["title"].Value);
+
+ var airyear = 0;
+ Int32.TryParse(match[0].Groups["airyear"].Value, out airyear);
+
+ EpisodeParseResult parsedEpisode;
+
+ if (airyear < 1 )
+ {
+ var season = 0;
+ Int32.TryParse(match[0].Groups["season"].Value, out season);
+
+ parsedEpisode = new EpisodeParseResult
+ {
+ Proper = title.ToLower().Contains("proper"),
+ CleanTitle = seriesName,
+ SeasonNumber = season,
+ Episodes = new List()
+ };
+
+ foreach (Match matchGroup in match)
+ {
+ var count = matchGroup.Groups["episode"].Captures.Count;
+ var first = Convert.ToInt32(matchGroup.Groups["episode"].Captures[0].Value);
+ var last = Convert.ToInt32(matchGroup.Groups["episode"].Captures[count - 1].Value);
+
+ for (int i = first; i <= last; i++)
+ {
+ parsedEpisode.Episodes.Add(i);
+ }
+ }
+ }
+
+ else
+ {
+ //Try to Parse as a daily show
+ if (airyear > 0)
+ {
+ var airmonth = Convert.ToInt32(match[0].Groups["airmonth"].Value);
+ var airday = Convert.ToInt32(match[0].Groups["airday"].Value);
+
+ parsedEpisode = new EpisodeParseResult
+ {
+ Proper = title.ToLower().Contains("proper"),
+ CleanTitle = seriesName,
+ AirDate = new DateTime(airyear, airmonth, airday)
+ };
+ }
+
+ //Something went wrong with this one... return null
+ else
+ return null;
+ }
+
+ parsedEpisode.Quality = ParseQuality(title);
+
+ Logger.Trace("Episode Parsed. {0}", parsedEpisode);
+
+ return parsedEpisode;
+ }
+ }
+ Logger.Debug("Unable to parse text into episode info. {0}", title);
+ return null;
+ }
+
+ ///
+ /// Parses a post title into season it contains
+ ///
+ /// Title of the report
+ /// Season information contained in the post
+ internal static SeasonParseResult ParseSeasonInfo(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);
+ int year;
+ Int32.TryParse(match[0].Groups["year"].Value, out year);
+
+ if (year < 1900 || year > DateTime.Now.Year + 1)
+ {
+ year = 0;
+ }
+
+ var seasonNumber = Convert.ToInt32(match[0].Groups["season"].Value);
+
+ var result = new SeasonParseResult
+ {
+ SeriesTitle = seriesName,
+ SeasonNumber = seasonNumber,
+ Year = year,
+ Quality = ParseQuality(title)
+ };
+
+
+ Logger.Trace("Season Parsed. {0}", result);
+ return result;
+ }
+ }
+
+ return null; //Return null
+ }
+
+ ///
+ /// 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);
+
+ Logger.Trace("Series Parsed. {0}", seriesName);
+ return seriesName;
+ }
+ }
+
+ return String.Empty;
+ }
+
+ ///
+ /// Parses proper status out of a report title
+ ///
+ /// Title of the report
+ ///
+ internal static bool ParseProper(string title)
+ {
+ return title.ToLower().Contains("proper");
+ }
+
+ internal static QualityTypes ParseQuality(string name)
+ {
+ Logger.Trace("Trying to parse quality for {0}", name);
+
+ var result = QualityTypes.Unknown;
+ name = name.ToLowerInvariant();
+
+ if (name.Contains("dvd"))
+ return QualityTypes.DVD;
+
+ if (name.Contains("bdrip") || name.Contains("brrip"))
+ {
+ return QualityTypes.BDRip;
+ }
+
+ if (name.Contains("xvid") || name.Contains("divx"))
+ {
+ if (name.Contains("bluray"))
+ {
+ return QualityTypes.BDRip;
+ }
+
+ return QualityTypes.TV;
+ }
+
+ if (name.Contains("bluray"))
+ {
+ if (name.Contains("720p"))
+ return QualityTypes.Bluray720;
+
+ if (name.Contains("1080p"))
+ return QualityTypes.Bluray1080;
+
+ return QualityTypes.Bluray720;
+ }
+ if (name.Contains("web-dl"))
+ return QualityTypes.WEBDL;
+ if (name.Contains("x264") || name.Contains("h264") || name.Contains("720p"))
+ return QualityTypes.HDTV;
+
+ //Based on extension
+ if (result == QualityTypes.Unknown)
+ {
+ switch (new FileInfo(name).Extension.ToLower())
+ {
+ case ".avi":
+ case ".xvid":
+ case ".wmv":
+ {
+ result = QualityTypes.TV;
+ break;
+ }
+ case ".mkv":
+ {
+ result = QualityTypes.HDTV;
+ break;
+ }
+ }
+ }
+
+ Logger.Trace("Quality Parsed:{0} Title:", result, name);
+ return result;
+ }
+
+ ///
+ /// Normalizes the title. removing all non-word characters as well as common tokens
+ /// such as 'the' and 'and'
+ ///
+ /// title
+ ///
+ public static string NormalizeTitle(string title)
+ {
+ return NormalizeRegex.Replace(title, String.Empty).ToLower();
+ }
+
+
+ public static string NormalizePath(string path)
+ {
+ if (String.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("Path can not be null or empty");
+
+ var info = new FileInfo(path);
+
+ if (info.FullName.StartsWith(@"\\")) //UNC
+ {
+ return info.FullName.TrimEnd('/', '\\', ' ');
+ }
+
+ return info.FullName.Trim('/', '\\', ' ');
+ }
+ }
+}
\ No newline at end of file