From dd553b94396cd26a5c93fe8c05c454976f04904e Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 22:13:55 +0100 Subject: [PATCH] WIP UI Update for adding lists. --- .../ClientSchema/SchemaBuilder.cs | 15 + src/NzbDrone.Api/NetImport/NetImportModule.cs | 22 +- .../Migration/119_create_netimport_table.cs | 2 +- .../Organizer/FileNameBuilder.cs | 2106 ++++++++--------- .../NetImport/Add/IndexerSchemaModal.js | 54 +- .../NetImport/IndexerCollectionView.js | 25 - .../IndexerCollectionViewTemplate.hbs | 16 - src/UI/Settings/NetImport/IndexerItemView.js | 24 - .../NetImport/IndexerItemViewTemplate.hbs | 27 - src/UI/Settings/NetImport/IndexerLayout.js | 30 - .../NetImport/IndexerLayoutTemplate.hbs | 5 - .../NetImport/NetImportCollectionView.hbs | 16 + .../NetImport/NetImportCollectionView.js | 25 + .../Settings/NetImport/NetImportItemView.js | 24 + .../NetImport/NetImportItemViewTemplate.hbs | 8 + src/UI/Settings/NetImport/NetImportLayout.js | 23 + .../NetImport/NetImportLayoutTemplate.hbs | 4 + src/UI/Settings/SettingsLayout.js | 5 +- src/UI/Settings/SettingsLayoutTemplate.hbs | 2 +- 19 files changed, 1221 insertions(+), 1212 deletions(-) delete mode 100644 src/UI/Settings/NetImport/IndexerCollectionView.js delete mode 100644 src/UI/Settings/NetImport/IndexerCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/NetImport/IndexerItemView.js delete mode 100644 src/UI/Settings/NetImport/IndexerItemViewTemplate.hbs delete mode 100644 src/UI/Settings/NetImport/IndexerLayout.js delete mode 100644 src/UI/Settings/NetImport/IndexerLayoutTemplate.hbs create mode 100644 src/UI/Settings/NetImport/NetImportCollectionView.hbs create mode 100644 src/UI/Settings/NetImport/NetImportCollectionView.js create mode 100644 src/UI/Settings/NetImport/NetImportItemView.js create mode 100644 src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/NetImportLayout.js create mode 100644 src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 0687a1413..45049818c 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Newtonsoft.Json.Linq; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Profiles; namespace NzbDrone.Api.ClientSchema { @@ -147,6 +150,18 @@ namespace NzbDrone.Api.ClientSchema private static List GetSelectOptions(Type selectOptions) { + if (selectOptions == typeof(Profile)) + { + return new List(); + } + + if (selectOptions == typeof(Quality)) + { + var qOptions = from Quality q in selectOptions.GetProperties(BindingFlags.Static | BindingFlags.Public) + select new SelectOption {Name = q.Name, Value = q.Id}; + return qOptions.OrderBy(o => o.Value).ToList(); + } + var options = from Enum e in Enum.GetValues(selectOptions) select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; diff --git a/src/NzbDrone.Api/NetImport/NetImportModule.cs b/src/NzbDrone.Api/NetImport/NetImportModule.cs index 6130191d7..81695d922 100644 --- a/src/NzbDrone.Api/NetImport/NetImportModule.cs +++ b/src/NzbDrone.Api/NetImport/NetImportModule.cs @@ -1,12 +1,16 @@ -using NzbDrone.Core.NetImport; +using NzbDrone.Api.ClientSchema; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.Profiles; namespace NzbDrone.Api.NetImport { public class NetImportModule : ProviderModuleBase { - public NetImportModule(NetImportFactory indexerFactory) + private readonly IProfileService _profileService; + public NetImportModule(NetImportFactory indexerFactory, IProfileService profileService) : base(indexerFactory, "netimport") { + _profileService = profileService; } protected override void MapToResource(NetImportResource resource, NetImportDefinition definition) @@ -14,6 +18,20 @@ namespace NzbDrone.Api.NetImport base.MapToResource(resource, definition); resource.Enabled = definition.Enabled; + Field theField = null; + int index = 0; + foreach (var field in resource.Fields) + { + if (field.Label == "Quality") + { + index = resource.Fields.FindIndex(f => f.Label == field.Label); + field.SelectOptions = + _profileService.All().ConvertAll(p => new SelectOption {Name = p.Name, Value = p.Id}); + + theField = field; + } + } + } protected override void MapToModel(NetImportDefinition definition, NetImportResource resource) diff --git a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs index f324ee925..0788c90d8 100644 --- a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration { - [Migration(119)] + [Migration(125)] public class create_netimport_table : NzbDroneMigrationBase { protected override void MainDbUpgrade() diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 535a89071..df327bee5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,321 +1,321 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Organizer -{ - public interface IBuildFileNames - { - string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); - string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); - string BuildFilePath(Movie movie, string fileName, string extension); - string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); - string BuildSeasonPath(Series series, int seasonNumber); - BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetSeriesFolder(Series series, NamingConfig namingConfig = null); - string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); - string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); - } - - public class FileNameBuilder : IBuildFileNames - { - private readonly INamingConfigService _namingConfigService; - private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _episodeFormatCache; - private readonly ICached _absoluteEpisodeFormatCache; - private readonly Logger _logger; - - 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+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); - private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); - - private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) - private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; - - public FileNameBuilder(INamingConfigService namingConfigService, - IQualityDefinitionService qualityDefinitionService, - ICacheManager cacheManager, - Logger logger) - { - _namingConfigService = namingConfigService; - _qualityDefinitionService = qualityDefinitionService; - //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); - _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); - _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); - _logger = logger; - } - - public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(episodeFile); - } - - if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) - { - throw new NamingFormatException("Standard episode format cannot be empty"); - } - - if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) - { - throw new NamingFormatException("Daily episode format cannot be empty"); - } - - if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) - { - throw new NamingFormatException("Anime episode format cannot be empty"); - } - - var pattern = namingConfig.StandardEpisodeFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); - - if (series.SeriesType == SeriesTypes.Daily) - { - pattern = namingConfig.DailyEpisodeFormat; - } - - if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) - { - pattern = namingConfig.AnimeEpisodeFormat; - } - - pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); - pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - - AddSeriesTokens(tokenHandlers, series); - AddEpisodeTokens(tokenHandlers, episodes); - AddEpisodeFileTokens(tokenHandlers, episodeFile); - AddQualityTokens(tokenHandlers, series, episodeFile); - AddMediaInfoTokens(tokenHandlers, episodeFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(movieFile); - } - - //TODO: Update namingConfig for Movies! - var pattern = namingConfig.StandardMovieFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - AddQualityTokens(tokenHandlers, movie, movieFile); - AddMediaInfoTokens(tokenHandlers, movieFile); - AddMovieFileTokens(tokenHandlers, movieFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = BuildSeasonPath(series, seasonNumber); - - return Path.Combine(path, fileName + extension); - } - - public string BuildFilePath(Movie movie, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = movie.Path; - - return Path.Combine(path, fileName + extension); - } - - public string BuildSeasonPath(Series series, int seasonNumber) - { - var path = series.Path; - - if (series.SeasonFolder) - { - if (seasonNumber == 0) - { - path = Path.Combine(path, "Specials"); - } - else - { - var seasonFolder = GetSeasonFolder(series, seasonNumber); - - seasonFolder = CleanFileName(seasonFolder); - - path = Path.Combine(path, seasonFolder); - } - } - - return path; - } - - public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) - { - return new BasicNamingConfig(); //For now let's be lazy - - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); - - if (episodeFormat == null) - { - return new BasicNamingConfig(); - } - - var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; - - var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); - - foreach (Match match in titleTokens) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - - if (!separator.Equals(" ")) - { - basicNamingConfig.ReplaceSpaces = true; - } - - if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeSeriesTitle = true; - } - - if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeEpisodeTitle = true; - } - - if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeQuality = true; - } - } - - return basicNamingConfig; - } - - public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - - return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - AddSeasonTokens(tokenHandlers, seasonNumber); - - return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) - { - if(namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - - return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); - } - - public static string CleanTitle(string title) - { - title = title.Replace("&", "and"); - title = ScenifyReplaceChars.Replace(title, " "); - title = ScenifyRemoveChars.Replace(title, string.Empty); - - return title; - } - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IBuildFileNames + { + string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); + string BuildFilePath(Movie movie, string fileName, string extension); + string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + string BuildSeasonPath(Series series, int seasonNumber); + BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); + string GetSeriesFolder(Series series, NamingConfig namingConfig = null); + string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); + } + + public class FileNameBuilder : IBuildFileNames + { + private readonly INamingConfigService _namingConfigService; + private readonly IQualityDefinitionService _qualityDefinitionService; + private readonly ICached _episodeFormatCache; + private readonly ICached _absoluteEpisodeFormatCache; + private readonly Logger _logger; + + 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+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); + private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); + + private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) + private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; + + public FileNameBuilder(INamingConfigService namingConfigService, + IQualityDefinitionService qualityDefinitionService, + ICacheManager cacheManager, + Logger logger) + { + _namingConfigService = namingConfigService; + _qualityDefinitionService = qualityDefinitionService; + //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); + _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); + _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); + _logger = logger; + } + + public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(episodeFile); + } + + if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) + { + throw new NamingFormatException("Standard episode format cannot be empty"); + } + + if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) + { + throw new NamingFormatException("Daily episode format cannot be empty"); + } + + if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) + { + throw new NamingFormatException("Anime episode format cannot be empty"); + } + + var pattern = namingConfig.StandardEpisodeFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); + + if (series.SeriesType == SeriesTypes.Daily) + { + pattern = namingConfig.DailyEpisodeFormat; + } + + if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) + { + pattern = namingConfig.AnimeEpisodeFormat; + } + + pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddSeriesTokens(tokenHandlers, series); + AddEpisodeTokens(tokenHandlers, episodes); + AddEpisodeFileTokens(tokenHandlers, episodeFile); + AddQualityTokens(tokenHandlers, series, episodeFile); + AddMediaInfoTokens(tokenHandlers, episodeFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(movieFile); + } + + //TODO: Update namingConfig for Movies! + var pattern = namingConfig.StandardMovieFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + AddQualityTokens(tokenHandlers, movie, movieFile); + AddMediaInfoTokens(tokenHandlers, movieFile); + AddMovieFileTokens(tokenHandlers, movieFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = BuildSeasonPath(series, seasonNumber); + + return Path.Combine(path, fileName + extension); + } + + public string BuildFilePath(Movie movie, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = movie.Path; + + return Path.Combine(path, fileName + extension); + } + + public string BuildSeasonPath(Series series, int seasonNumber) + { + var path = series.Path; + + if (series.SeasonFolder) + { + if (seasonNumber == 0) + { + path = Path.Combine(path, "Specials"); + } + else + { + var seasonFolder = GetSeasonFolder(series, seasonNumber); + + seasonFolder = CleanFileName(seasonFolder); + + path = Path.Combine(path, seasonFolder); + } + } + + return path; + } + + public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) + { + return new BasicNamingConfig(); //For now let's be lazy + + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); + + if (episodeFormat == null) + { + return new BasicNamingConfig(); + } + + var basicNamingConfig = new BasicNamingConfig + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; + + var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); + + foreach (Match match in titleTokens) + { + var separator = match.Groups["separator"].Value; + var token = match.Groups["token"].Value; + + if (!separator.Equals(" ")) + { + basicNamingConfig.ReplaceSpaces = true; + } + + if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeSeriesTitle = true; + } + + if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeEpisodeTitle = true; + } + + if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeQuality = true; + } + } + + return basicNamingConfig; + } + + public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + + return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + AddSeasonTokens(tokenHandlers, seasonNumber); + + return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) + { + if(namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + + return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); + } + + public static string CleanTitle(string title) + { + title = title.Replace("&", "and"); + title = ScenifyReplaceChars.Replace(title, " "); + title = ScenifyRemoveChars.Replace(title, string.Empty); + + return title; + } + public static string TitleThe(string title) { string[] prefixes = { "The ", "An ", "A " }; @@ -330,738 +330,738 @@ namespace NzbDrone.Core.Organizer } return title.Trim(); - } - - public static string CleanFileName(string name, bool replace = true) - { - string result = name; - string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; - - for (int i = 0; i < badCharacters.Length; i++) - { - result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); - } - - return result.Trim(); - } - - public static string CleanFolderName(string name) - { - name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); - name = name.Trim(' ', '.'); - - return CleanFileName(name); - } - - private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) - { - tokenHandlers["{Series Title}"] = m => series.Title; - tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); - } - - private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) - { - var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); - - int index = 1; - foreach (var episodeFormat in episodeFormats) - { - var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - string formatPattern; - - switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) - { - case MultiEpisodeStyle.Duplicate: - formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - } - - var token = string.Format("{{Season Episode{0}}}", index++); - pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token); - tokenHandlers[token] = m => seasonEpisodePattern; - } - - AddSeasonTokens(tokenHandlers, episodes.First().SeasonNumber); - - if (episodes.Count > 1) - { - tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat) + "-" + episodes.Last().EpisodeNumber.ToString(m.CustomFormat); - } - else - { - tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat); - } - - return pattern; - } - - private string AddAbsoluteNumberingTokens(string pattern, Dictionary> tokenHandlers, Series series, List episodes, NamingConfig namingConfig) - { - var absoluteEpisodeFormats = GetAbsoluteFormat(pattern).DistinctBy(v => v.AbsoluteEpisodePattern).ToList(); - - int index = 1; - foreach (var absoluteEpisodeFormat in absoluteEpisodeFormats) - { - if (series.SeriesType != SeriesTypes.Anime) - { - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); - continue; - } - - var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; - string formatPattern; - - switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) - { - - case MultiEpisodeStyle.Duplicate: - formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); - - formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - var eps = new List {episodes.First()}; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - } - - var token = string.Format("{{Absolute Pattern{0}}}", index++); - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); - tokenHandlers[token] = m => absoluteEpisodePattern; - } - - return pattern; - } - - private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) - { - tokenHandlers["{Movie Title}"] = m => movie.Title; - tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); - tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); - } - - private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) - { - tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? - } - - private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) - { - tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; - } - - private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) - { - tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); - } - - private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) - { - if (!episodes.First().AirDate.IsNullOrWhiteSpace()) - { - tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); - } - else - { - tokenHandlers["{Air Date}"] = m => "Unknown"; - } - - tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); - tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); - } - - private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - //tokenHandlers["{IMDb Id}"] = m => - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) - { - var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(series, episodeFile.Quality); - var qualityReal = GetQualityReal(series, episodeFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) - { - var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(movie, movieFile.Quality); - var qualityReal = GetQualityReal(movie, movieFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - if (episodeFile.MediaInfo == null) return; - - string videoCodec; - switch (episodeFile.MediaInfo.VideoCodec) - { - case "AVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = episodeFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (episodeFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (episodeFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = episodeFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? - episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) - { - if (movieFile.MediaInfo == null) return; - - string videoCodec; - switch (movieFile.MediaInfo.VideoCodec) - { - case "AVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = movieFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (movieFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (movieFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? - movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private string GetLanguagesToken(string mediaInfoLanguages) - { - List tokens = new List(); - foreach (var item in mediaInfoLanguages.Split('/')) - { - if (!string.IsNullOrWhiteSpace(item)) - tokens.Add(item.Trim()); - } - - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); - for (int i = 0; i < tokens.Count; i++) - { - try - { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); - - if (cultureInfo != null) - tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); - } - catch - { - } - } - - return string.Join("+", tokens.Distinct()); - } - - private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); - } - - private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - var tokenMatch = new TokenMatch - { - RegexMatch = match, - Prefix = match.Groups["prefix"].Value, - Separator = match.Groups["separator"].Value, - Suffix = match.Groups["suffix"].Value, - Token = match.Groups["token"].Value, - CustomFormat = match.Groups["customFormat"].Value - }; - - if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) - { - tokenMatch.CustomFormat = null; - } - - var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); - - var replacementText = tokenHandler(tokenMatch).Trim(); - - if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) - { - replacementText = replacementText.ToLower(); - } - else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) - { - replacementText = replacementText.ToUpper(); - } - - if (!tokenMatch.Separator.IsNullOrWhiteSpace()) - { - replacementText = replacementText.Replace(" ", tokenMatch.Separator); - } - - replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); - - if (!replacementText.IsNullOrWhiteSpace()) - { - replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; - } - - return replacementText; - } - - private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) - { - var eps = new List { episodes.First() }; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); - } - - private string ReplaceSeasonTokens(string pattern, int seasonNumber) - { - return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); - } - - private string ReplaceNumberToken(string token, int value) - { - var split = token.Trim('{', '}').Split(':'); - if (split.Length == 1) return value.ToString("0"); - - return value.ToString(split[1]); - } - - private EpisodeFormat[] GetEpisodeFormat(string pattern) - { - return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new EpisodeFormat - { - EpisodeSeparator = match.Groups["episodeSeparator"].Value, - Separator = match.Groups["separator"].Value, - EpisodePattern = match.Groups["episode"].Value, - SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, - }).ToArray()); - } - - private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) - { - return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new AbsoluteEpisodeFormat - { - Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", - AbsoluteEpisodePattern = match.Groups["absolute"].Value - }).ToArray()); - } - - private string GetEpisodeTitle(List episodes, string separator) - { - separator = string.Format(" {0} ", separator.Trim()); - - if (episodes.Count == 1) - { - return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); - } - - var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Select(CleanupEpisodeTitle) - .Distinct() - .ToList(); - - if (titles.All(t => t.IsNullOrWhiteSpace())) - { - titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Distinct() - .ToList(); - } - - return string.Join(separator, titles); - } - - private string CleanupEpisodeTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } - - private string GetQualityProper(Movie movie, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityProper(Series series, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - if (series.SeriesType == SeriesTypes.Anime) - { - return "v" + quality.Revision.Version; - } - - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityReal(Series series, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetQualityReal(Movie movie, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetOriginalTitle(EpisodeFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(EpisodeFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - - private string GetOriginalTitle(MovieFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(MovieFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - } - - internal sealed class TokenMatch - { - public Match RegexMatch { get; set; } - public string Prefix { get; set; } - public string Separator { get; set; } - public string Suffix { get; set; } - public string Token { get; set; } - public string CustomFormat { get; set; } - - public string DefaultValue(string defaultValue) - { - if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) - { - return defaultValue; - } - else - { - return string.Empty; - } - } - } - - public enum MultiEpisodeStyle - { - Extend = 0, - Duplicate = 1, - Repeat = 2, - Scene = 3, - Range = 4, - PrefixedRange = 5 - } -} + } + + public static string CleanFileName(string name, bool replace = true) + { + string result = name; + string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; + string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; + + for (int i = 0; i < badCharacters.Length; i++) + { + result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); + } + + return result.Trim(); + } + + public static string CleanFolderName(string name) + { + name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); + name = name.Trim(' ', '.'); + + return CleanFileName(name); + } + + private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) + { + tokenHandlers["{Series Title}"] = m => series.Title; + tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); + } + + private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) + { + var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); + + int index = 1; + foreach (var episodeFormat in episodeFormats) + { + var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; + string formatPattern; + + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + } + + var token = string.Format("{{Season Episode{0}}}", index++); + pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token); + tokenHandlers[token] = m => seasonEpisodePattern; + } + + AddSeasonTokens(tokenHandlers, episodes.First().SeasonNumber); + + if (episodes.Count > 1) + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat) + "-" + episodes.Last().EpisodeNumber.ToString(m.CustomFormat); + } + else + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat); + } + + return pattern; + } + + private string AddAbsoluteNumberingTokens(string pattern, Dictionary> tokenHandlers, Series series, List episodes, NamingConfig namingConfig) + { + var absoluteEpisodeFormats = GetAbsoluteFormat(pattern).DistinctBy(v => v.AbsoluteEpisodePattern).ToList(); + + int index = 1; + foreach (var absoluteEpisodeFormat in absoluteEpisodeFormats) + { + if (series.SeriesType != SeriesTypes.Anime) + { + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); + continue; + } + + var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; + string formatPattern; + + switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) + { + + case MultiEpisodeStyle.Duplicate: + formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); + + formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + var eps = new List {episodes.First()}; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + } + + var token = string.Format("{{Absolute Pattern{0}}}", index++); + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); + tokenHandlers[token] = m => absoluteEpisodePattern; + } + + return pattern; + } + + private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) + { + tokenHandlers["{Movie Title}"] = m => movie.Title; + tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); + tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); + } + + private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) + { + tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? + } + + private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) + { + tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; + } + + private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) + { + tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); + } + + private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) + { + if (!episodes.First().AirDate.IsNullOrWhiteSpace()) + { + tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); + } + else + { + tokenHandlers["{Air Date}"] = m => "Unknown"; + } + + tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); + tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); + } + + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + //tokenHandlers["{IMDb Id}"] = m => + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) + { + var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(series, episodeFile.Quality); + var qualityReal = GetQualityReal(series, episodeFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) + { + var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(movie, movieFile.Quality); + var qualityReal = GetQualityReal(movie, movieFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + if (episodeFile.MediaInfo == null) return; + + string videoCodec; + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = episodeFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = episodeFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? + episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) + { + if (movieFile.MediaInfo == null) return; + + string videoCodec; + switch (movieFile.MediaInfo.VideoCodec) + { + case "AVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = movieFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (movieFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (movieFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = movieFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? + movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private string GetLanguagesToken(string mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + + private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); + } + + private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + var tokenMatch = new TokenMatch + { + RegexMatch = match, + Prefix = match.Groups["prefix"].Value, + Separator = match.Groups["separator"].Value, + Suffix = match.Groups["suffix"].Value, + Token = match.Groups["token"].Value, + CustomFormat = match.Groups["customFormat"].Value + }; + + if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) + { + tokenMatch.CustomFormat = null; + } + + var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); + + var replacementText = tokenHandler(tokenMatch).Trim(); + + if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) + { + replacementText = replacementText.ToLower(); + } + else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) + { + replacementText = replacementText.ToUpper(); + } + + if (!tokenMatch.Separator.IsNullOrWhiteSpace()) + { + replacementText = replacementText.Replace(" ", tokenMatch.Separator); + } + + replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); + + if (!replacementText.IsNullOrWhiteSpace()) + { + replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; + } + + return replacementText; + } + + private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) + { + var eps = new List { episodes.First() }; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); + } + + private string ReplaceSeasonTokens(string pattern, int seasonNumber) + { + return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); + } + + private string ReplaceNumberToken(string token, int value) + { + var split = token.Trim('{', '}').Split(':'); + if (split.Length == 1) return value.ToString("0"); + + return value.ToString(split[1]); + } + + private EpisodeFormat[] GetEpisodeFormat(string pattern) + { + return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new EpisodeFormat + { + EpisodeSeparator = match.Groups["episodeSeparator"].Value, + Separator = match.Groups["separator"].Value, + EpisodePattern = match.Groups["episode"].Value, + SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, + }).ToArray()); + } + + private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) + { + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new AbsoluteEpisodeFormat + { + Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", + AbsoluteEpisodePattern = match.Groups["absolute"].Value + }).ToArray()); + } + + private string GetEpisodeTitle(List episodes, string separator) + { + separator = string.Format(" {0} ", separator.Trim()); + + if (episodes.Count == 1) + { + return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + } + + var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(CleanupEpisodeTitle) + .Distinct() + .ToList(); + + if (titles.All(t => t.IsNullOrWhiteSpace())) + { + titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Distinct() + .ToList(); + } + + return string.Join(separator, titles); + } + + private string CleanupEpisodeTitle(string title) + { + //this will remove (1),(2) from the end of multi part episodes. + return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); + } + + private string GetQualityProper(Movie movie, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityProper(Series series, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + if (series.SeriesType == SeriesTypes.Anime) + { + return "v" + quality.Revision.Version; + } + + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityReal(Series series, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetQualityReal(Movie movie, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetOriginalTitle(EpisodeFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(EpisodeFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + + private string GetOriginalTitle(MovieFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(MovieFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + } + + internal sealed class TokenMatch + { + public Match RegexMatch { get; set; } + public string Prefix { get; set; } + public string Separator { get; set; } + public string Suffix { get; set; } + public string Token { get; set; } + public string CustomFormat { get; set; } + + public string DefaultValue(string defaultValue) + { + if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) + { + return defaultValue; + } + else + { + return string.Empty; + } + } + } + + public enum MultiEpisodeStyle + { + Extend = 0, + Duplicate = 1, + Repeat = 2, + Scene = 3, + Range = 4, + PrefixedRange = 5 + } +} diff --git a/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js b/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js index 52b430e89..c80ef734d 100644 --- a/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js +++ b/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js @@ -1,39 +1,39 @@ var _ = require('underscore'); var AppLayout = require('../../../AppLayout'); var Backbone = require('backbone'); -var SchemaCollection = require('../IndexerCollection'); +var SchemaCollection = require('../NetImportCollection'); var AddCollectionView = require('./IndexerAddCollectionView'); module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; + open : function(collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; - var groupedSchemaCollection = new Backbone.Collection(); + var groupedSchemaCollection = new Backbone.Collection(); - schemaCollection.on('sync', function() { + schemaCollection.on('sync', function() { - var groups = schemaCollection.groupBy(function(model, iterator) { - return model.get('protocol'); - }); - var modelCollection = _.map(groups, function(values, key, list) { - return { - "header" : key, - collection : values - }; - }); + var groups = schemaCollection.groupBy(function(model, iterator) { + return model.get('protocol'); + }); + var modelCollection = _.map(groups, function(values, key, list) { + return { + "header" : key, + collection : values + }; + }); - groupedSchemaCollection.reset(modelCollection); - }); + groupedSchemaCollection.reset(modelCollection); + }); - var view = new AddCollectionView({ - collection : groupedSchemaCollection, - targetCollection : collection - }); + var view = new AddCollectionView({ + collection : groupedSchemaCollection, + targetCollection : collection + }); - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file + AppLayout.modalRegion.show(view); + } +}; diff --git a/src/UI/Settings/NetImport/IndexerCollectionView.js b/src/UI/Settings/NetImport/IndexerCollectionView.js deleted file mode 100644 index df6ae9596..000000000 --- a/src/UI/Settings/NetImport/IndexerCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./IndexerItemView'); -var SchemaModal = require('./Add/IndexerSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.indexer-list', - template : 'Settings/Indexers/IndexerCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/IndexerCollectionViewTemplate.hbs b/src/UI/Settings/NetImport/IndexerCollectionViewTemplate.hbs deleted file mode 100644 index 0f4ece3d8..000000000 --- a/src/UI/Settings/NetImport/IndexerCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -
- Indexers -
-
-
    -
  • -
    - - - -
    -
  • -
-
-
-
\ No newline at end of file diff --git a/src/UI/Settings/NetImport/IndexerItemView.js b/src/UI/Settings/NetImport/IndexerItemView.js deleted file mode 100644 index 29cf3d7c5..000000000 --- a/src/UI/Settings/NetImport/IndexerItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/IndexerEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/IndexerItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/IndexerItemViewTemplate.hbs b/src/UI/Settings/NetImport/IndexerItemViewTemplate.hbs deleted file mode 100644 index abef39886..000000000 --- a/src/UI/Settings/NetImport/IndexerItemViewTemplate.hbs +++ /dev/null @@ -1,27 +0,0 @@ -
-
-

{{name}}

-
- -
- {{#if supportsRss}} - {{#if enableRss}} - RSS - {{else}} - RSS - {{/if}} - {{else}} - RSS - {{/if}} - - {{#if supportsSearch}} - {{#if enableSearch}} - Search - {{else}} - Search - {{/if}} - {{else}} - Search - {{/if}} -
-
diff --git a/src/UI/Settings/NetImport/IndexerLayout.js b/src/UI/Settings/NetImport/IndexerLayout.js deleted file mode 100644 index f6cbd1ab6..000000000 --- a/src/UI/Settings/NetImport/IndexerLayout.js +++ /dev/null @@ -1,30 +0,0 @@ -var Marionette = require('marionette'); -var IndexerCollection = require('./IndexerCollection'); -var CollectionView = require('./IndexerCollectionView'); -var OptionsView = require('./Options/IndexerOptionsView'); -var RestrictionCollection = require('./Restriction/RestrictionCollection'); -var RestrictionCollectionView = require('./Restriction/RestrictionCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Indexers/IndexerLayoutTemplate', - - regions : { - indexers : '#x-indexers-region', - indexerOptions : '#x-indexer-options-region', - restriction : '#x-restriction-region' - }, - - initialize : function() { - this.indexersCollection = new IndexerCollection(); - this.indexersCollection.fetch(); - - this.restrictionCollection = new RestrictionCollection(); - this.restrictionCollection.fetch(); - }, - - onShow : function() { - this.indexers.show(new CollectionView({ collection : this.indexersCollection })); - this.indexerOptions.show(new OptionsView({ model : this.model })); - this.restriction.show(new RestrictionCollectionView({ collection : this.restrictionCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/IndexerLayoutTemplate.hbs b/src/UI/Settings/NetImport/IndexerLayoutTemplate.hbs deleted file mode 100644 index b82535642..000000000 --- a/src/UI/Settings/NetImport/IndexerLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
-
-
-
-
diff --git a/src/UI/Settings/NetImport/NetImportCollectionView.hbs b/src/UI/Settings/NetImport/NetImportCollectionView.hbs new file mode 100644 index 000000000..1381f4c70 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportCollectionView.hbs @@ -0,0 +1,16 @@ +
+ Lists +
+
+
    +
  • +
    + + + +
    +
  • +
+
+
+
diff --git a/src/UI/Settings/NetImport/NetImportCollectionView.js b/src/UI/Settings/NetImport/NetImportCollectionView.js new file mode 100644 index 000000000..f0c300a61 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportCollectionView.js @@ -0,0 +1,25 @@ +var Marionette = require('marionette'); +var ItemView = require('./NetImportItemView'); +var SchemaModal = require('./Add/IndexerSchemaModal'); + +module.exports = Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer : '.list-list', + template : 'Settings/NetImport/NetImportCollectionViewTemplate', + + ui : { + 'addCard' : '.x-add-card' + }, + + events : { + 'click .x-add-card' : '_openSchemaModal' + }, + + appendHtml : function(collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal : function() { + SchemaModal.open(this.collection); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportItemView.js b/src/UI/Settings/NetImport/NetImportItemView.js new file mode 100644 index 000000000..7efca78fe --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportItemView.js @@ -0,0 +1,24 @@ +var AppLayout = require('../../AppLayout'); +var Marionette = require('marionette'); +var EditView = require('./Edit/IndexerEditView'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/NetImport/NetImportItemViewTemplate', + tagName : 'li', + + events : { + 'click' : '_edit' + }, + + initialize : function() { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit : function() { + var view = new EditView({ + model : this.model, + targetCollection : this.model.collection + }); + AppLayout.modalRegion.show(view); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs b/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs new file mode 100644 index 000000000..071da700d --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportItemViewTemplate.hbs @@ -0,0 +1,8 @@ +
+
+

{{name}}

+
+ +
+
+
diff --git a/src/UI/Settings/NetImport/NetImportLayout.js b/src/UI/Settings/NetImport/NetImportLayout.js new file mode 100644 index 000000000..4470c6fe8 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportLayout.js @@ -0,0 +1,23 @@ +var Marionette = require('marionette'); +var NetImportCollection = require('./NetImportCollection'); +var CollectionView = require('./NetImportCollectionView'); +var OptionsView = require('./Options/IndexerOptionsView'); + +module.exports = Marionette.Layout.extend({ + template : 'Settings/NetImport/NetImportLayoutTemplate', + + regions : { + lists : '#x-lists-region', + listOption : '#x-list-options-region', + }, + + initialize : function() { + this.indexersCollection = new NetImportCollection(); + this.indexersCollection.fetch(); + }, + + onShow : function() { + this.lists.show(new CollectionView({ collection : this.indexersCollection })); + this.listOption.show(new OptionsView({ model : this.model })); + } +}); diff --git a/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs new file mode 100644 index 000000000..c97943aa1 --- /dev/null +++ b/src/UI/Settings/NetImport/NetImportLayoutTemplate.hbs @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index fab5fc000..8d852ea5f 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -13,6 +13,8 @@ var IndexerLayout = require('./Indexers/IndexerLayout'); var IndexerCollection = require('./Indexers/IndexerCollection'); var IndexerSettingsModel = require('./Indexers/IndexerSettingsModel'); var NetImportSettingsModel = require("./NetImport/NetImportSettingsModel"); +var NetImportCollection = require('./NetImport/NetImportCollection'); +var NetImportLayout = require('./NetImport/NetImportLayout'); var DownloadClientLayout = require('./DownloadClient/DownloadClientLayout'); var DownloadClientSettingsModel = require('./DownloadClient/DownloadClientSettingsModel'); var NotificationCollectionView = require('./Notifications/NotificationCollectionView'); @@ -102,6 +104,7 @@ module.exports = Marionette.Layout.extend({ self.quality.show(new QualityLayout()); self.indexers.show(new IndexerLayout({ model : self.indexerSettings })); self.downloadClient.show(new DownloadClientLayout({ model : self.downloadClientSettings })); + self.netImport.show(new NetImportLayout({model : self.netImportSettings})); self.notifications.show(new NotificationCollectionView({ collection : self.notificationCollection })); self.metadata.show(new MetadataLayout()); self.general.show(new GeneralView({ model : self.generalSettings })); @@ -190,7 +193,7 @@ module.exports = Marionette.Layout.extend({ e.preventDefault(); } - this.ui.downloadClientTab.tab('show'); + this.ui.netImportTab.tab('show'); this._navigate('settings/netimport'); }, diff --git a/src/UI/Settings/SettingsLayoutTemplate.hbs b/src/UI/Settings/SettingsLayoutTemplate.hbs index 38e5942b6..440f25895 100644 --- a/src/UI/Settings/SettingsLayoutTemplate.hbs +++ b/src/UI/Settings/SettingsLayoutTemplate.hbs @@ -41,7 +41,7 @@
-
+