From f973c74c87c7ec9f58665823a23ec61053c64b32 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 6 Oct 2011 21:36:47 -0700 Subject: [PATCH] ConfigFileProvider will now add missing config values automatically, with a default value. Added Handbrake and AtomicParsley wrappers for iPod video conversion. --- NzbDrone.Core.Test/ConfigFileProviderTest.cs | 63 ++++++++++- NzbDrone.Core/CentralDispatch.cs | 1 + NzbDrone.Core/Model/AtomicParsleyTitleType.cs | 9 ++ NzbDrone.Core/Model/AuthenticationType.cs | 13 +++ NzbDrone.Core/NzbDrone.Core.csproj | 5 + .../Converting/AtomicParsleyProvider.cs | 74 ++++++++++++ .../Providers/Converting/HandbrakeProvider.cs | 105 ++++++++++++++++++ .../Providers/Core/ConfigFileProvider.cs | 71 +++++++++--- .../Providers/Jobs/ConvertEpisodeJob.cs | 53 +++++++++ 9 files changed, 375 insertions(+), 19 deletions(-) create mode 100644 NzbDrone.Core/Model/AtomicParsleyTitleType.cs create mode 100644 NzbDrone.Core/Model/AuthenticationType.cs create mode 100644 NzbDrone.Core/Providers/Converting/AtomicParsleyProvider.cs create mode 100644 NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs create mode 100644 NzbDrone.Core/Providers/Jobs/ConvertEpisodeJob.cs diff --git a/NzbDrone.Core.Test/ConfigFileProviderTest.cs b/NzbDrone.Core.Test/ConfigFileProviderTest.cs index 471bec774..cf02dae96 100644 --- a/NzbDrone.Core.Test/ConfigFileProviderTest.cs +++ b/NzbDrone.Core.Test/ConfigFileProviderTest.cs @@ -2,6 +2,7 @@ using AutoMoq; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Model; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Repository; using NzbDrone.Core.Test.Framework; @@ -32,7 +33,7 @@ namespace NzbDrone.Core.Test var mocker = new AutoMoqer(); //Act - var result = mocker.Resolve().GetValue(key); + var result = mocker.Resolve().GetValue(key, value); //Assert result.Should().Be(value); @@ -47,7 +48,7 @@ namespace NzbDrone.Core.Test var mocker = new AutoMoqer(); //Act - var result = mocker.Resolve().GetValueInt(key); + var result = mocker.Resolve().GetValueInt(key, value); //Assert result.Should().Be(value); @@ -57,11 +58,12 @@ namespace NzbDrone.Core.Test public void GetBool_Success() { const string key = "LaunchBrowser"; + const bool value = true; var mocker = new AutoMoqer(); //Act - var result = mocker.Resolve().GetValueBoolean(key); + var result = mocker.Resolve().GetValueBoolean(key, value); //Assert result.Should().BeTrue(); @@ -124,5 +126,60 @@ namespace NzbDrone.Core.Test var result = mocker.Resolve().Port; result.Should().Be(value); } + + [Test] + public void GetValue_New_Key() + { + const string key = "Hello"; + const string value = "World"; + + var mocker = new AutoMoqer(); + + //Act + var result = mocker.Resolve().GetValue(key, value); + + //Assert + result.Should().Be(value); + } + + [Test] + public void GetValue_New_Key_with_new_parent() + { + const string key = "Hello"; + const string value = "World"; + + var mocker = new AutoMoqer(); + + //Act + var result = mocker.Resolve().GetValue(key, value, "Universe"); + + //Assert + result.Should().Be(value); + } + + [Test] + public void GetAuthenticationType_No_Existing_Value() + { + var mocker = new AutoMoqer(); + + //Act + var result = mocker.Resolve().AuthenticationType; + + //Assert + result.Should().Be(AuthenticationType.Anonymous); + } + + [Test] + public void GetAuthenticationType_Windows() + { + var mocker = new AutoMoqer(); + mocker.Resolve().SetValue("AuthenticationType", 1); + + //Act + var result = mocker.Resolve().AuthenticationType; + + //Assert + result.Should().Be(AuthenticationType.Windows); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/CentralDispatch.cs b/NzbDrone.Core/CentralDispatch.cs index ca707daa5..6eccb3a2c 100644 --- a/NzbDrone.Core/CentralDispatch.cs +++ b/NzbDrone.Core/CentralDispatch.cs @@ -113,6 +113,7 @@ namespace NzbDrone.Core _kernel.Bind().To().InSingletonScope(); _kernel.Bind().To().InSingletonScope(); _kernel.Bind().To().InSingletonScope(); + _kernel.Bind().To().InSingletonScope(); _kernel.Get().Initialize(); _kernel.Get().StartTimer(30); diff --git a/NzbDrone.Core/Model/AtomicParsleyTitleType.cs b/NzbDrone.Core/Model/AtomicParsleyTitleType.cs new file mode 100644 index 000000000..d133b977a --- /dev/null +++ b/NzbDrone.Core/Model/AtomicParsleyTitleType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Model +{ + public enum AtomicParsleyTitleType + { + None = 0, + EpisodeNumber = 1, + Both = 2 + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Model/AuthenticationType.cs b/NzbDrone.Core/Model/AuthenticationType.cs new file mode 100644 index 000000000..0ffc02991 --- /dev/null +++ b/NzbDrone.Core/Model/AuthenticationType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model +{ + public enum AuthenticationType + { + Anonymous = 0, + Windows = 1 + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index ea2d903c2..5a958d2e4 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -189,6 +189,8 @@ + + @@ -203,10 +205,13 @@ + + + diff --git a/NzbDrone.Core/Providers/Converting/AtomicParsleyProvider.cs b/NzbDrone.Core/Providers/Converting/AtomicParsleyProvider.cs new file mode 100644 index 000000000..ae99c9258 --- /dev/null +++ b/NzbDrone.Core/Providers/Converting/AtomicParsleyProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.IO; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; + +namespace NzbDrone.Core.Providers.Converting +{ + public class AtomicParsleyProvider + { + private readonly ConfigProvider _configProvider; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public AtomicParsleyProvider(ConfigProvider configProvider) + { + _configProvider = configProvider; + } + + public AtomicParsleyProvider() + { + + } + + public virtual bool RunAtomicParsley(Episode episode, string outputFile) + { + throw new NotImplementedException(); + + var atomicParsleyLocation = _configProvider.GetValue("AtomicParsleyLocation", ""); + var atomicParsleyTitleType = (AtomicParsleyTitleType) System.Convert.ToInt32(_configProvider.GetValue("AtomicParsley", 0)); + + var atomicParsleyCommand = String.Format("\"{0}\" --overWrite --title \"{1}\" --genre \"TV Shows\" --stik \"TV Show\" --TVShowName \"{2}\" --TVEpisodeNum \"{3}\" --TVSeason \"{4}\"", + outputFile, episode.Title, episode.Series.Title, episode.EpisodeNumber, episode.SeasonNumber); + + //If Episode Number + Name should be in Episode Title (Number - Title) + if (atomicParsleyTitleType == AtomicParsleyTitleType.EpisodeNumber) + { + atomicParsleyCommand = String.Format("\"{0}\" --overWrite --title \"{3} - {1}\" --genre \"TV Shows\" --stik \"TV Show\" --TVShowName \"{2}\" --TVEpisodeNum \"{3}\" --TVSeason \"{4}\"", + outputFile, episode.Title, episode.Series.Title, episode.EpisodeNumber, episode.SeasonNumber); + } + + //If Season/Episode Number + Name should be in Episode Title (SeasonNumber'x'EpisodeNumber - Title) + else if (atomicParsleyTitleType == AtomicParsleyTitleType.Both) + { + atomicParsleyCommand = String.Format("\"{0}\" --overWrite --title \"{4}x{3:00} - {1}\" --genre \"TV Shows\" --stik \"TV Show\" --TVShowName \"{2}\" --TVEpisodeNum \"{3}\" --TVSeason \"{4}\"", + outputFile, episode.Title, episode.Series.Title, episode.EpisodeNumber, episode.SeasonNumber); + } + + try + { + var process = new Process(); + process.StartInfo.FileName = Path.Combine(atomicParsleyLocation, "AtomicParsley.exe"); + process.StartInfo.Arguments = atomicParsleyCommand; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + //process.OutputDataReceived += new DataReceivedEventHandler(HandBrakeOutputDataReceived); + process.Start(); + //process.BeginOutputReadLine(); + process.WaitForExit(); + } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + return false; + } + + return true; + } + } +} diff --git a/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs b/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs new file mode 100644 index 000000000..0379730ca --- /dev/null +++ b/NzbDrone.Core/Providers/Converting/HandbrakeProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; + +namespace NzbDrone.Core.Providers.Converting +{ + public class HandbrakeProvider + { + //Interacts with Handbrake + private readonly ConfigProvider _configProvider; + private ProgressNotification _notification; + private Episode _currentEpisode; + + private Regex _processingRegex = + new Regex(@"^(?:Encoding).+?(?:\,\s(?\d{1,3}\.\d{2})\s\%)(?:.+?ETA\s(?\d{2})h(?\d{2})m(?\d{2})s)?", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public HandbrakeProvider(ConfigProvider configProvider) + { + _configProvider = configProvider; + } + + public HandbrakeProvider() + { + + } + + public virtual string ConvertFile(Episode episode, ProgressNotification notification) + { + _notification = notification; + _currentEpisode = episode; + + var outputFile = _configProvider.GetValue("iPodConvertDir", ""); + + var handBrakePreset = _configProvider.GetValue("HandBrakePreset", "iPhone & iPod Touch"); + var handBrakeCommand = String.Format("-i \"{0}\" -o \"{1}\" --preset=\"{2}\"", episode.EpisodeFile.Path, outputFile, handBrakePreset); + var handBrakeFile = @"C:\Program Files (x86)\Handbrake\HandBrakeCLI.exe"; + + try + { + var process = new Process(); + process.StartInfo.FileName = handBrakeFile; + process.StartInfo.Arguments = handBrakeCommand; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.OutputDataReceived += new DataReceivedEventHandler(HandBrakeOutputDataReceived); + process.Start(); + process.BeginOutputReadLine(); + process.WaitForExit(); + } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + return String.Empty; + } + + return outputFile; + } + + private void HandBrakeOutputDataReceived(object obj, DataReceivedEventArgs args) + { + throw new NotImplementedException(); + + //args.Data contains the line writen + + var match = _processingRegex.Matches(args.Data); + + if (match.Count != 1) + return; + + var episodeString = String.Format("{0} - {1}x{2:00}", + _currentEpisode.Series.Title, + _currentEpisode.SeasonNumber, + _currentEpisode.EpisodeNumber); + + var percent = System.Convert.ToDecimal(match[0].Groups["percent"].Value); + int hours; + int minutes; + int seconds; + + Int32.TryParse(match[0].Groups["hours"].Value, out hours); + Int32.TryParse(match[0].Groups["minutes"].Value, out minutes); + Int32.TryParse(match[0].Groups["seconds"].Value, out seconds); + + if (seconds > 0 || minutes > 0 || hours > 0) + { + var eta = DateTime.Now.Add(new TimeSpan(0, hours, minutes, seconds)); + _notification.CurrentMessage = String.Format("Converting: {0}, {1}%. ETA: {2}", episodeString, percent, eta); + } + + else + _notification.CurrentMessage = String.Format("Converting: {0}, {1}%.", episodeString, percent); + + Console.WriteLine(args.Data); + } + } +} diff --git a/NzbDrone.Core/Providers/Core/ConfigFileProvider.cs b/NzbDrone.Core/Providers/Core/ConfigFileProvider.cs index 193c126a3..e6fdc787a 100644 --- a/NzbDrone.Core/Providers/Core/ConfigFileProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigFileProvider.cs @@ -5,29 +5,39 @@ using System.Linq; using System.Reflection; using System.Text; using System.Xml.Linq; +using NzbDrone.Core.Model; namespace NzbDrone.Core.Providers.Core { public class ConfigFileProvider { + private string _configFile = Path.Combine(CentralDispatch.AppPath, "App_Data", "Config.xml"); + public string ConfigFile { - get { return Path.Combine(CentralDispatch.AppPath, "App_Data", "Config.xml"); } + get { return _configFile; } + set { _configFile = value; } } public virtual int Port { - get { return GetValueInt("Port"); } + get { return GetValueInt("Port", 8989); } set { SetValue("Port", value); } } public virtual bool LaunchBrowser { - get { return GetValueBoolean("LaunchBrowser"); } + get { return GetValueBoolean("LaunchBrowser", true); } set { SetValue("LaunchBrowser", value); } } - public virtual string GetValue(string key, string parent = null) + public virtual AuthenticationType AuthenticationType + { + get { return (AuthenticationType)GetValueInt("AuthenticationType", 0); } + set { SetValue("AuthenticationType", (int)value); } + } + + public virtual string GetValue(string key, object defaultValue, string parent = null) { var xDoc = XDocument.Load(ConfigFile); var config = xDoc.Descendants("Config").Single(); @@ -35,21 +45,40 @@ namespace NzbDrone.Core.Providers.Core var parentContainer = config; if (!String.IsNullOrEmpty(parent)) + { + //Add the parent + if (config.Descendants(parent).Count() != 1) + { + SetValue(key, defaultValue, parent); + + //Reload the configFile + xDoc = XDocument.Load(ConfigFile); + config = xDoc.Descendants("Config").Single(); + } + parentContainer = config.Descendants(parent).Single(); + } + + var valueHolder = parentContainer.Descendants(key).ToList(); + + if (valueHolder.Count() == 1) + return valueHolder.First().Value; - var value = parentContainer.Descendants(key).Single().Value; + //Save the value + SetValue(key, defaultValue, parent); - return value; + //return the default value + return defaultValue.ToString(); } - public virtual int GetValueInt(string key, string parent = null) + public virtual int GetValueInt(string key, int defaultValue, string parent = null) { - return Convert.ToInt32(GetValue(key, parent)); + return Convert.ToInt32(GetValue(key, defaultValue, parent)); } - public virtual bool GetValueBoolean(string key, string parent = null) + public virtual bool GetValueBoolean(string key, bool defaultValue, string parent = null) { - return Convert.ToBoolean(GetValue(key, parent)); + return Convert.ToBoolean(GetValue(key, defaultValue, parent)); } public virtual void SetValue(string key, object value, string parent = null) @@ -60,9 +89,23 @@ namespace NzbDrone.Core.Providers.Core var parentContainer = config; if (!String.IsNullOrEmpty(parent)) + { + //Add the parent container if it doesn't already exist + if (config.Descendants(parent).Count() != 1) + { + config.Add(new XElement(parent)); + } + parentContainer = config.Descendants(parent).Single(); + } + + var keyHolder = parentContainer.Descendants(key); + + if (keyHolder.Count() != 1) + parentContainer.Add(new XElement(key, value)); - parentContainer.Descendants(key).Single().Value = value.ToString(); + else + parentContainer.Descendants(key).Single().Value = value.ToString(); xDoc.Save(ConfigFile); } @@ -82,11 +125,7 @@ namespace NzbDrone.Core.Providers.Core { var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); - xDoc.Add(new XElement("Config", - new XElement("Port", 8989), - new XElement("LaunchBrowser", true) - ) - ); + xDoc.Add(new XElement("Config")); xDoc.Save(ConfigFile); } diff --git a/NzbDrone.Core/Providers/Jobs/ConvertEpisodeJob.cs b/NzbDrone.Core/Providers/Jobs/ConvertEpisodeJob.cs new file mode 100644 index 000000000..5363fc3e9 --- /dev/null +++ b/NzbDrone.Core/Providers/Jobs/ConvertEpisodeJob.cs @@ -0,0 +1,53 @@ +using System; +using Ninject; +using NLog; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.Converting; + +namespace NzbDrone.Core.Providers.Jobs +{ + public class ConvertEpisodeJob : IJob + { + private readonly HandbrakeProvider _handbrakeProvider; + private readonly AtomicParsleyProvider _atomicParsleyProvider; + private readonly EpisodeProvider _episodeProvider; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public ConvertEpisodeJob(HandbrakeProvider handbrakeProvider, AtomicParsleyProvider atomicParsleyProvider, + EpisodeProvider episodeProvider) + { + _handbrakeProvider = handbrakeProvider; + _atomicParsleyProvider = atomicParsleyProvider; + _episodeProvider = episodeProvider; + } + + public string Name + { + get { return "Convert Episode"; } + } + + public int DefaultInterval + { + get { return 0; } + } + + public void Start(ProgressNotification notification, int targetId, int secondaryTargetId) + { + if (targetId <= 0) + throw new ArgumentOutOfRangeException("targetId"); + + var episode = _episodeProvider.GetEpisode(targetId); + notification.CurrentMessage = String.Format("Starting Conversion for {0}", episode); + var outputFile = _handbrakeProvider.ConvertFile(episode, notification); + + if (String.IsNullOrEmpty(outputFile)) + notification.CurrentMessage = String.Format("Conversion failed for {0}", episode); + + _atomicParsleyProvider.RunAtomicParsley(episode, outputFile); + + notification.CurrentMessage = String.Format("Conversion completed for {0}", episode); + } + } +} \ No newline at end of file