From bee69140626f3b1b897d1fa06950b10f864e68b5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 3 Aug 2012 00:01:34 -0700 Subject: [PATCH 1/8] Allow scene name to be used for renaming New: Added option to use scene name for episodefiles --- NzbDrone.Core.Test/ParserTest.cs | 16 +- .../ProviderTests/DiskScanProviderTest.cs | 6 +- .../GetNewFilenameFixture.cs | 102 +- .../ProviderTests/MisnamedProviderTest.cs | 20 +- .../Datastore/Migrations/Migration20120802.cs | 17 + NzbDrone.Core/NzbDrone.Core.csproj | 1 + NzbDrone.Core/Parser.cs | 955 +++++++++--------- .../Providers/Core/ConfigProvider.cs | 6 + NzbDrone.Core/Providers/DiskScanProvider.cs | 3 +- NzbDrone.Core/Providers/MediaFileProvider.cs | 16 +- NzbDrone.Core/Providers/MisnamedProvider.cs | 5 +- NzbDrone.Core/Repository/EpisodeFile.cs | 1 + .../Controllers/SettingsController.cs | 2 + NzbDrone.Web/Models/EpisodeNamingModel.cs | 4 + .../Settings/EpisodeNamingPartial.cshtml | 4 + 15 files changed, 647 insertions(+), 511 deletions(-) create mode 100644 NzbDrone.Core/Datastore/Migrations/Migration20120802.cs diff --git a/NzbDrone.Core.Test/ParserTest.cs b/NzbDrone.Core.Test/ParserTest.cs index 5289e2b10..90898be65 100644 --- a/NzbDrone.Core.Test/ParserTest.cs +++ b/NzbDrone.Core.Test/ParserTest.cs @@ -185,7 +185,6 @@ namespace NzbDrone.Core.Test { var qualityEnums = Enum.GetValues(typeof(QualityTypes)); - foreach (var qualityEnum in qualityEnums) { var fileName = String.Format("My series S01E01 [{0}]", qualityEnum); @@ -276,8 +275,6 @@ namespace NzbDrone.Core.Test result.Should().Be(seriesName); } - - [TestCase("CaPitAl", "capital")] [TestCase("peri.od", "period")] [TestCase("this.^&%^**$%@#$!That", "thisthat")] @@ -290,7 +287,6 @@ namespace NzbDrone.Core.Test result.Should().Be(clean); } - [TestCase("the")] [TestCase("and")] [TestCase("or")] @@ -360,7 +356,6 @@ namespace NzbDrone.Core.Test result.Should().Be(Parser.NormalizeTitle(title)); } - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", LanguageType.English)] [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL", LanguageType.French)] [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL", LanguageType.Spanish)] @@ -445,5 +440,16 @@ namespace NzbDrone.Core.Test ExceptionVerification.IgnoreWarns(); ExceptionVerification.ExpectedErrors(1); } + + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] + [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] + [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] + [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] + [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] + [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] + public void parse_releaseGroup(string title, string expected) + { + Parser.ParseReleaseGroup(title).Should().Be(expected); + } } } diff --git a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTest.cs index 596da2640..8973b29c0 100644 --- a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTest.cs @@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.ProviderTests .Returns(fakeEpisode); Mocker.GetMock() - .Setup(e => e.GetNewFilename(fakeEpisode, fakeSeries.Title, It.IsAny(), It.IsAny())) + .Setup(e => e.GetNewFilename(fakeEpisode, fakeSeries.Title, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(filename); Mocker.GetMock() @@ -298,7 +298,7 @@ namespace NzbDrone.Core.Test.ProviderTests Mocker.GetMock().Setup(s => s.GetEpisodesByFileId(episodeFile.EpisodeFileId)) .Returns(episode); - Mocker.GetMock().Setup(s => s.GetNewFilename(It.IsAny>(), series.Title, QualityTypes.Unknown, false)) + Mocker.GetMock().Setup(s => s.GetNewFilename(It.IsAny>(), series.Title, QualityTypes.Unknown, false, It.IsAny())) .Returns(newFilename); Mocker.GetMock().Setup(s => s.CalculateFilePath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -350,7 +350,7 @@ namespace NzbDrone.Core.Test.ProviderTests .Returns(fakeEpisode); Mocker.GetMock() - .Setup(e => e.GetNewFilename(fakeEpisode, fakeSeries.Title, It.IsAny(), It.IsAny())) + .Setup(e => e.GetNewFilename(fakeEpisode, fakeSeries.Title, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(filename); Mocker.GetMock() diff --git a/NzbDrone.Core.Test/ProviderTests/MediaFileProviderTests/GetNewFilenameFixture.cs b/NzbDrone.Core.Test/ProviderTests/MediaFileProviderTests/GetNewFilenameFixture.cs index 12f333614..21d0de628 100644 --- a/NzbDrone.Core.Test/ProviderTests/MediaFileProviderTests/GetNewFilenameFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/MediaFileProviderTests/GetNewFilenameFixture.cs @@ -1,7 +1,7 @@ // ReSharper disable RedundantUsingDirective using System.Collections.Generic; - +using System.IO; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South Park - S15E06 - City Sushi [HDTV]", result); @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("15x06 - City Sushi [HDTV]", result); @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South Park 05x06 [HDTV]", result); @@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South Park s05e06", result); @@ -148,7 +148,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South.Park.s05e06.City.Sushi", result); @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South.Park.-.s05e06.-.City.Sushi.[HDTV]", result); @@ -203,7 +203,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("S15E06", result); @@ -237,7 +237,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("The Mentalist - S03E23-E24 - Strawberries and Cream (1) + Strawberries and Cream (2) [HDTV]", result); @@ -271,7 +271,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("3x23x24 - Strawberries and Cream (1) + Strawberries and Cream (2) [HDTV]", result); @@ -305,7 +305,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("3x23x24 Strawberries and Cream (1) + Strawberries and Cream (2) [HDTV]", result); @@ -339,7 +339,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("The.Mentalist.s03e23.s03e24.Strawberries.and.Cream.(1).+.Strawberries.and.Cream.(2)", result); @@ -373,7 +373,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("The.Mentalist.-.S03E23-24", result); @@ -407,7 +407,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episodeOne, episodeTwo }, "The Mentalist", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("3x23x24", result); @@ -432,7 +432,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, true); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, true, new EpisodeFile()); //Assert result.Should().Be("South Park - S15E06 - City Sushi [HDTV] [Proper]"); @@ -457,7 +457,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert result.Should().Be("South Park - S15E06 - City Sushi [HDTV]"); @@ -482,7 +482,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, true); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, true, new EpisodeFile()); //Assert result.Should().Be("South Park - S15E06 - City Sushi"); @@ -514,7 +514,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode2, episode }, "30 Rock", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode2, episode }, "30 Rock", QualityTypes.HDTV, false, new EpisodeFile()); //Assert result.Should().Be("30 Rock - S06E06-E07 - Hey, Baby, What's Wrong! (1) + Hey, Baby, What's Wrong! (2)"); @@ -541,7 +541,7 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("South Park.S15E06.City Sushi [HDTV]", result); @@ -568,10 +568,72 @@ namespace NzbDrone.Core.Test.ProviderTests.MediaFileProviderTests .Build(); //Act - string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false); + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, new EpisodeFile()); //Assert Assert.AreEqual("15x06.City Sushi [HDTV]", result); } + + [Test] + public void GetNewFilename_UseSceneName_when_sceneName_isNull() + { + //Setup + var fakeConfig = Mocker.GetMock(); + fakeConfig.SetupGet(c => c.SortingIncludeSeriesName).Returns(false); + fakeConfig.SetupGet(c => c.SortingIncludeEpisodeTitle).Returns(true); + fakeConfig.SetupGet(c => c.SortingAppendQuality).Returns(true); + fakeConfig.SetupGet(c => c.SortingSeparatorStyle).Returns(2); + fakeConfig.SetupGet(c => c.SortingNumberStyle).Returns(0); + fakeConfig.SetupGet(c => c.SortingReplaceSpaces).Returns(false); + fakeConfig.SetupGet(c => c.SortingUseSceneName).Returns(true); + + var episode = Builder.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.SeasonNumber = 15) + .With(e => e.EpisodeNumber = 6) + .Build(); + + var episodeFile = Builder.CreateNew() + .With(e => e.SceneName = null) + .With(e => e.Path = @"C:\Test\TV\30 Rock - S01E01 - Test") + .Build(); + + //Act + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, episodeFile); + + //Assert + result.Should().Be(Path.GetFileNameWithoutExtension(episodeFile.Path)); + } + + [Test] + public void GetNewFilename_UseSceneName_when_sceneName_isNotNull() + { + //Setup + var fakeConfig = Mocker.GetMock(); + fakeConfig.SetupGet(c => c.SortingIncludeSeriesName).Returns(false); + fakeConfig.SetupGet(c => c.SortingIncludeEpisodeTitle).Returns(true); + fakeConfig.SetupGet(c => c.SortingAppendQuality).Returns(true); + fakeConfig.SetupGet(c => c.SortingSeparatorStyle).Returns(2); + fakeConfig.SetupGet(c => c.SortingNumberStyle).Returns(0); + fakeConfig.SetupGet(c => c.SortingReplaceSpaces).Returns(false); + fakeConfig.SetupGet(c => c.SortingUseSceneName).Returns(true); + + var episode = Builder.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.SeasonNumber = 15) + .With(e => e.EpisodeNumber = 6) + .Build(); + + var episodeFile = Builder.CreateNew() + .With(e => e.SceneName = "30.Rock.S01E01.xvid-LOL") + .With(e => e.Path = @"C:\Test\TV\30 Rock - S01E01 - Test") + .Build(); + + //Act + string result = Mocker.Resolve().GetNewFilename(new List { episode }, "South Park", QualityTypes.HDTV, false, episodeFile); + + //Assert + result.Should().Be(episodeFile.SceneName); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/MisnamedProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/MisnamedProviderTest.cs index 646cfd372..8636ef9ef 100644 --- a/NzbDrone.Core.Test/ProviderTests/MisnamedProviderTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/MisnamedProviderTest.cs @@ -49,11 +49,11 @@ namespace NzbDrone.Core.Test.ProviderTests .Setup(c => c.EpisodesWithFiles()).Returns(episodes); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[0])) .Returns("Title1"); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[1])) .Returns("Title2"); //Act @@ -98,11 +98,11 @@ namespace NzbDrone.Core.Test.ProviderTests .Setup(c => c.EpisodesWithFiles()).Returns(episodes); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[0])) .Returns("New Title 1"); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[1])) .Returns("New Title 2"); //Act @@ -147,11 +147,11 @@ namespace NzbDrone.Core.Test.ProviderTests .Setup(c => c.EpisodesWithFiles()).Returns(episodes); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[0] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[0])) .Returns("New Title 1"); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[1])) .Returns("Title2"); //Act @@ -198,11 +198,11 @@ namespace NzbDrone.Core.Test.ProviderTests .Setup(c => c.EpisodesWithFiles()).Returns(episodes); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[0], episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[0], episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[0])) .Returns("New Title 1"); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[2] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[2] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[1])) .Returns("Title2"); //Act @@ -249,11 +249,11 @@ namespace NzbDrone.Core.Test.ProviderTests .Setup(c => c.EpisodesWithFiles()).Returns(episodes); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[0], episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[0], episodes[1] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[0])) .Returns("Title1"); Mocker.GetMock() - .Setup(c => c.GetNewFilename(new List { episodes[2] }, "SeriesTitle", It.IsAny(), It.IsAny())) + .Setup(c => c.GetNewFilename(new List { episodes[2] }, "SeriesTitle", It.IsAny(), It.IsAny(), episodeFiles[1])) .Returns("Title2"); //Act diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs b/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs new file mode 100644 index 000000000..203d08874 --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs @@ -0,0 +1,17 @@ +using System; +using System.Data; +using Migrator.Framework; +using NzbDrone.Common; + +namespace NzbDrone.Core.Datastore.Migrations +{ + + [Migration(20120802)] + public class Migration20120802 : NzbDroneMigration + { + protected override void MainDbUpgrade() + { + Database.AddColumn("EpisodeFiles", new Column("SceneName", DbType.String, ColumnProperty.Null)); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index edf77d7e4..99eef5003 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -227,6 +227,7 @@ + diff --git a/NzbDrone.Core/Parser.cs b/NzbDrone.Core/Parser.cs index da7d6c51f..462f81ad8 100644 --- a/NzbDrone.Core/Parser.cs +++ b/NzbDrone.Core/Parser.cs @@ -1,468 +1,487 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common; -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[] - { - //Episodes with airdate - new Regex(@"^(?.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Multi-Part episodes without a title (S01E05.S01E06) - new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,2}(?!\d+)))+){2,}\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc) - new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,2}(?!\d+)))+){2,}\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) - new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex])(?<episode>\d{2}(?!\d+)))+\W*)+\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Episodes over 99 (3-digits or more) (S01E105, S01E105E106, etc) - new Regex(@"^(?<title>.*?)(?:\W?S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d+))+)+\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex])(?<episode>\d{2}(?!\d+)))+\W*)+\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Supports 103/113 naming - new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!p|i|\d+)))+\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Mini-Series, treated as season 1, episodes are labeled as Part01, Part 01, Part.1 - new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Supports 1103/1113 naming - new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+|\(|\[)\d{2})(?<episode>\d{2}(?!p|i|\d+|\)|\])))+\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - - //Supports Season only releases - new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))\W?(?<extras>EXTRAS|SUBPACK)?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled) - }; - - private static readonly Regex NormalizeRegex = new Regex(@"((^|\W)(a|an|the|and|or|of)($|\W))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[x|h|x\s|h\s]264|DD\W?5\W1|\<|\>|\?|\*|\:|\||""", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex ReportSizeRegex = new Regex(@"(?<value>\d+\.\d{1,2}|\d+\,\d+\.\d{1,2})\W?(?<unit>GB|MB|GiB|MiB)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - private static readonly Regex HeaderRegex = new Regex(@"(?:\[.+\]\-\[.+\]\-\[.+\]\-\[)(?<nzbTitle>.+)(?:\]\-.+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - - internal static EpisodeParseResult ParsePath(string path) - { - var fileInfo = new FileInfo(path); - - var result = ParseTitle(fileInfo.Name); - - if (result == null) - { - Logger.Trace("Attempting to parse episode info using full path. {0}", fileInfo.FullName); - result = ParseTitle(fileInfo.FullName); - } - - if (result != null) - { - result.OriginalString = path; - } - else - { - Logger.Warn("Unable to parse episode info from path {0}", path); - } - - return result; - } - - internal static EpisodeParseResult ParseTitle(string title) - { - try - { - Logger.Trace("Parsing string '{0}'", title); - var simpleTitle = SimpleTitleRegex.Replace(title, String.Empty); - - foreach (var regex in ReportTitleRegex) - { - var match = regex.Matches(simpleTitle); - - if (match.Count != 0) - { - var result = ParseMatchCollection(match); - if (result != null) - { - //Check if episode is in the future (most likley a parse error) - if (result.AirDate > DateTime.Now.AddDays(1).Date) - break; - - result.Language = ParseLanguage(title); - result.Quality = ParseQuality(title); - result.OriginalString = title; - return result; - } - } - } - } - catch (Exception e) - { - Logger.ErrorException("An error has occurred while trying to parse " + title, e); - } - - Logger.Trace("Unable to parse {0}", title); - ReportingService.ReportParseError(title); - return null; - } - - private static EpisodeParseResult ParseMatchCollection(MatchCollection matchCollection) - { - var seriesName = matchCollection[0].Groups["title"].Value; - - int airyear; - Int32.TryParse(matchCollection[0].Groups["airyear"].Value, out airyear); - - EpisodeParseResult parsedEpisode; - - if (airyear < 1900) - { - var seasons = new List<int>(); - - foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures) - { - int parsedSeason; - if (Int32.TryParse(seasonCapture.Value, out parsedSeason)) - seasons.Add(parsedSeason); - } - - //If no season was found it should be treated as a mini series and season 1 - if (seasons.Count == 0) - seasons.Add(1); - - //If more than 1 season was parsed go to the next REGEX (A multi-season release is unlikely) - if (seasons.Distinct().Count() > 1) - return null; - - parsedEpisode = new EpisodeParseResult - { - SeasonNumber = seasons.First(), - EpisodeNumbers = new List<int>() - }; - - foreach (Match matchGroup in matchCollection) - { - var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList(); - - //Allows use to return a list of 0 episodes (We can handle that as a full season release) - if (episodeCaptures.Any()) - { - var first = Convert.ToInt32(episodeCaptures.First().Value); - var last = Convert.ToInt32(episodeCaptures.Last().Value); - parsedEpisode.EpisodeNumbers = Enumerable.Range(first, last - first + 1).ToList(); - } - else - { - //Check to see if this is an "Extras" or "SUBPACK" release, if it is, return NULL - //Todo: Set a "Extras" flag in EpisodeParseResult if we want to download them ever - if (!String.IsNullOrWhiteSpace(matchCollection[0].Groups["extras"].Value)) - return null; - - parsedEpisode.FullSeason = true; - } - } - } - - else - { - //Try to Parse as a daily show - var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); - var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); - - //Swap day and month if month is bigger than 12 (scene fail) - if (airmonth > 12) - { - var tempDay = airday; - airday = airmonth; - airmonth = tempDay; - } - - parsedEpisode = new EpisodeParseResult - { - - AirDate = new DateTime(airyear, airmonth, airday).Date, - }; - } - - parsedEpisode.SeriesTitle = seriesName; - - Logger.Trace("Episode Parsed. {0}", parsedEpisode); - - return parsedEpisode; - } - - public 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 NormalizeTitle(title); - } - - internal static Quality ParseQuality(string name) - { - Logger.Trace("Trying to parse quality for {0}", name); - - name = name.Trim(); - var normalizedName = NormalizeTitle(name); - var result = new Quality { QualityType = QualityTypes.Unknown }; - result.Proper = (normalizedName.Contains("proper") || normalizedName.Contains("repack")); - - if (normalizedName.Contains("dvd") || normalizedName.Contains("bdrip") || normalizedName.Contains("brrip")) - { - result.QualityType = QualityTypes.DVD; - return result; - } - - if (normalizedName.Contains("xvid") || normalizedName.Contains("divx") || normalizedName.Contains("dsr")) - { - if (normalizedName.Contains("bluray")) - { - result.QualityType = QualityTypes.DVD; - return result; - } - - result.QualityType = QualityTypes.SDTV; - return result; - } - - if (normalizedName.Contains("bluray")) - { - if (normalizedName.Contains("720p")) - { - result.QualityType = QualityTypes.Bluray720p; - return result; - } - - if (normalizedName.Contains("1080p")) - { - result.QualityType = QualityTypes.Bluray1080p; - return result; - } - - result.QualityType = QualityTypes.Bluray720p; - return result; - } - if (normalizedName.Contains("webdl")) - { - result.QualityType = QualityTypes.WEBDL; - return result; - } - if (normalizedName.Contains("x264") || normalizedName.Contains("h264") || normalizedName.Contains("720p")) - { - result.QualityType = QualityTypes.HDTV; - return result; - } - //Based on extension - - if (result.QualityType == QualityTypes.Unknown) - { - try - { - switch (Path.GetExtension(name).ToLower()) - { - case ".avi": - case ".xvid": - case ".divx": - case ".wmv": - case ".mp4": - case ".mpg": - case ".mpeg": - case ".mov": - case ".rm": - case ".rmvb": - case ".flv": - case ".dvr-ms": - case ".ogm": - case ".strm": - { - result.QualityType = QualityTypes.SDTV; - break; - } - case ".mkv": - case ".ts": - { - result.QualityType = QualityTypes.HDTV; - break; - } - } - } - catch (ArgumentException) - { - //Swallow exception for cases where string contains illegal - //path characters. - } - } - - if (name.Contains("[HDTV]")) - { - result.QualityType = QualityTypes.HDTV; - return result; - } - - if ((normalizedName.Contains("sdtv") || normalizedName.Contains("pdtv") || - (result.QualityType == QualityTypes.Unknown && normalizedName.Contains("hdtv"))) && - !normalizedName.Contains("mpeg")) - { - result.QualityType = QualityTypes.SDTV; - return result; - } - - return result; - } - - internal static LanguageType ParseLanguage(string title) - { - var lowerTitle = title.ToLower(); - - if (lowerTitle.Contains("english")) - return LanguageType.English; - - if (lowerTitle.Contains("french")) - return LanguageType.French; - - if (lowerTitle.Contains("spanish")) - return LanguageType.Spanish; - - if (lowerTitle.Contains("german")) - { - //Make sure it doesn't contain Germany (Since we're not using REGEX for all this) - if (!lowerTitle.Contains("germany")) - return LanguageType.German; - } - - if (lowerTitle.Contains("italian")) - return LanguageType.Italian; - - if (lowerTitle.Contains("danish")) - return LanguageType.Danish; - - if (lowerTitle.Contains("dutch")) - return LanguageType.Dutch; - - if (lowerTitle.Contains("japanese")) - return LanguageType.Japanese; - - if (lowerTitle.Contains("cantonese")) - return LanguageType.Cantonese; - - if (lowerTitle.Contains("mandarin")) - return LanguageType.Mandarin; - - if (lowerTitle.Contains("korean")) - return LanguageType.Korean; - - if (lowerTitle.Contains("russian")) - return LanguageType.Russian; - - if (lowerTitle.Contains("polish")) - return LanguageType.Polish; - - if (lowerTitle.Contains("vietnamese")) - return LanguageType.Vietnamese; - - if (lowerTitle.Contains("swedish")) - return LanguageType.Swedish; - - if (lowerTitle.Contains("norwegian")) - return LanguageType.Norwegian; - - if (lowerTitle.Contains("finnish")) - return LanguageType.Finnish; - - if (lowerTitle.Contains("turkish")) - return LanguageType.Turkish; - - if (lowerTitle.Contains("portuguese")) - return LanguageType.Portuguese; - - return LanguageType.English; - } - - public static string NormalizeTitle(string title) - { - long number = 0; - - //If Title only contains numbers return it as is. - if (Int64.TryParse(title, out number)) - return title; - - return NormalizeRegex.Replace(title, String.Empty).ToLower(); - } - - public static long GetReportSize(string sizeString) - { - var match = ReportSizeRegex.Matches(sizeString); - - if (match.Count != 0) - { - var cultureInfo = new CultureInfo("en-US"); - var value = Decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), cultureInfo); - - var unit = match[0].Groups["unit"].Value; - - if (unit.Equals("MB", StringComparison.InvariantCultureIgnoreCase) || unit.Equals("MiB", StringComparison.InvariantCultureIgnoreCase)) - return Convert.ToInt64(value * 1048576L); - - if (unit.Equals("GB", StringComparison.InvariantCultureIgnoreCase) || unit.Equals("GiB", StringComparison.InvariantCultureIgnoreCase)) - return Convert.ToInt64(value * 1073741824L); - } - return 0; - } - - internal static string ParseHeader(string header) - { - var match = HeaderRegex.Matches(header); - - if (match.Count != 0) - return match[0].Groups["nzbTitle"].Value; - - return header; - } - } -}��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common; +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[] + { + //Episodes with airdate + new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Multi-Part episodes without a title (S01E05.S01E06) + new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,2}(?!\d+)))+){2,}\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc) + new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,2}(?!\d+)))+){2,}\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) + new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex])(?<episode>\d{2}(?!\d+)))+\W*)+\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) + new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Episodes over 99 (3-digits or more) (S01E105, S01E105E106, etc) + new Regex(@"^(?<title>.*?)(?:\W?S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d+))+)+\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex])(?<episode>\d{2}(?!\d+)))+\W*)+\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) + new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Supports 103/113 naming + new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!p|i|\d+)))+\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Mini-Series, treated as season 1, episodes are labeled as Part01, Part 01, Part.1 + new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Supports 1103/1113 naming + new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+|\(|\[)\d{2})(?<episode>\d{2}(?!p|i|\d+|\)|\])))+\W?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Supports Season only releases + new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))\W?(?<extras>EXTRAS|SUBPACK)?(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled) + }; + + private static readonly Regex NormalizeRegex = new Regex(@"((^|\W)(a|an|the|and|or|of)($|\W))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[x|h|x\s|h\s]264|DD\W?5\W1|\<|\>|\?|\*|\:|\||""", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex ReportSizeRegex = new Regex(@"(?<value>\d+\.\d{1,2}|\d+\,\d+\.\d{1,2})\W?(?<unit>GB|MB|GiB|MiB)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex HeaderRegex = new Regex(@"(?:\[.+\]\-\[.+\]\-\[.+\]\-\[)(?<nzbTitle>.+)(?:\]\-.+)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + internal static EpisodeParseResult ParsePath(string path) + { + var fileInfo = new FileInfo(path); + + var result = ParseTitle(fileInfo.Name); + + if (result == null) + { + Logger.Trace("Attempting to parse episode info using full path. {0}", fileInfo.FullName); + result = ParseTitle(fileInfo.FullName); + } + + if (result != null) + { + result.OriginalString = path; + } + else + { + Logger.Warn("Unable to parse episode info from path {0}", path); + } + + return result; + } + + internal static EpisodeParseResult ParseTitle(string title) + { + try + { + Logger.Trace("Parsing string '{0}'", title); + var simpleTitle = SimpleTitleRegex.Replace(title, String.Empty); + + foreach (var regex in ReportTitleRegex) + { + var match = regex.Matches(simpleTitle); + + if (match.Count != 0) + { + var result = ParseMatchCollection(match); + if (result != null) + { + //Check if episode is in the future (most likley a parse error) + if (result.AirDate > DateTime.Now.AddDays(1).Date) + break; + + result.Language = ParseLanguage(title); + result.Quality = ParseQuality(title); + result.OriginalString = title; + return result; + } + } + } + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while trying to parse " + title, e); + } + + Logger.Trace("Unable to parse {0}", title); + ReportingService.ReportParseError(title); + return null; + } + + private static EpisodeParseResult ParseMatchCollection(MatchCollection matchCollection) + { + var seriesName = matchCollection[0].Groups["title"].Value; + + int airyear; + Int32.TryParse(matchCollection[0].Groups["airyear"].Value, out airyear); + + EpisodeParseResult parsedEpisode; + + if (airyear < 1900) + { + var seasons = new List<int>(); + + foreach (Capture seasonCapture in matchCollection[0].Groups["season"].Captures) + { + int parsedSeason; + if (Int32.TryParse(seasonCapture.Value, out parsedSeason)) + seasons.Add(parsedSeason); + } + + //If no season was found it should be treated as a mini series and season 1 + if (seasons.Count == 0) + seasons.Add(1); + + //If more than 1 season was parsed go to the next REGEX (A multi-season release is unlikely) + if (seasons.Distinct().Count() > 1) + return null; + + parsedEpisode = new EpisodeParseResult + { + SeasonNumber = seasons.First(), + EpisodeNumbers = new List<int>() + }; + + foreach (Match matchGroup in matchCollection) + { + var episodeCaptures = matchGroup.Groups["episode"].Captures.Cast<Capture>().ToList(); + + //Allows use to return a list of 0 episodes (We can handle that as a full season release) + if (episodeCaptures.Any()) + { + var first = Convert.ToInt32(episodeCaptures.First().Value); + var last = Convert.ToInt32(episodeCaptures.Last().Value); + parsedEpisode.EpisodeNumbers = Enumerable.Range(first, last - first + 1).ToList(); + } + else + { + //Check to see if this is an "Extras" or "SUBPACK" release, if it is, return NULL + //Todo: Set a "Extras" flag in EpisodeParseResult if we want to download them ever + if (!String.IsNullOrWhiteSpace(matchCollection[0].Groups["extras"].Value)) + return null; + + parsedEpisode.FullSeason = true; + } + } + } + + else + { + //Try to Parse as a daily show + var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value); + var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value); + + //Swap day and month if month is bigger than 12 (scene fail) + if (airmonth > 12) + { + var tempDay = airday; + airday = airmonth; + airmonth = tempDay; + } + + parsedEpisode = new EpisodeParseResult + { + + AirDate = new DateTime(airyear, airmonth, airday).Date, + }; + } + + parsedEpisode.SeriesTitle = seriesName; + + Logger.Trace("Episode Parsed. {0}", parsedEpisode); + + return parsedEpisode; + } + + public 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 NormalizeTitle(title); + } + + internal static Quality ParseQuality(string name) + { + Logger.Trace("Trying to parse quality for {0}", name); + + name = name.Trim(); + var normalizedName = NormalizeTitle(name); + var result = new Quality { QualityType = QualityTypes.Unknown }; + result.Proper = (normalizedName.Contains("proper") || normalizedName.Contains("repack")); + + if (normalizedName.Contains("dvd") || normalizedName.Contains("bdrip") || normalizedName.Contains("brrip")) + { + result.QualityType = QualityTypes.DVD; + return result; + } + + if (normalizedName.Contains("xvid") || normalizedName.Contains("divx") || normalizedName.Contains("dsr")) + { + if (normalizedName.Contains("bluray")) + { + result.QualityType = QualityTypes.DVD; + return result; + } + + result.QualityType = QualityTypes.SDTV; + return result; + } + + if (normalizedName.Contains("bluray")) + { + if (normalizedName.Contains("720p")) + { + result.QualityType = QualityTypes.Bluray720p; + return result; + } + + if (normalizedName.Contains("1080p")) + { + result.QualityType = QualityTypes.Bluray1080p; + return result; + } + + result.QualityType = QualityTypes.Bluray720p; + return result; + } + if (normalizedName.Contains("webdl")) + { + result.QualityType = QualityTypes.WEBDL; + return result; + } + if (normalizedName.Contains("x264") || normalizedName.Contains("h264") || normalizedName.Contains("720p")) + { + result.QualityType = QualityTypes.HDTV; + return result; + } + //Based on extension + + if (result.QualityType == QualityTypes.Unknown) + { + try + { + switch (Path.GetExtension(name).ToLower()) + { + case ".avi": + case ".xvid": + case ".divx": + case ".wmv": + case ".mp4": + case ".mpg": + case ".mpeg": + case ".mov": + case ".rm": + case ".rmvb": + case ".flv": + case ".dvr-ms": + case ".ogm": + case ".strm": + { + result.QualityType = QualityTypes.SDTV; + break; + } + case ".mkv": + case ".ts": + { + result.QualityType = QualityTypes.HDTV; + break; + } + } + } + catch (ArgumentException) + { + //Swallow exception for cases where string contains illegal + //path characters. + } + } + + if (name.Contains("[HDTV]")) + { + result.QualityType = QualityTypes.HDTV; + return result; + } + + if ((normalizedName.Contains("sdtv") || normalizedName.Contains("pdtv") || + (result.QualityType == QualityTypes.Unknown && normalizedName.Contains("hdtv"))) && + !normalizedName.Contains("mpeg")) + { + result.QualityType = QualityTypes.SDTV; + return result; + } + + return result; + } + + internal static LanguageType ParseLanguage(string title) + { + var lowerTitle = title.ToLower(); + + if (lowerTitle.Contains("english")) + return LanguageType.English; + + if (lowerTitle.Contains("french")) + return LanguageType.French; + + if (lowerTitle.Contains("spanish")) + return LanguageType.Spanish; + + if (lowerTitle.Contains("german")) + { + //Make sure it doesn't contain Germany (Since we're not using REGEX for all this) + if (!lowerTitle.Contains("germany")) + return LanguageType.German; + } + + if (lowerTitle.Contains("italian")) + return LanguageType.Italian; + + if (lowerTitle.Contains("danish")) + return LanguageType.Danish; + + if (lowerTitle.Contains("dutch")) + return LanguageType.Dutch; + + if (lowerTitle.Contains("japanese")) + return LanguageType.Japanese; + + if (lowerTitle.Contains("cantonese")) + return LanguageType.Cantonese; + + if (lowerTitle.Contains("mandarin")) + return LanguageType.Mandarin; + + if (lowerTitle.Contains("korean")) + return LanguageType.Korean; + + if (lowerTitle.Contains("russian")) + return LanguageType.Russian; + + if (lowerTitle.Contains("polish")) + return LanguageType.Polish; + + if (lowerTitle.Contains("vietnamese")) + return LanguageType.Vietnamese; + + if (lowerTitle.Contains("swedish")) + return LanguageType.Swedish; + + if (lowerTitle.Contains("norwegian")) + return LanguageType.Norwegian; + + if (lowerTitle.Contains("finnish")) + return LanguageType.Finnish; + + if (lowerTitle.Contains("turkish")) + return LanguageType.Turkish; + + if (lowerTitle.Contains("portuguese")) + return LanguageType.Portuguese; + + return LanguageType.English; + } + + internal static string ParseReleaseGroup(string name) + { + Logger.Trace("Trying to parse release group for {0}", name); + + name = name.Trim(); + var index = name.LastIndexOf('-'); + + if (index < 0) + index = name.LastIndexOf(' '); + + var group = name.Substring(index + 1); + + if (group.Length == name.Length) + return String.Empty; + + Logger.Trace("Release Group found: {0}", group); + return group; + } + + public static string NormalizeTitle(string title) + { + long number = 0; + + //If Title only contains numbers return it as is. + if (Int64.TryParse(title, out number)) + return title; + + return NormalizeRegex.Replace(title, String.Empty).ToLower(); + } + + public static long GetReportSize(string sizeString) + { + var match = ReportSizeRegex.Matches(sizeString); + + if (match.Count != 0) + { + var cultureInfo = new CultureInfo("en-US"); + var value = Decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), cultureInfo); + + var unit = match[0].Groups["unit"].Value; + + if (unit.Equals("MB", StringComparison.InvariantCultureIgnoreCase) || unit.Equals("MiB", StringComparison.InvariantCultureIgnoreCase)) + return Convert.ToInt64(value * 1048576L); + + if (unit.Equals("GB", StringComparison.InvariantCultureIgnoreCase) || unit.Equals("GiB", StringComparison.InvariantCultureIgnoreCase)) + return Convert.ToInt64(value * 1073741824L); + } + return 0; + } + + internal static string ParseHeader(string header) + { + var match = HeaderRegex.Matches(header); + + if (match.Count != 0) + return match[0].Groups["nzbTitle"].Value; + + return header; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index c2637dc3c..a296c23e5 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -214,6 +214,12 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("Sorting_MultiEpisodeStyle", value); } } + public virtual bool SortingUseSceneName + { + get { return GetValueBoolean("Sorting_UseSceneName", false); } + set { SetValue("Sorting_UseSceneName", value); } + } + public virtual int DefaultQualityProfile { get { return GetValueInt("DefaultQualityProfile", 1); } diff --git a/NzbDrone.Core/Providers/DiskScanProvider.cs b/NzbDrone.Core/Providers/DiskScanProvider.cs index 00705ec35..dbad6d008 100644 --- a/NzbDrone.Core/Providers/DiskScanProvider.cs +++ b/NzbDrone.Core/Providers/DiskScanProvider.cs @@ -153,6 +153,7 @@ namespace NzbDrone.Core.Providers episodeFile.Quality = parseResult.Quality.QualityType; episodeFile.Proper = parseResult.Quality.Proper; episodeFile.SeasonNumber = parseResult.SeasonNumber; + episodeFile.SceneName = Path.GetFileNameWithoutExtension(filePath.NormalizePath()); var fileId = _mediaFileProvider.Add(episodeFile); //Link file to all episodes @@ -175,7 +176,7 @@ namespace NzbDrone.Core.Providers var series = _seriesProvider.GetSeries(episodeFile.SeriesId); var episodes = _episodeProvider.GetEpisodesByFileId(episodeFile.EpisodeFileId); - string newFileName = _mediaFileProvider.GetNewFilename(episodes, series.Title, episodeFile.Quality, episodeFile.Proper); + string newFileName = _mediaFileProvider.GetNewFilename(episodes, series.Title, episodeFile.Quality, episodeFile.Proper, episodeFile); var newFile = _mediaFileProvider.CalculateFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); //Only rename if existing and new filenames don't match diff --git a/NzbDrone.Core/Providers/MediaFileProvider.cs b/NzbDrone.Core/Providers/MediaFileProvider.cs index a45b7f9fe..831cb60f3 100644 --- a/NzbDrone.Core/Providers/MediaFileProvider.cs +++ b/NzbDrone.Core/Providers/MediaFileProvider.cs @@ -142,8 +142,22 @@ namespace NzbDrone.Core.Providers } } - public virtual string GetNewFilename(IList<Episode> episodes, string seriesTitle, QualityTypes quality, bool proper) + public virtual string GetNewFilename(IList<Episode> episodes, string seriesTitle, QualityTypes quality, bool proper, EpisodeFile episodeFile) { + if (_configProvider.SortingUseSceneName) + { + Logger.Trace("Attempting to use scene name"); + if (String.IsNullOrWhiteSpace(episodeFile.SceneName)) + { + var name = Path.GetFileNameWithoutExtension(episodeFile.Path); + Logger.Trace("Unable to use scene name, because it is null, sticking with current name: {0}", name); + + return name; + } + + return episodeFile.SceneName; + } + var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber); var separatorStyle = EpisodeSortingHelper.GetSeparatorStyle(_configProvider.SortingSeparatorStyle); diff --git a/NzbDrone.Core/Providers/MisnamedProvider.cs b/NzbDrone.Core/Providers/MisnamedProvider.cs index 0b92e3397..f50f2edda 100644 --- a/NzbDrone.Core/Providers/MisnamedProvider.cs +++ b/NzbDrone.Core/Providers/MisnamedProvider.cs @@ -29,7 +29,6 @@ namespace NzbDrone.Core.Providers var episodesWithFiles = _episodeProvider.EpisodesWithFiles().GroupBy(e => e.EpisodeFileId).ToList(); totalItems = episodesWithFiles.Count(); - var stopwatch = new Stopwatch(); stopwatch.Start(); @@ -37,7 +36,7 @@ namespace NzbDrone.Core.Providers w => w.First().EpisodeFile.Path != _mediaFileProvider.GetNewFilename(w.Select(e => e).ToList(), w.First().Series.Title, - w.First().EpisodeFile.Quality, w.First().EpisodeFile.Proper)).Skip(Math.Max(pageSize * (pageNumber - 1), 0)).Take(pageSize); + w.First().EpisodeFile.Quality, w.First().EpisodeFile.Proper, w.First().EpisodeFile)).Skip(Math.Max(pageSize * (pageNumber - 1), 0)).Take(pageSize); //Process the episodes misnamedFilesSelect.AsParallel().ForAll(f => @@ -46,7 +45,7 @@ namespace NzbDrone.Core.Providers var firstEpisode = episodes[0]; var properName = _mediaFileProvider.GetNewFilename(episodes, firstEpisode.Series.Title, - firstEpisode.EpisodeFile.Quality, firstEpisode.EpisodeFile.Proper); + firstEpisode.EpisodeFile.Quality, firstEpisode.EpisodeFile.Proper, firstEpisode.EpisodeFile); var currentName = Path.GetFileNameWithoutExtension(firstEpisode.EpisodeFile.Path); diff --git a/NzbDrone.Core/Repository/EpisodeFile.cs b/NzbDrone.Core/Repository/EpisodeFile.cs index 01b17f3b7..86215ef9c 100644 --- a/NzbDrone.Core/Repository/EpisodeFile.cs +++ b/NzbDrone.Core/Repository/EpisodeFile.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Repository public bool Proper { get; set; } public long Size { get; set; } public DateTime DateAdded { get; set; } + public string SceneName { get; set; } [Ignore] public Model.Quality QualityWrapper diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index a22fca30a..33d6e6882 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -205,6 +205,7 @@ namespace NzbDrone.Web.Controllers model.SeparatorStyle = _configProvider.SortingSeparatorStyle; model.NumberStyle = _configProvider.SortingNumberStyle; model.MultiEpisodeStyle = _configProvider.SortingMultiEpisodeStyle; + model.SceneName = _configProvider.SortingUseSceneName; model.SeparatorStyles = new SelectList(EpisodeSortingHelper.GetSeparatorStyles(), "Id", "Name"); model.NumberStyles = new SelectList(EpisodeSortingHelper.GetNumberStyles(), "Id", "Name"); @@ -594,6 +595,7 @@ namespace NzbDrone.Web.Controllers _configProvider.SortingSeparatorStyle = data.SeparatorStyle; _configProvider.SortingNumberStyle = data.NumberStyle; _configProvider.SortingMultiEpisodeStyle = data.MultiEpisodeStyle; + _configProvider.SortingUseSceneName = data.SceneName; //Metadata _configProvider.MetadataUseBanners = data.MetadataUseBanners; diff --git a/NzbDrone.Web/Models/EpisodeNamingModel.cs b/NzbDrone.Web/Models/EpisodeNamingModel.cs index 3331f5883..59f58ec88 100644 --- a/NzbDrone.Web/Models/EpisodeNamingModel.cs +++ b/NzbDrone.Web/Models/EpisodeNamingModel.cs @@ -43,6 +43,10 @@ namespace NzbDrone.Web.Models [Description("How will multi-episode files be named?")] public int MultiEpisodeStyle { get; set; } + [DisplayName("Use Scene Name")] + [Description("Use the scene name, ignoring all other naming settings?")] + public bool SceneName { get; set; } + [DisplayName("XBMC")] [Description("Enable creating metadata for XBMC")] public bool MetadataXbmcEnabled { get; set; } diff --git a/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml b/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml index b751a045e..83c1a6713 100644 --- a/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml +++ b/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml @@ -42,6 +42,10 @@ <span class="small">@Html.DescriptionFor(m => m.MultiEpisodeStyle)</span> </label> @Html.DropDownListFor(m => m.MultiEpisodeStyle, Model.MultiEpisodeStyles, new { @class = "inputClass selectClass" }) + <label class="labelClass">@Html.LabelFor(m => m.SceneName) + <span class="small">@Html.DescriptionFor(m => m.SceneName)</span> + </label> + @Html.CheckBoxFor(m => m.SceneName, new { @class = "inputClass checkClass" }) </div> <div id="examples"> <div id="singleEpisodeExample"> From d6c90e7f36ebab1eca371bfd1d1d7e2a490e9d57 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 3 Aug 2012 08:18:21 -0700 Subject: [PATCH 2/8] Moved Use Scene Name to top of checkboxes --- NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml b/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml index 83c1a6713..bec5a1c5e 100644 --- a/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml +++ b/NzbDrone.Web/Views/Settings/EpisodeNamingPartial.cshtml @@ -6,6 +6,10 @@ <div class="settingsContainer"> @Html.ValidationSummary(true, "Unable to save your settings. Please correct the errors and try again.") + <label class="labelClass">@Html.LabelFor(m => m.SceneName) + <span class="small">@Html.DescriptionFor(m => m.SceneName)</span> + </label> + @Html.CheckBoxFor(m => m.SceneName, new { @class = "inputClass checkClass" }) <label class="labelClass">@Html.LabelFor(m => m.SeriesName) <span class="small">@Html.DescriptionFor(m => m.SeriesName)</span> </label> @@ -42,10 +46,6 @@ <span class="small">@Html.DescriptionFor(m => m.MultiEpisodeStyle)</span> </label> @Html.DropDownListFor(m => m.MultiEpisodeStyle, Model.MultiEpisodeStyles, new { @class = "inputClass selectClass" }) - <label class="labelClass">@Html.LabelFor(m => m.SceneName) - <span class="small">@Html.DescriptionFor(m => m.SceneName)</span> - </label> - @Html.CheckBoxFor(m => m.SceneName, new { @class = "inputClass checkClass" }) </div> <div id="examples"> <div id="singleEpisodeExample"> From 6f3262c68abe7e0a024ba4dc95a80ede1f676f13 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 5 Aug 2012 23:33:07 -0700 Subject: [PATCH 3/8] Release group is added to history and episodefile --- .../Datastore/Migrations/Migration20120802.cs | 2 ++ NzbDrone.Core/Model/EpisodeParseResult.cs | 2 ++ NzbDrone.Core/Parser.cs | 18 +++++++++------- NzbDrone.Core/Providers/DiskScanProvider.cs | 1 + NzbDrone.Core/Providers/DownloadProvider.cs | 1 + .../Providers/Indexer/IndexerBase.cs | 1 - NzbDrone.Core/Providers/Indexer/Newzbin.cs | 21 ++++++++++++++++--- NzbDrone.Core/Repository/EpisodeFile.cs | 1 + NzbDrone.Core/Repository/History.cs | 1 + 9 files changed, 37 insertions(+), 11 deletions(-) diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs b/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs index 203d08874..fbe04ca84 100644 --- a/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs +++ b/NzbDrone.Core/Datastore/Migrations/Migration20120802.cs @@ -12,6 +12,8 @@ namespace NzbDrone.Core.Datastore.Migrations protected override void MainDbUpgrade() { Database.AddColumn("EpisodeFiles", new Column("SceneName", DbType.String, ColumnProperty.Null)); + Database.AddColumn("EpisodeFiles", new Column("ReleaseGroup", DbType.String, ColumnProperty.Null)); + Database.AddColumn("History", new Column("ReleaseGroup", DbType.String, ColumnProperty.Null)); } } } \ No newline at end of file diff --git a/NzbDrone.Core/Model/EpisodeParseResult.cs b/NzbDrone.Core/Model/EpisodeParseResult.cs index 97a4a78a4..e2385d5ff 100644 --- a/NzbDrone.Core/Model/EpisodeParseResult.cs +++ b/NzbDrone.Core/Model/EpisodeParseResult.cs @@ -44,6 +44,8 @@ namespace NzbDrone.Core.Model public int Age { get; set; } + public string ReleaseGroup { get; set; } + public override string ToString() { diff --git a/NzbDrone.Core/Parser.cs b/NzbDrone.Core/Parser.cs index 462f81ad8..a151e0ef5 100644 --- a/NzbDrone.Core/Parser.cs +++ b/NzbDrone.Core/Parser.cs @@ -124,6 +124,7 @@ namespace NzbDrone.Core result.Language = ParseLanguage(title); result.Quality = ParseQuality(title); result.OriginalString = title; + result.ReleaseGroup = ParseReleaseGroup(title); return result; } } @@ -424,19 +425,22 @@ namespace NzbDrone.Core return LanguageType.English; } - internal static string ParseReleaseGroup(string name) + internal static string ParseReleaseGroup(string title) { - Logger.Trace("Trying to parse release group for {0}", name); + Logger.Trace("Trying to parse release group for {0}", title); - name = name.Trim(); - var index = name.LastIndexOf('-'); + title = title.Trim(); + var index = title.LastIndexOf('-'); if (index < 0) - index = name.LastIndexOf(' '); + index = title.LastIndexOf(' '); - var group = name.Substring(index + 1); + if (index < 0) + return String.Empty; - if (group.Length == name.Length) + var group = title.Substring(index + 1); + + if (group.Length == title.Length) return String.Empty; Logger.Trace("Release Group found: {0}", group); diff --git a/NzbDrone.Core/Providers/DiskScanProvider.cs b/NzbDrone.Core/Providers/DiskScanProvider.cs index dbad6d008..429a9b2bf 100644 --- a/NzbDrone.Core/Providers/DiskScanProvider.cs +++ b/NzbDrone.Core/Providers/DiskScanProvider.cs @@ -154,6 +154,7 @@ namespace NzbDrone.Core.Providers episodeFile.Proper = parseResult.Quality.Proper; episodeFile.SeasonNumber = parseResult.SeasonNumber; episodeFile.SceneName = Path.GetFileNameWithoutExtension(filePath.NormalizePath()); + episodeFile.ReleaseGroup = parseResult.ReleaseGroup; var fileId = _mediaFileProvider.Add(episodeFile); //Link file to all episodes diff --git a/NzbDrone.Core/Providers/DownloadProvider.cs b/NzbDrone.Core/Providers/DownloadProvider.cs index e70f87e74..d9ab4f3b2 100644 --- a/NzbDrone.Core/Providers/DownloadProvider.cs +++ b/NzbDrone.Core/Providers/DownloadProvider.cs @@ -63,6 +63,7 @@ namespace NzbDrone.Core.Providers history.EpisodeId = episode.EpisodeId; history.SeriesId = episode.SeriesId; history.NzbInfoUrl = parseResult.NzbInfoUrl; + history.ReleaseGroup = parseResult.ReleaseGroup; _historyProvider.Add(history); _episodeProvider.MarkEpisodeAsFetched(episode.EpisodeId); diff --git a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs index 626104378..f6089a23c 100644 --- a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs +++ b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs @@ -63,7 +63,6 @@ namespace NzbDrone.Core.Providers.Indexer get { return null; } } - protected abstract IList<String> GetEpisodeSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber); protected abstract IList<String> GetDailyEpisodeSearchUrls(string seriesTitle, DateTime date); protected abstract IList<String> GetSeasonSearchUrls(string seriesTitle, int seasonNumber); diff --git a/NzbDrone.Core/Providers/Indexer/Newzbin.cs b/NzbDrone.Core/Providers/Indexer/Newzbin.cs index 733f70ce3..db91708cf 100644 --- a/NzbDrone.Core/Providers/Indexer/Newzbin.cs +++ b/NzbDrone.Core/Providers/Indexer/Newzbin.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.ServiceModel.Syndication; using System.Text.RegularExpressions; +using System.Xml.Linq; using Ninject; using NzbDrone.Common; using NzbDrone.Core.Model; @@ -44,7 +46,6 @@ namespace NzbDrone.Core.Providers.Indexer } } - protected override NetworkCredential Credentials { get { return new NetworkCredential(_configProvider.NewzbinUsername, _configProvider.NewzbinPassword); } @@ -90,7 +91,6 @@ namespace NzbDrone.Core.Providers.Indexer }; } - public override string Name { get { return "Newzbin"; } @@ -118,10 +118,25 @@ namespace NzbDrone.Core.Providers.Indexer var sizeString = Regex.Match(item.Summary.Text, @"\(Size: \d*\,?\d+\.\d{1,2}\w{2}\)", RegexOptions.IgnoreCase).Value; currentResult.Size = Parser.GetReportSize(sizeString); + + try + { + var releaseGroupText = item.ElementExtensions.Single(s => s.OuterName == "nfo") + .GetObject<XElement>() + .Element(XName.Get("filename", "http://www.newzbin2.es/DTD/2007/feeds/report/")) + .Value; + + var releaseGroup = Parser.ParseReleaseGroup(releaseGroupText.Replace(".nfo", "")); + currentResult.ReleaseGroup = releaseGroup; + } + catch(Exception ex) + { + _logger.TraceException("Error getting release group for newzbin release", ex); + currentResult.ReleaseGroup = ""; + } } return currentResult; } - } } \ No newline at end of file diff --git a/NzbDrone.Core/Repository/EpisodeFile.cs b/NzbDrone.Core/Repository/EpisodeFile.cs index 86215ef9c..32758b8ce 100644 --- a/NzbDrone.Core/Repository/EpisodeFile.cs +++ b/NzbDrone.Core/Repository/EpisodeFile.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.Repository public long Size { get; set; } public DateTime DateAdded { get; set; } public string SceneName { get; set; } + public string ReleaseGroup { get; set; } [Ignore] public Model.Quality QualityWrapper diff --git a/NzbDrone.Core/Repository/History.cs b/NzbDrone.Core/Repository/History.cs index 0c00ff2c9..597aab3da 100644 --- a/NzbDrone.Core/Repository/History.cs +++ b/NzbDrone.Core/Repository/History.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Repository public bool IsProper { get; set; } public string Indexer { get; set; } public string NzbInfoUrl { get; set; } + public string ReleaseGroup { get; set; } [ResultColumn] public Episode Episode { get; set; } From 2a316e0b989ba5c708efbaf3cdc7d7a2a42e06c5 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Mon, 6 Aug 2012 12:10:41 -0700 Subject: [PATCH 4/8] Added test for Newzbin release group --- .../Files/RSS/SizeParsing/newzbin.xml | 4 ++-- NzbDrone.Core.Test/Files/RSS/newzbin.xml | 2 +- NzbDrone.Core.Test/IndexerTests.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/NzbDrone.Core.Test/Files/RSS/SizeParsing/newzbin.xml b/NzbDrone.Core.Test/Files/RSS/SizeParsing/newzbin.xml index 220f672d2..502d1b85f 100644 --- a/NzbDrone.Core.Test/Files/RSS/SizeParsing/newzbin.xml +++ b/NzbDrone.Core.Test/Files/RSS/SizeParsing/newzbin.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8" ?> <rss version="2.0" xml:lang="en-GB" -xmlns:report="http://www.newzbin.com/DTD/2007/feeds/report/"> +xmlns:report="http://www.newzbin2.es/DTD/2007/feeds/report/"> <channel> <title>www.newzbin.com (reports) http://www.newzbin.com/browse/category/p/tv/ @@ -63,7 +63,7 @@ xmlns:report="http://www.newzbin.com/DTD/2007/feeds/report/"> http://www.tvrage.com/Rookie_Blue/episodes/1064943067/1x10/ 373966350 - tvp-rookieblue-s01e10-720p.nfo + rookieblue-s01e10-720p-tvp.nfo http://www.newzbin.com/nfo/view/txt/373966350/ http://www.newzbin.com/browse/post/6076287/nzb/ diff --git a/NzbDrone.Core.Test/Files/RSS/newzbin.xml b/NzbDrone.Core.Test/Files/RSS/newzbin.xml index 99b5bb09d..909632fb9 100644 --- a/NzbDrone.Core.Test/Files/RSS/newzbin.xml +++ b/NzbDrone.Core.Test/Files/RSS/newzbin.xml @@ -1,6 +1,6 @@  +xmlns:report="http://www.newzbin2.es/DTD/2007/feeds/report/"> www.newzbin.com (reports) http://www.newzbin.com/browse/category/p/tv/ diff --git a/NzbDrone.Core.Test/IndexerTests.cs b/NzbDrone.Core.Test/IndexerTests.cs index c51defcee..d843dac5c 100644 --- a/NzbDrone.Core.Test/IndexerTests.cs +++ b/NzbDrone.Core.Test/IndexerTests.cs @@ -692,5 +692,21 @@ namespace NzbDrone.Core.Test episodeParseResult.NzbInfoUrl.Should().Contain(expectedString); } } + + [Test] + public void releaseGroup_should_use_nfo_filename_for_newzbin() + { + WithConfiguredIndexers(); + + Mocker.GetMock() + .Setup(h => h.DownloadStream(It.IsAny(), It.IsAny())) + .Returns(File.OpenRead(".\\Files\\Rss\\SizeParsing\\newzbin.xml")); + + //Act + var parseResults = Mocker.Resolve().FetchRss(); + + parseResults.Should().HaveCount(1); + parseResults[0].ReleaseGroup.Should().Be("tvp"); + } } } From 67064ec4956e38d43fad386b0ceccfe68636be5a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 6 Aug 2012 22:24:15 -0700 Subject: [PATCH 5/8] Restrict nzbs based on release group, server side New: Only grab NZBs if release group is wanted (configurable) --- NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 1 + ...AllowedReleaseGroupSpecificationFixture.cs | 69 +++++++++++++++++++ NzbDrone.Core/Model/ReportRejectionType.cs | 1 + NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Providers/Core/ConfigProvider.cs | 6 ++ .../AllowedDownloadSpecification.cs | 8 ++- .../AllowedReleaseGroupSpecification.cs | 48 +++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedReleaseGroupSpecificationFixture.cs create mode 100644 NzbDrone.Core/Providers/DecisionEngine/AllowedReleaseGroupSpecification.cs diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index cfaef9e0f..93eea64bf 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -114,6 +114,7 @@ + diff --git a/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedReleaseGroupSpecificationFixture.cs b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedReleaseGroupSpecificationFixture.cs new file mode 100644 index 000000000..760496dcb --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedReleaseGroupSpecificationFixture.cs @@ -0,0 +1,69 @@ +// ReSharper disable RedundantUsingDirective + +using System.Linq; +using System; +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ProviderTests.DecisionEngineTests +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class AllowedReleaseGroupSpecificationFixture : CoreTest + { + private EpisodeParseResult parseResult; + + [SetUp] + public void Setup() + { + parseResult = new EpisodeParseResult + { + SeriesTitle = "Title", + Language = LanguageType.English, + Quality = new Quality(QualityTypes.SDTV, true), + EpisodeNumbers = new List { 3 }, + SeasonNumber = 12, + AirDate = DateTime.Now.AddDays(-12).Date, + ReleaseGroup = "2HD" + }; + } + + [Test] + public void should_be_true_when_allowedReleaseGroups_is_empty() + { + Mocker.GetMock().SetupGet(s => s.AllowedReleaseGroups).Returns(String.Empty); + Mocker.Resolve().IsSatisfiedBy(parseResult).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_allowedReleaseGroups_is_nzbs_releaseGroup() + { + Mocker.GetMock().SetupGet(s => s.AllowedReleaseGroups).Returns("2HD"); + Mocker.Resolve().IsSatisfiedBy(parseResult).Should().BeTrue(); + } + + [Test] + public void should_be_true_when_allowedReleaseGroups_contains_nzbs_releaseGroup() + { + Mocker.GetMock().SetupGet(s => s.AllowedReleaseGroups).Returns("2HD, LOL"); + Mocker.Resolve().IsSatisfiedBy(parseResult).Should().BeTrue(); + } + + [Test] + public void should_be_false_when_allowedReleaseGroups_does_not_contain_nzbs_releaseGroup() + { + Mocker.GetMock().SetupGet(s => s.AllowedReleaseGroups).Returns("LOL,DTD"); + Mocker.Resolve().IsSatisfiedBy(parseResult).Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Model/ReportRejectionType.cs b/NzbDrone.Core/Model/ReportRejectionType.cs index 867f37565..b4cf7336f 100644 --- a/NzbDrone.Core/Model/ReportRejectionType.cs +++ b/NzbDrone.Core/Model/ReportRejectionType.cs @@ -17,5 +17,6 @@ namespace NzbDrone.Core.Model DownloadClientFailure = 10, Skipped = 11, Failure = 12, + ReleaseGroupNotWanted = 13 } } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 99eef5003..00692a8c2 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -289,6 +289,7 @@ + diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index a296c23e5..605af206b 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -514,6 +514,12 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("MetadataUseBanners", value); } } + public virtual string AllowedReleaseGroups + { + get { return GetValue("AllowedReleaseGroups"); } + set { SetValue("AllowedReleaseGroups", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs index c6e437c5f..df58a271f 100644 --- a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs +++ b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs @@ -13,18 +13,21 @@ namespace NzbDrone.Core.Providers.DecisionEngine private readonly AcceptableSizeSpecification _acceptableSizeSpecification; private readonly AlreadyInQueueSpecification _alreadyInQueueSpecification; private readonly RetentionSpecification _retentionSpecification; + private readonly AllowedReleaseGroupSpecification _allowedReleaseGroupSpecification; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); [Inject] public AllowedDownloadSpecification(QualityAllowedByProfileSpecification qualityAllowedByProfileSpecification, UpgradeDiskSpecification upgradeDiskSpecification, AcceptableSizeSpecification acceptableSizeSpecification, - AlreadyInQueueSpecification alreadyInQueueSpecification, RetentionSpecification retentionSpecification) + AlreadyInQueueSpecification alreadyInQueueSpecification, RetentionSpecification retentionSpecification, + AllowedReleaseGroupSpecification allowedReleaseGroupSpecification) { _qualityAllowedByProfileSpecification = qualityAllowedByProfileSpecification; _upgradeDiskSpecification = upgradeDiskSpecification; _acceptableSizeSpecification = acceptableSizeSpecification; _alreadyInQueueSpecification = alreadyInQueueSpecification; _retentionSpecification = retentionSpecification; + _allowedReleaseGroupSpecification = allowedReleaseGroupSpecification; } public AllowedDownloadSpecification() @@ -37,8 +40,9 @@ namespace NzbDrone.Core.Providers.DecisionEngine if (!_upgradeDiskSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.ExistingQualityIsEqualOrBetter; if (!_retentionSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Retention; if (!_acceptableSizeSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Size; + if (!_allowedReleaseGroupSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.ReleaseGroupNotWanted; if (_alreadyInQueueSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.AlreadyInQueue; - + logger.Debug("Episode {0} is needed", subject); return ReportRejectionType.None; } diff --git a/NzbDrone.Core/Providers/DecisionEngine/AllowedReleaseGroupSpecification.cs b/NzbDrone.Core/Providers/DecisionEngine/AllowedReleaseGroupSpecification.cs new file mode 100644 index 000000000..4d24886ea --- /dev/null +++ b/NzbDrone.Core/Providers/DecisionEngine/AllowedReleaseGroupSpecification.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using NLog; +using Ninject; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers.Core; + +namespace NzbDrone.Core.Providers.DecisionEngine +{ + public class AllowedReleaseGroupSpecification + { + private readonly ConfigProvider _configProvider; + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public AllowedReleaseGroupSpecification(ConfigProvider configProvider) + { + _configProvider = configProvider; + } + + public AllowedReleaseGroupSpecification() + { + + } + + public virtual bool IsSatisfiedBy(EpisodeParseResult subject) + { + logger.Trace("Beginning release group check for: {0}", subject); + + var allowed = _configProvider.AllowedReleaseGroups; + + if (string.IsNullOrWhiteSpace(allowed)) + return true; + + foreach(var group in allowed.Trim(',', ' ').Split(',')) + { + if (subject.ReleaseGroup.Equals(group.Trim(' '), StringComparison.CurrentCultureIgnoreCase)) + { + logger.Trace("Item: {0}'s release group is wanted: {1}", subject, subject.ReleaseGroup); + return true; + } + } + + logger.Trace("Item: {0}'s release group is not wanted: {1}", subject, subject.ReleaseGroup); + return false; + } + } +} From 33274d04f0ab243f5e7ab6360ee5c738722819f1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 6 Aug 2012 22:32:07 -0700 Subject: [PATCH 6/8] Allowed release group added to UI. --- NzbDrone.Web/Controllers/SettingsController.cs | 2 ++ NzbDrone.Web/Models/MiscSettingsModel.cs | 4 ++++ NzbDrone.Web/Views/Settings/Misc.cshtml | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index 33d6e6882..6d2cfb7f6 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -244,6 +244,7 @@ namespace NzbDrone.Web.Controllers var model = new MiscSettingsModel(); model.EnableBacklogSearching = _configProvider.EnableBacklogSearching; model.AutoIgnorePreviouslyDownloadedEpisodes = _configProvider.AutoIgnorePreviouslyDownloadedEpisodes; + model.AllowedReleaseGroups = _configProvider.AllowedReleaseGroups; return View(model); } @@ -633,6 +634,7 @@ namespace NzbDrone.Web.Controllers { _configProvider.EnableBacklogSearching = data.EnableBacklogSearching; _configProvider.AutoIgnorePreviouslyDownloadedEpisodes = data.AutoIgnorePreviouslyDownloadedEpisodes; + _configProvider.AllowedReleaseGroups = data.AllowedReleaseGroups; return GetSuccessResult(); } diff --git a/NzbDrone.Web/Models/MiscSettingsModel.cs b/NzbDrone.Web/Models/MiscSettingsModel.cs index 82cb57d30..d84550ed9 100644 --- a/NzbDrone.Web/Models/MiscSettingsModel.cs +++ b/NzbDrone.Web/Models/MiscSettingsModel.cs @@ -15,5 +15,9 @@ namespace NzbDrone.Web.Models [DisplayName("Automatically Ignore Deleted Episodes")] [Description("Should NzbDrone automatically ignore episodes that were deleted from disk?")] public bool AutoIgnorePreviouslyDownloadedEpisodes { get; set; } + + [DisplayName("Allowed Release Groups")] + [Description("Comma separated list of release groups to download episodes")] + public string AllowedReleaseGroups { get; set; } } } \ No newline at end of file diff --git a/NzbDrone.Web/Views/Settings/Misc.cshtml b/NzbDrone.Web/Views/Settings/Misc.cshtml index 80ca1a904..570c420e3 100644 --- a/NzbDrone.Web/Views/Settings/Misc.cshtml +++ b/NzbDrone.Web/Views/Settings/Misc.cshtml @@ -19,6 +19,11 @@ @Html.CheckBoxFor(m => m.AutoIgnorePreviouslyDownloadedEpisodes, new { @class = "inputClass checkClass" }) + + @Html.TextBoxFor(m => m.AllowedReleaseGroups, new { @class = "inputClass" }) +
From 0867f16e2a18ad17613587e0f76390117f6c2b1e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 6 Aug 2012 22:41:56 -0700 Subject: [PATCH 7/8] Release group will be shown on history now. --- NzbDrone.Web/Models/HistoryModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/NzbDrone.Web/Models/HistoryModel.cs b/NzbDrone.Web/Models/HistoryModel.cs index 2f60a9d6d..dfa46a463 100644 --- a/NzbDrone.Web/Models/HistoryModel.cs +++ b/NzbDrone.Web/Models/HistoryModel.cs @@ -21,5 +21,6 @@ namespace NzbDrone.Web.Models public int EpisodeId { get; set; } public string Details { get; set; } public string NzbInfoUrl { get; set; } + public string ReleaseGroup { get; set; } } } \ No newline at end of file From 5665f6498839cc3b624e723de213ec02aad98dc9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 7 Aug 2012 16:46:23 -0700 Subject: [PATCH 8/8] Release group shown on history details, take 2 --- NzbDrone.Core/Model/HistoryQueryModel.cs | 1 + NzbDrone.Web/Controllers/HistoryController.cs | 3 ++- NzbDrone.Web/Views/History/Index.cshtml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NzbDrone.Core/Model/HistoryQueryModel.cs b/NzbDrone.Core/Model/HistoryQueryModel.cs index 00a018c2b..198178f60 100644 --- a/NzbDrone.Core/Model/HistoryQueryModel.cs +++ b/NzbDrone.Core/Model/HistoryQueryModel.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Model public bool IsProper { get; set; } public string Indexer { get; set; } public string NzbInfoUrl { get; set; } + public string ReleaseGroup { get; set; } public string EpisodeTitle { get; set; } public int SeasonNumber { get; set; } diff --git a/NzbDrone.Web/Controllers/HistoryController.cs b/NzbDrone.Web/Controllers/HistoryController.cs index be528a0ef..e5d050f6e 100644 --- a/NzbDrone.Web/Controllers/HistoryController.cs +++ b/NzbDrone.Web/Controllers/HistoryController.cs @@ -48,7 +48,8 @@ namespace NzbDrone.Web.Controllers DateSorter = h.Date.ToString("MM/dd/yyyy h:mm:ss tt"), Indexer = h.Indexer, EpisodeId = h.EpisodeId, - NzbInfoUrl = h.NzbInfoUrl + NzbInfoUrl = h.NzbInfoUrl, + ReleaseGroup = h.ReleaseGroup }); return Json(new diff --git a/NzbDrone.Web/Views/History/Index.cshtml b/NzbDrone.Web/Views/History/Index.cshtml index f7bad9ef1..2890e31b0 100644 --- a/NzbDrone.Web/Views/History/Index.cshtml +++ b/NzbDrone.Web/Views/History/Index.cshtml @@ -112,6 +112,9 @@ "Proper: " + row.aData["IsProper"] + "
" + "Indexer: " + row.aData["Indexer"].replace('_', ' - '); + if (row.aData["ReleaseGroup"] != null && row.aData["ReleaseGroup"] != "") + result += "
Release Group: " + row.aData["ReleaseGroup"]; + if (row.aData["NzbInfoUrl"] != null && row.aData["NzbInfoUrl"] != "") result += "
Nzb Details: Details";