diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs index e2b7b5bb1..49075ac06 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.OrganizerTests private void GivenProper() { - _episodeFile.Quality.Revision.Version =2; + _episodeFile.Quality.Revision.Version = 2; } [Test] @@ -214,13 +214,13 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - public void should_replace_quality_title_with_proper() + public void should_replace_quality_proper_with_proper() { - _namingConfig.StandardEpisodeFormat = "{Quality Title}"; + _namingConfig.StandardEpisodeFormat = "{Quality Proper}"; GivenProper(); Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be("HDTV-720p Proper"); + .Should().Be("Proper"); } [Test] @@ -564,10 +564,10 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_include_affixes_if_value_not_empty() { - _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}{_Episode.Title_}"; + _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}{_Episode.Title_}{Quality.Title}"; Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be("South.Park.S15E06_City.Sushi_"); + .Should().Be("South.Park.S15E06_City.Sushi_HDTV-720p"); } [Test] @@ -691,6 +691,91 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("South Park - 15x06 - 15x07 - [100-101] - City Sushi - HDTV-720p"); } + [Test] + public void should_replace_quality_proper_with_v2_for_anime_v2() + { + _series.SeriesType = SeriesTypes.Anime; + _namingConfig.AnimeEpisodeFormat = "{Quality Proper}"; + + GivenProper(); + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("v2"); + } + + [Test] + public void should_not_include_quality_proper_when_release_is_not_a_proper() + { + _namingConfig.StandardEpisodeFormat = "{Quality Title} {Quality Proper}"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("HDTV-720p"); + } + + [Test] + public void should_wrap_proper_in_square_brackets() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Title}] {[Quality Proper]}"; + + GivenProper(); + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South Park - S15E06 [HDTV-720p] [Proper]"); + } + + [Test] + public void should_not_wrap_proper_in_square_brackets_when_not_a_proper() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Title}] {[Quality Proper]}"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South Park - S15E06 [HDTV-720p]"); + } + + [Test] + public void should_replace_quality_full_with_quality_title_only_when_not_a_proper() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Full}]"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South Park - S15E06 [HDTV-720p]"); + } + + [Test] + public void should_replace_quality_full_with_quality_title_and_proper_only_when_a_proper() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Full}]"; + + GivenProper(); + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South Park - S15E06 [HDTV-720p Proper]"); + } + + [TestCase(' ')] + [TestCase('-')] + [TestCase('.')] + [TestCase('_')] + public void should_trim_extra_separators_from_end_when_quality_proper_is_not_included(char separator) + { + _namingConfig.StandardEpisodeFormat = String.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}", separator); + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("HDTV-720p"); + } + + [TestCase(' ')] + [TestCase('-')] + [TestCase('.')] + [TestCase('_')] + public void should_trim_extra_separators_from_middle_when_quality_proper_is_not_included(char separator) + { + _namingConfig.StandardEpisodeFormat = String.Format("{{Quality{0}Title}}{0}{{Quality{0}Proper}}{0}{{Episode{0}Title}}", separator); + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be(String.Format("HDTV-720p{0}City{0}Sushi", separator)); + } + [Test] public void should_format_range_multi_episode_properly() { diff --git a/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs b/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs new file mode 100644 index 000000000..f20ba69e0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/069_quality_proper.cs @@ -0,0 +1,100 @@ +using System; +using System.Data; +using System.Linq; +using System.Text.RegularExpressions; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(69)] + public class quality_proper : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertQualityTitle); + } + + private static readonly Regex QualityTitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:quality)(?:(?[- ._]+)(?:title))?)(?[- ._)\]]*)\}", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private void ConvertQualityTitle(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand namingConfigCmd = conn.CreateCommand()) + { + namingConfigCmd.Transaction = tran; + namingConfigCmd.CommandText = @"SELECT StandardEpisodeFormat, DailyEpisodeFormat, AnimeEpisodeFormat FROM NamingConfig LIMIT 1"; + + using (IDataReader configReader = namingConfigCmd.ExecuteReader()) + { + while (configReader.Read()) + { + var currentStandard = configReader.GetString(0); + var currentDaily = configReader.GetString(1); + var currentAnime = configReader.GetString(1); + + var newStandard = GetNewFormat(currentStandard); + var newDaily = GetNewFormat(currentDaily); + var newAnime = GetNewFormat(currentAnime); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + + updateCmd.CommandText = "UPDATE NamingConfig SET StandardEpisodeFormat = ?, DailyEpisodeFormat = ?, AnimeEpisodeFormat = ?"; + updateCmd.AddParameter(newStandard); + updateCmd.AddParameter(newDaily); + updateCmd.AddParameter(newAnime); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + + private string GetNewFormat(string currentFormat) + { + var matches = QualityTitleRegex.Matches(currentFormat); + var result = currentFormat; + + foreach (Match match in matches) + { + var tokenMatch = GetTokenMatch(match); + var qualityFullToken = String.Format("Quality{0}Full", tokenMatch.Separator); ; + + if (tokenMatch.Token.All(t => !Char.IsLetter(t) || Char.IsLower(t))) + { + qualityFullToken = String.Format("quality{0}full", tokenMatch.Separator); + } + else if (tokenMatch.Token.All(t => !Char.IsLetter(t) || Char.IsUpper(t))) + { + qualityFullToken = String.Format("QUALITY{0}FULL", tokenMatch.Separator); + } + + result = result.Replace(match.Groups["token"].Value, qualityFullToken); + } + + return result; + } + + private TokenMatch69 GetTokenMatch(Match match) + { + return new TokenMatch69 + { + Prefix = match.Groups["prefix"].Value, + Token = match.Groups["token"].Value, + Separator = match.Groups["separator"].Value, + Suffix = match.Groups["suffix"].Value, + }; + } + + private class TokenMatch69 + { + public string Prefix { get; set; } + public string Token { get; set; } + public string Separator { get; set; } + public string Suffix { get; set; } + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b070d0b70..245957df6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -228,6 +228,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 87aeb532f..fa406c7ce 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached _absoluteEpisodeFormatCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._]*)\}", + private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", @@ -56,7 +56,8 @@ namespace NzbDrone.Core.Organizer private static readonly Regex OriginalTitleRegex = new Regex(@"(\^{original[- ._]title\}$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex FileNameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled); + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); + private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; @@ -120,10 +121,12 @@ namespace NzbDrone.Core.Organizer AddSeriesTokens(tokenHandlers, series); AddEpisodeTokens(tokenHandlers, episodes); AddEpisodeFileTokens(tokenHandlers, series, episodeFile); + AddQualityTokens(tokenHandlers, series, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile); var fileName = ReplaceTokens(pattern, tokenHandlers).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, String.Empty); return fileName; } @@ -417,7 +420,16 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? "DRONE"; - tokenHandlers["{Quality Title}"] = m => GetQualityTitle(series, episodeFile.Quality); + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) + { + var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(series, episodeFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1}", qualityTitle, qualityProper); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; } private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) @@ -651,24 +663,19 @@ namespace NzbDrone.Core.Organizer return String.Join(" + ", titles); } - private String GetQualityTitle(Series series, QualityModel quality) + private String GetQualityProper(Series series, QualityModel quality) { - var qualitySuffix = String.Empty; - if (quality.Revision.Version > 1) { if (series.SeriesType == SeriesTypes.Anime) { - qualitySuffix = " v" + quality.Revision.Version; + return "v" + quality.Revision.Version; } - else - { - qualitySuffix = " Proper"; - } + return "Proper"; } - return _qualityDefinitionService.Get(quality.Quality).Title + qualitySuffix; + return String.Empty; } private String GetOriginalTitle(EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 4177fde6a..974713c39 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -12,9 +12,9 @@ namespace NzbDrone.Core.Organizer { RenameEpisodes = false, MultiEpisodeStyle = 0, - StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}", - DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Title}", - AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}", + StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", + DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", + AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", SeriesFolderFormat = "{Series Title}", SeasonFolderFormat = "Season {season}" }; diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs index 9232dd5a3..4498c22ad 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs @@ -47,7 +47,7 @@ {{> SeasonNamingPartial}} {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} - {{> QualityTitleNamingPartial}} + {{> QualityNamingPartial}} {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} @@ -79,7 +79,7 @@ {{> SeasonNamingPartial}} {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} - {{> QualityTitleNamingPartial}} + {{> QualityNamingPartial}} {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} @@ -111,7 +111,7 @@ {{> SeasonNamingPartial}} {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} - {{> QualityTitleNamingPartial}} + {{> QualityNamingPartial}} {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} {{> OriginalTitleNamingPartial}} diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs similarity index 51% rename from src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.hbs rename to src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs index 4fe8dc65e..b3da5f0af 100644 --- a/src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.hbs +++ b/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs @@ -1,6 +1,9 @@