From b601c8bcfe3a8a10b2e3afd4aec5a62df5ff174b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 2 Jan 2020 21:57:40 +0100 Subject: [PATCH] New: Added advanced subtitle/audio language filter to {MediaInfo ..} closes #3367 --- frontend/src/Helpers/Props/icons.js | 2 + .../MediaManagement/Naming/NamingModal.css | 18 +++++ .../MediaManagement/Naming/NamingModal.js | 20 ++++-- .../MediaManagement/Naming/NamingOption.css | 6 ++ .../MediaManagement/Naming/NamingOption.js | 11 ++- .../FileNameBuilderFixture.cs | 23 ++++++ .../Organizer/FileNameBuilder.cs | 72 ++++++++++++------- 7 files changed, 120 insertions(+), 32 deletions(-) diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 49fbc457d..cad3ef748 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -24,6 +24,7 @@ import { import { faArrowCircleLeft as fasArrowCircleLeft, faArrowCircleRight as fasArrowCircleRight, + faAsterisk as fasAsterisk, faBackward as fasBackward, faBars as fasBars, faBolt as fasBolt, @@ -138,6 +139,7 @@ export const EXTERNAL_LINK = fasExternalLinkAlt; export const FATAL = fasTimesCircle; export const FILE = farFile; export const FILTER = fasFilter; +export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; export const GROUP = farObjectGroup; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css index c178d82cb..51b3396dc 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css @@ -16,3 +16,21 @@ margin-left: 10px; width: 200px; } + +.footNote { + + color: $helpTextColor; + display: flex; + + .icon { + padding: 2px; + margin-top: 3px; + margin-right: 5px; + } + + code { + background-color: #f7f7f7; + border: 1px solid $borderColor; + padding: 0px 1px; + } +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index c841581d2..3ef7dd636 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -1,8 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { sizes } from 'Helpers/Props'; +import { sizes, icons } from 'Helpers/Props'; import FieldSet from 'Components/FieldSet'; import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; import SelectInput from 'Components/Form/SelectInput'; import TextInput from 'Components/Form/TextInput'; import Modal from 'Components/Modal/Modal'; @@ -90,12 +91,12 @@ const qualityTokens = [ const mediaInfoTokens = [ { token: '{MediaInfo Simple}', example: 'x264 DTS' }, - { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: 1 }, { token: '{MediaInfo AudioCodec}', example: 'DTS' }, { token: '{MediaInfo AudioChannels}', example: '5.1' }, - { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]' }, - { token: '{MediaInfo SubtitleLanguages}', example: '[DE]' }, + { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 }, + { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 }, { token: '{MediaInfo VideoCodec}', example: 'x264' }, { token: '{MediaInfo VideoBitDepth}', example: '10' }, @@ -444,7 +445,7 @@ class NamingModal extends Component {
{ - mediaInfoTokens.map(({ token, example }) => { + mediaInfoTokens.map(({ token, example, footNote }) => { return ( + +
+ +
+ MediaInfo Full/AudioLanguages/SubtitleLanguages support a :EN+DE suffix allowing you to filter the languages included in the filename. Use -DE to exclude specific languages. + Appending + (eg :EN+) will output [EN]/[EN+--]/[--] depending on excluded languages. For example {'{'}MediaInfo Full:EN+DE{'}'}. +
+
diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 89149e213..e04111857 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -37,6 +37,12 @@ flex: 0 0 50%; padding: 6px 16px; background-color: #ddd; + justify-content: space-between; + + .footNote { + color: #aaa; + padding: 2px; + } } .lower { diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js index 269266a5f..15f0346ef 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js @@ -1,8 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; -import { sizes } from 'Helpers/Props'; +import { sizes, icons } from 'Helpers/Props'; import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; import styles from './NamingOption.css'; class NamingOption extends Component { @@ -39,6 +40,7 @@ class NamingOption extends Component { token, tokenSeparator, example, + footNote, tokenCase, isFullFilename, size @@ -60,6 +62,11 @@ class NamingOption extends Component {
{example.replace(/ /g, tokenSeparator)} + + { + footNote !== 0 && + + }
); @@ -69,6 +76,7 @@ class NamingOption extends Component { NamingOption.propTypes = { token: PropTypes.string.isRequired, example: PropTypes.string.isRequired, + footNote: PropTypes.number.isRequired, tokenSeparator: PropTypes.string.isRequired, tokenCase: PropTypes.string.isRequired, isFullFilename: PropTypes.bool.isRequired, @@ -77,6 +85,7 @@ NamingOption.propTypes = { }; NamingOption.defaultProps = { + footNote: 0, size: sizes.SMALL, isFullFilename: false }; diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index d4ebae3e1..2630ac9a3 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -796,6 +796,29 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(expected); } + [TestCase("English/German", "", "[EN+DE]")] + [TestCase("English/Dutch/German", "", "[EN+NL+DE]")] + [TestCase("English/German", ":DE", "[DE]")] + [TestCase("English/Dutch/German", ":EN+NL", "[EN+NL]")] + [TestCase("English/Dutch/German", ":NL+EN", "[NL+EN]")] + [TestCase("English/Dutch/German", ":-NL", "[EN+DE]")] + [TestCase("English/Dutch/German", ":DE+", "[DE+-]")] + [TestCase("English/Dutch/German", ":DE+NO.", "[DE].")] + [TestCase("English/Dutch/German", ":-EN-", "[NL+DE]-")] + public void should_format_subtitle_languages_all(string subtitleLanguages, string format, string expected) + { + _episodeFile.ReleaseGroup = null; + + GivenMediaInfoModel(subtitles: subtitleLanguages); + + + _namingConfig.StandardEpisodeFormat = "{MediaInfo SubtitleLanguages" + format +"}End"; + + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be(expected + "End"); + } + [TestCase(8, "BT.601 NTSC", "BT.709", "South.Park.S15E06.City.Sushi")] [TestCase(10, "BT.2020", "PQ", "South.Park.S15E06.City.Sushi.HDR")] [TestCase(10, "BT.2020", "HLG", "South.Park.S15E06.City.Sushi.HDR")] diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index d7f9c3c2d..5d77ba747 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached _requiresAbsoluteEpisodeNumberCache; 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+)?})", @@ -601,24 +601,6 @@ namespace NzbDrone.Core.Organizer var audioLanguages = episodeFile.MediaInfo.AudioLanguages ?? string.Empty; var subtitles = episodeFile.MediaInfo.Subtitles ?? string.Empty; - var mediaInfoAudioLanguages = GetLanguagesToken(audioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; - } - - var mediaInfoAudioLanguagesAll = mediaInfoAudioLanguages; - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; - } - var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; var audioChannelsFormatted = audioChannels > 0 ? audioChannels.ToString("F1", CultureInfo.InvariantCulture) : @@ -631,15 +613,15 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted; - tokenHandlers["{MediaInfo AudioLanguages}"] = m => mediaInfoAudioLanguages; - tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => mediaInfoAudioLanguagesAll; + tokenHandlers["{MediaInfo AudioLanguages}"] = m => GetLanguagesToken(audioLanguages, m.CustomFormat, true, true); + tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => GetLanguagesToken(audioLanguages, m.CustomFormat, false, true); - tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => mediaInfoSubtitleLanguages; - tokenHandlers["{MediaInfo SubtitleLanguagesAll}"] = m => mediaInfoSubtitleLanguages; + tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => GetLanguagesToken(subtitles, m.CustomFormat, false, true); + tokenHandlers["{MediaInfo SubtitleLanguagesAll}"] = m => GetLanguagesToken(subtitles, m.CustomFormat, false, true); tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; - tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{mediaInfoAudioLanguages} {mediaInfoSubtitleLanguages}"; + tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{GetLanguagesToken(audioLanguages, m.CustomFormat, true, true)} {GetLanguagesToken(subtitles, m.CustomFormat, false, true)}"; tokenHandlers[MediaInfoVideoDynamicRangeToken] = m => MediaInfoFormatter.FormatVideoDynamicRange(episodeFile.MediaInfo); @@ -662,7 +644,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Preferred Words}"] = m => string.Join(" ", preferredWords); } - private string GetLanguagesToken(string mediaInfoLanguages) + private string GetLanguagesToken(string mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { List tokens = new List(); foreach (var item in mediaInfoLanguages.Split('/')) @@ -686,7 +668,45 @@ namespace NzbDrone.Core.Organizer } } - return string.Join("+", tokens.Distinct()); + tokens = tokens.Distinct().ToList(); + + var filteredTokens = tokens; + + // Exclude or filter + if (filter.IsNotNullOrWhiteSpace()) + { + if (filter.StartsWith("-")) + { + filteredTokens = tokens.Except(filter.Split('-')).ToList(); + } + else + { + filteredTokens = filter.Split('+').Intersect(tokens).ToList(); + } + } + + // Replace with wildcard (maybe too limited) + if (filter.IsNotNullOrWhiteSpace() && filter.EndsWith("+") && filteredTokens.Count != tokens.Count) + { + filteredTokens.Add("--"); + } + + + if (skipEnglishOnly && filteredTokens.Count == 1 && filteredTokens.First() == "EN") + { + return string.Empty; + } + + var response = string.Join("+", filteredTokens); + + if (quoted && response.IsNotNullOrWhiteSpace()) + { + return $"[{response}]"; + } + else + { + return response; + } } private void UpdateMediaInfoIfNeeded(string pattern, EpisodeFile episodeFile, Series series)