From bee69140626f3b1b897d1fa06950b10f864e68b5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 3 Aug 2012 00:01:34 -0700 Subject: [PATCH] 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">