Start of AutoConfigureSab

This commit is contained in:
Mark McDowall 2011-04-25 00:42:29 -07:00
parent ea2e520632
commit a34bd818cf
13 changed files with 768 additions and 906 deletions

3
.gitignore vendored
View File

@ -31,4 +31,5 @@ _ReSharper*/
[Ll]ogs/
/[Pp]ackage/
#NZBDrone specific
*.db
*.db
*Web.Publish.xml

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Model
{
public class SabnzbdInfoModel
{
public string ApiKey { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -166,6 +166,8 @@
<Compile Include="Instrumentation\SubsonicTarget.cs" />
<Compile Include="Instrumentation\ExceptioneerTarget.cs" />
<Compile Include="Instrumentation\NlogWriter.cs" />
<Compile Include="Model\SabnzbdInfoModel.cs" />
<Compile Include="Providers\AutoConfigureProvider.cs" />
<Compile Include="Providers\Indexer\NzbMatrixProvider.cs" />
<Compile Include="Providers\Jobs\NewSeriesUpdate.cs" />
<Compile Include="Providers\Jobs\JobProvider.cs" />

View File

@ -1,290 +0,0 @@
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(@"^(?<title>.+?)?\W?(?<year>\d{4}?)?\W+(?<airyear>\d{4})\W+(?<airmonth>\d{2})\W+(?<airday>\d{2})\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
new Regex(@"^(?<title>.*?)?(?:\W?S?(?<season>\d{1,2}(?!\d+))(?:(?:\-|\.|[ex]|\s|to)+(?<episode>\d{1,2}(?!\d+)))+)+\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
new Regex(@"^(?<title>.+?)?\W?(?<year>\d{4}?)?(?:\W(?<season>\d+)(?<episode>\d{2}))+\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 103/113 naming
new Regex(@"^(?<title>.*?)?(?:\W?S?(?<season>\d{1,2}(?!\d+))(?:(?:\-|\.|[ex]|\s|to)+(?<episode>\d+))+)+\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled)
};
private static readonly Regex[] SeasonReportTitleRegex = new[]
{
new Regex(
@"(?<title>.+?)?\W?(?<year>\d{4}?)?\W(?:S|Season)?\W?(?<season>\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);
/// <summary>
/// Parses a post title into list of episodes it contains
/// </summary>
/// <param name = "title">Title of the report</param>
/// <returns>List of episodes contained to the post</returns>
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<int>()
};
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.Warn("Unable to parse text into episode info. {0}", title);
return null;
}
/// <summary>
/// Parses a post title into season it contains
/// </summary>
/// <param name = "title">Title of the report</param>
/// <returns>Season information contained in the post</returns>
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
}
/// <summary>
/// Parses a post title to find the series that relates to it
/// </summary>
/// <param name = "title">Title of the report</param>
/// <returns>Normalized Series Name</returns>
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;
}
/// <summary>
/// Parses proper status out of a report title
/// </summary>
/// <param name = "title">Title of the report</param>
/// <returns></returns>
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;
}
/// <summary>
/// Normalizes the title. removing all non-word characters as well as common tokens
/// such as 'the' and 'and'
/// </summary>
/// <param name = "title">title</param>
/// <returns></returns>
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('/', '\\', ' ');
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NzbDrone.Core.Model;
using NzbDrone.Core.Providers.Core;
namespace NzbDrone.Core.Providers
{
public class AutoConfigureProvider
{
private HttpProvider _httpProvider;
private ConfigProvider _configProvider;
public AutoConfigureProvider(HttpProvider httpProvider, ConfigProvider configProvider)
{
_httpProvider = httpProvider;
_configProvider = configProvider;
}
public SabnzbdInfoModel AutoConfigureSab(string username, string password)
{
//Get Output from Netstat
var netStatOutput = String.Empty;
//var port = GetSabnzbdPort(netStatOutput);
var port = 2222;
var apiKey = GetSabnzbdApiKey(port);
if (port > 0 && !String.IsNullOrEmpty(apiKey))
{
return new SabnzbdInfoModel
{
ApiKey = apiKey,
Port = port,
Username = username,
Password = password
};
}
return null;
}
private int GetSabnzbdPort(string netstatOutput)
{
Regex regex = new Regex(@"^(?:TCP\W+127.0.0.1:(?<port>\d+\W+).+?\r\n\W+\[sabnzbd.exe\])", RegexOptions.IgnoreCase
| RegexOptions.Compiled);
var match = regex.Match(netstatOutput);
var port = 0;
Int32.TryParse(match.Groups["port"].Value, out port);
return port;
}
private string GetSabnzbdApiKey(int port, string ipAddress = "127.0.0.1")
{
var request = String.Format("http://{0}:{1}/config/general/", ipAddress, port);
var result = _httpProvider.DownloadString(request);
Regex regex = new Regex("\\<input\\Wtype\\=\\\"text\\\"\\Wid\\=\\\"apikey\\\"\\Wvalue\\=\\\"(?<apikey>\\w+)\\W", RegexOptions.IgnoreCase
| RegexOptions.Compiled);
var match = regex.Match(result);
return match.Groups["apikey"].Value;
}
}
}

View File

@ -61,13 +61,13 @@ namespace NzbDrone.Core.Providers.Indexer
/// </summary>
public void Fetch()
{
_logger.Info("Fetching feeds from " + Settings.Name);
_logger.Debug("Fetching feeds from " + Settings.Name);
foreach (var url in Urls)
{
try
{
_logger.Debug("Downloading RSS " + url);
_logger.Trace("Downloading RSS " + url);
var feed = SyndicationFeed.Load(_httpProvider.DownloadXml(url)).Items;
foreach (var item in feed)
@ -130,7 +130,7 @@ namespace NzbDrone.Core.Providers.Indexer
{
return;
}
var sabTitle = _sabProvider.GetSabTitle(parseResult);
if (_sabProvider.IsInQueue(sabTitle))

View File

@ -78,7 +78,6 @@ namespace NzbDrone.Core.Providers.Jobs
try
{
Logger.Trace("Getting list of jobs needing to be executed");
var pendingJobs = All().Where(
t => t.Enable &&
@ -114,16 +113,14 @@ namespace NzbDrone.Core.Providers.Jobs
{
if (_isRunning)
{
Logger.Info("Another instance of this job is already running. Ignoring request.");
Logger.Info("Another job is already running. Ignoring request.");
return false;
}
_isRunning = true;
}
Logger.Info("User has requested a manual execution of {0}", jobType.Name);
if (_jobThread == null || !_jobThread.IsAlive)
{
Logger.Debug("Initializing background thread");
Logger.Trace("Initializing background thread");
ThreadStart starter = () =>
{
@ -170,7 +167,7 @@ namespace NzbDrone.Core.Providers.Jobs
{
try
{
Logger.Info("Starting job '{0}'. Last execution {1}", settings.Name, settings.LastExecution);
Logger.Debug("Starting job '{0}'. Last execution {1}", settings.Name, settings.LastExecution);
settings.LastExecution = DateTime.Now;
var sw = Stopwatch.StartNew();
@ -180,7 +177,7 @@ namespace NzbDrone.Core.Providers.Jobs
settings.Success = true;
sw.Stop();
Logger.Info("Job '{0}' successfully completed in {1} seconds", timerClass.Name, sw.Elapsed.Minutes,
Logger.Debug("Job '{0}' successfully completed in {1} seconds", timerClass.Name, sw.Elapsed.Minutes,
sw.Elapsed.Seconds);
}
catch (Exception e)
@ -201,7 +198,7 @@ namespace NzbDrone.Core.Providers.Jobs
/// </summary>
public virtual void Initialize()
{
Logger.Info("Initializing jobs. Count {0}", _jobs.Count());
Logger.Debug("Initializing jobs. Count {0}", _jobs.Count());
var currentTimer = All();
foreach (var timer in _jobs)

View File

@ -24,14 +24,17 @@ namespace NzbDrone.Web.Controllers
private readonly IndexerProvider _indexerProvider;
private readonly QualityProvider _qualityProvider;
private readonly RootDirProvider _rootDirProvider;
private readonly AutoConfigureProvider _autoConfigureProvider;
public SettingsController(ConfigProvider configProvider, IndexerProvider indexerProvider,
QualityProvider qualityProvider, RootDirProvider rootDirProvider)
QualityProvider qualityProvider, RootDirProvider rootDirProvider,
AutoConfigureProvider autoConfigureProvider)
{
_configProvider = configProvider;
_indexerProvider = indexerProvider;
_qualityProvider = qualityProvider;
_rootDirProvider = rootDirProvider;
_autoConfigureProvider = autoConfigureProvider;
}
public ActionResult Index(string viewName)
@ -276,6 +279,30 @@ namespace NzbDrone.Web.Controllers
return new JsonResult { Data = "ok" };
}
public JsonResult AutoConfigureSab(string username, string password)
{
SabnzbdInfoModel info;
try
{
//info = _autoConfigureProvider.AutoConfigureSab(username, password);
info = new SabnzbdInfoModel
{
ApiKey = "123456",
Port = 2222
};
}
catch (Exception)
{
return new JsonResult { Data = "failed" };
}
return Json(info);
}
[HttpPost]
public ActionResult SaveGeneral(SettingsModel data)
{

File diff suppressed because it is too large Load Diff

View File

@ -94,6 +94,8 @@
<fieldset class="sub-field">
<legend>SABnzbd</legend>
<button type="button" onclick="autoConfigureSab()">Auto-Configure</button>
<div class="config-section">
<div class="config-group">
<div class="config-title">@Html.LabelFor(m => m.SabHost)</div>
@ -172,4 +174,26 @@
</fieldset>
}
<div id="result"></div>
<script type="text/javascript">
var autoConfigureSabUrl = '@Url.Action("AutoConfigureSab", "Settings")';
function autoConfigureSab() {
$.ajax({
type: "POST",
url: autoConfigureSabUrl,
data: jQuery.param({ username: $('#SabUsername').val(), password: $('#SabPassword').val() }),
error: function (req, status, error) {
alert("Sorry! We could not autoconfigure SABnzbd for you");
},
success: autoConfigureSuccess
});
function autoConfigureSuccess(data) {
$('#SabApiKey').val(data.ApiKey);
$('#SabPort').val(data.Port);
$('#SabUsername').val(data.Username);
$('#SabPassword').val(data.Password);
}
}
</script>

View File

@ -44,7 +44,8 @@ namespace NzbDrone
IISProcess.StartInfo.CreateNoWindow = true;
IISProcess.OutputDataReceived += (OnDataReceived);
IISProcess.OutputDataReceived += (OnOutputDataReceived);
IISProcess.ErrorDataReceived += (OnErrorDataReceived);
//Set Variables for the config file.
Environment.SetEnvironmentVariable("NZBDRONE_PATH", Config.ProjectRoot);
@ -60,6 +61,9 @@ namespace NzbDrone
Logger.Info("Starting process. [{0}]", IISProcess.StartInfo.FileName);
IISProcess.Start();
IISProcess.BeginErrorReadLine();
@ -73,6 +77,14 @@ namespace NzbDrone
return IISProcess;
}
private static void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e == null || String.IsNullOrWhiteSpace(e.Data))
return;
IISLogger.Error(e.Data);
}
internal static void StopServer()
{
KillProcess(IISProcess);
@ -82,7 +94,7 @@ namespace NzbDrone
{
string processPath = process.MainModule.FileName;
Logger.Info("[{0}]IIS Process found. Path:{1}", process.Id, processPath);
if (CleanPath(processPath) == CleanPath(IISExe))
if (NormalizePath(processPath) == NormalizePath(IISExe))
{
Logger.Info("[{0}]Process is considered orphaned.", process.Id);
KillProcess(process);
@ -124,7 +136,7 @@ namespace NzbDrone
}
}
private static void OnDataReceived(object s, DataReceivedEventArgs e)
private static void OnOutputDataReceived(object s, DataReceivedEventArgs e)
{
if (e == null || String.IsNullOrWhiteSpace(e.Data) || e.Data.StartsWith("Request started:") ||
e.Data.StartsWith("Request ended:") || e.Data == ("IncrementMessages called"))
@ -167,9 +179,19 @@ namespace NzbDrone
}
}
private static string CleanPath(string path)
public static string NormalizePath(string path)
{
return path.ToLower().Replace("\\", "").Replace("//", "//");
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('/', '\\', ' ').ToLower();
}

View File

@ -13,6 +13,7 @@
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
@ -25,7 +26,6 @@
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>

View File

@ -21,6 +21,7 @@ namespace NzbDrone
AppDomain.CurrentDomain.UnhandledException += ((s, e) => AppDomainException(e));
AppDomain.CurrentDomain.ProcessExit += ProgramExited;
AppDomain.CurrentDomain.DomainUnload += ProgramExited;
Process.GetCurrentProcess().EnableRaisingEvents = true;
Process.GetCurrentProcess().Exited += ProgramExited;
Config.ConfigureNlog();