diff --git a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs new file mode 100644 index 000000000..2055fe41e --- /dev/null +++ b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Notifications.Synology; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.NotificationTests +{ + [TestFixture] + public class SynologyIndexerFixture : CoreTest + { + private Series _series; + private DownloadMessage _upgrade; + + [SetUp] + public void SetUp() + { + _series = new Series() + { + Path = @"C:\Test\".AsOsAgnostic() + }; + + _upgrade = new DownloadMessage() + { + Series = _series, + + EpisodeFile = new EpisodeFile + { + RelativePath = "file1.S01E01E02.mkv" + }, + + OldFiles = new List + { + new EpisodeFile + { + RelativePath = "file1.S01E01.mkv" + }, + new EpisodeFile + { + RelativePath = "file1.S01E02.mkv" + } + } + }; + + Subject.Definition = new NotificationDefinition + { + Settings = new SynologyIndexerSettings + { + UpdateLibrary = true + } + }; + } + + [Test] + public void should_not_update_library_if_disabled() + { + (Subject.Definition.Settings as SynologyIndexerSettings).UpdateLibrary = false; + + Subject.AfterRename(_series); + + Mocker.GetMock() + .Verify(v => v.UpdateFolder(_series.Path), Times.Never()); + } + + [Test] + public void should_remove_old_episodes_on_upgrade() + { + Subject.OnDownload(_upgrade); + + Mocker.GetMock() + .Verify(v => v.DeleteFile(@"C:\Test\file1.S01E01.mkv".AsOsAgnostic()), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.DeleteFile(@"C:\Test\file1.S01E02.mkv".AsOsAgnostic()), Times.Once()); + } + + [Test] + public void should_add_new_episode_on_upgrade() + { + Subject.OnDownload(_upgrade); + + Mocker.GetMock() + .Verify(v => v.AddFile(@"C:\Test\file1.S01E01E02.mkv".AsOsAgnostic()), Times.Once()); + } + + [Test] + public void should_update_entire_series_folder_on_rename() + { + Subject.AfterRename(_series); + + Mocker.GetMock() + .Verify(v => v.UpdateFolder(@"C:\Test\".AsOsAgnostic()), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 298cdffda..3efcec4c5 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -241,6 +241,7 @@ + diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyException.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyException.cs new file mode 100644 index 000000000..f7534671b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyException.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Synology +{ + public class SynologyException : NzbDroneException + { + public SynologyException(string message) : base(message) + { + } + + public SynologyException(string message, params object[] args) : base(message, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs new file mode 100644 index 000000000..0f8ada22e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.IO; +using FluentValidation.Results; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications.Synology +{ + public class SynologyIndexer : NotificationBase + { + private readonly ISynologyIndexerProxy _indexerProxy; + + public SynologyIndexer(ISynologyIndexerProxy indexerProxy) + { + _indexerProxy = indexerProxy; + } + + public override string Link + { + get { return "http://www.synology.com"; } + } + + public override void OnGrab(string message) + { + + } + + public override void OnDownload(DownloadMessage message) + { + if (Settings.UpdateLibrary) + { + foreach (var oldFile in message.OldFiles) + { + var fullPath = Path.Combine(message.Series.Path, oldFile.RelativePath); + + _indexerProxy.DeleteFile(fullPath); + } + + { + var fullPath = Path.Combine(message.Series.Path, message.EpisodeFile.RelativePath); + + _indexerProxy.AddFile(fullPath); + } + } + } + + public override void AfterRename(Series series) + { + if (Settings.UpdateLibrary) + { + _indexerProxy.UpdateFolder(series.Path); + } + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(TestConnection()); + + return new ValidationResult(failures); + } + + protected virtual ValidationFailure TestConnection() + { + if (!OsInfo.IsLinux) + { + return new ValidationFailure(null, "Must be a Synology"); + } + + if (!_indexerProxy.Test()) + { + return new ValidationFailure(null, "Not a Synology or synoindex not available"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerProxy.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerProxy.cs new file mode 100644 index 000000000..18fa33433 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerProxy.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using NLog; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Core.Notifications.Synology +{ + public interface ISynologyIndexerProxy + { + bool Test(); + void AddFile(string filepath); + void DeleteFile(string filepath); + void AddFolder(string folderpath); + void DeleteFolder(string folderpath); + void UpdateFolder(string folderpath); + void UpdateLibrary(); + } + + public class SynologyIndexerProxy : ISynologyIndexerProxy + { + private const string SynoIndexPath = "/usr/syno/bin/synoindex"; + + private readonly IProcessProvider _processProvider; + private readonly Logger _logger; + + public SynologyIndexerProxy(IProcessProvider processProvider, Logger logger) + { + _processProvider = processProvider; + _logger = logger; + } + + public bool Test() + { + try + { + ExecuteCommand("--help", false); + return true; + } + catch (Exception ex) + { + _logger.WarnException("synoindex not available", ex); + return false; + } + } + + public void AddFile(string filePath) + { + ExecuteCommand("-a " + Escape(filePath)); + } + + public void DeleteFile(string filePath) + { + ExecuteCommand("-d " + Escape(filePath)); + } + + public void AddFolder(string folderPath) + { + ExecuteCommand("-A " + Escape(folderPath)); + } + + public void DeleteFolder(string folderPath) + { + ExecuteCommand("-D " + Escape(folderPath)); + } + + public void UpdateFolder(string folderPath) + { + ExecuteCommand("-R " + Escape(folderPath)); + } + + public void UpdateLibrary() + { + ExecuteCommand("-R video"); + } + + private void ExecuteCommand(string args, bool throwOnStdOut = true) + { + var output = _processProvider.StartAndCapture(SynoIndexPath, args); + + if (output.Standard.Count != 0 && throwOnStdOut) + { + throw new SynologyException("synoindex returned an error: {0}", string.Join("\n", output.Standard)); + } + + if (output.Error.Count != 0) + { + throw new SynologyException("synoindex returned an error: {0}", string.Join("\n", output.Error)); + } + } + + private string Escape(string arg) + { + return string.Format("\"{0}\"", arg.Replace("\"", "\\\"")); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs new file mode 100644 index 000000000..b1c14ba82 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexerSettings.cs @@ -0,0 +1,35 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Synology +{ + public class SynologyIndexerSettingsValidator : AbstractValidator + { + public SynologyIndexerSettingsValidator() + { + + } + } + + public class SynologyIndexerSettings : IProviderConfig + { + private static readonly SynologyIndexerSettingsValidator Validator = new SynologyIndexerSettingsValidator(); + + public SynologyIndexerSettings() + { + UpdateLibrary = true; + } + + [FieldDefinition(0, Label = "Update Library", Type = FieldType.Checkbox, HelpText = "Call synoindex on localhost to update a library file")] + public Boolean UpdateLibrary { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 37cfafc54..ba8b068d8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -665,6 +665,10 @@ + + + +