From 856a55a9c9628069c601854d64c71b62be25d233 Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 17 Oct 2022 20:35:02 -0500 Subject: [PATCH] New: Added advanced subtitle/audio language filter to {MediaInfo ..} Fixes #4710 Co-Authored-By: Taloth --- frontend/src/Helpers/Props/icons.js | 2 + .../MediaManagement/Naming/NamingModal.css | 17 ++ .../MediaManagement/Naming/NamingModal.js | 164 ++++++++++-------- .../MediaManagement/Naming/NamingOption.css | 6 + .../MediaManagement/Naming/NamingOption.js | 11 +- .../FileNameBuilderFixture.cs | 21 +++ .../Organizer/FileNameBuilder.cs | 71 +++++--- 7 files changed, 188 insertions(+), 104 deletions(-) diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 2e867329e..124549719 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -22,6 +22,7 @@ import { import { faArrowCircleLeft as fasArrowCircleLeft, faArrowCircleRight as fasArrowCircleRight, + faAsterisk as fasAsterisk, faBackward as fasBackward, faBan as fasBan, faBars as fasBars, @@ -154,6 +155,7 @@ export const FILE = farFile; export const FILM = fasFilm; export const FILTER = fasFilter; export const FLAG = fasFlag; +export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; export const GENRE = fasTheaterMasks; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css index c178d82cb..66e9bd73a 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css @@ -16,3 +16,20 @@ margin-left: 10px; width: 200px; } + +.footNote { + display: flex; + color: $helpTextColor; + + .icon { + margin-top: 3px; + margin-right: 5px; + padding: 2px; + } + + code { + padding: 0 1px; + border: 1px solid $borderColor; + background-color: #f7f7f7; + } +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 163cc33d1..9545ec62f 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -3,17 +3,93 @@ import React, { Component } from 'react'; import FieldSet from 'Components/FieldSet'; import SelectInput from 'Components/Form/SelectInput'; import TextInput from 'Components/Form/TextInput'; +import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; -import { sizes } from 'Helpers/Props'; +import { icons, sizes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import NamingOption from './NamingOption'; import styles from './NamingModal.css'; +const separatorOptions = [ + { key: ' ', value: 'Space ( )' }, + { key: '.', value: 'Period (.)' }, + { key: '_', value: 'Underscore (_)' }, + { key: '-', value: 'Dash (-)' } +]; + +const caseOptions = [ + { key: 'title', value: translate('DefaultCase') }, + { key: 'lower', value: translate('LowerCase') }, + { key: 'upper', value: translate('UpperCase') } +]; + +const fileNameTokens = [ + { + token: '{Movie Title} - {Quality Full}', + example: 'Movie Title (2010) - HDTV-720p Proper' + } +]; + +const movieTokens = [ + { token: '{Movie Title}', example: 'Movie\'s Title' }, + { token: '{Movie Title:DE}', example: 'Titel des Films' }, + { token: '{Movie CleanTitle}', example: 'Movies Title' }, + { token: '{Movie TitleThe}', example: 'Movie\'s Title, The' }, + { token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' }, + { token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' }, + { token: '{Movie TitleFirstCharacter}', example: 'M' }, + { token: '{Movie Collection}', example: 'The Movie Collection' }, + { token: '{Movie Certification}', example: 'R' }, + { token: '{Release Year}', example: '2009' } +]; + +const movieIdTokens = [ + { token: '{ImdbId}', example: 'tt12345' }, + { token: '{TmdbId}', example: '123456' } +]; + +const qualityTokens = [ + { token: '{Quality Full}', example: 'HDTV-720p Proper' }, + { token: '{Quality Title}', example: 'HDTV-720p' } +]; + +const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { 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]', footNote: 1 }, + { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 }, + + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo VideoBitDepth}', example: '10' }, + { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, + { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' } +]; + +const releaseGroupTokens = [ + { token: '{Release Group}', example: 'Rls Grp' } +]; + +const editionTokens = [ + { token: '{Edition Tags}', example: 'IMAX' } +]; + +const customFormatTokens = [ + { token: '{Custom Formats}', example: 'Surround Sound x264' } +]; + +const originalTokens = [ + { token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' }, + { token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' } +]; + class NamingModal extends Component { // @@ -94,81 +170,6 @@ class NamingModal extends Component { case: tokenCase } = this.state; - const separatorOptions = [ - { key: ' ', value: 'Space ( )' }, - { key: '.', value: 'Period (.)' }, - { key: '_', value: 'Underscore (_)' }, - { key: '-', value: 'Dash (-)' } - ]; - - const caseOptions = [ - { key: 'title', value: translate('DefaultCase') }, - { key: 'lower', value: translate('LowerCase') }, - { key: 'upper', value: translate('UpperCase') } - ]; - - const fileNameTokens = [ - { - token: '{Movie Title} - {Quality Full}', - example: 'Movie Title (2010) - HDTV-720p Proper' - } - ]; - - const movieTokens = [ - { token: '{Movie Title}', example: 'Movie\'s Title' }, - { token: '{Movie Title:DE}', example: 'Titel des Films' }, - { token: '{Movie CleanTitle}', example: 'Movies Title' }, - { token: '{Movie TitleThe}', example: 'Movie\'s Title, The' }, - { token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' }, - { token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' }, - { token: '{Movie TitleFirstCharacter}', example: 'M' }, - { token: '{Movie Collection}', example: 'The Movie Collection' }, - { token: '{Movie Certification}', example: 'R' }, - { token: '{Release Year}', example: '2009' } - ]; - - const movieIdTokens = [ - { token: '{ImdbId}', example: 'tt12345' }, - { token: '{TmdbId}', example: '123456' } - ]; - - const qualityTokens = [ - { token: '{Quality Full}', example: 'HDTV-720p Proper' }, - { token: '{Quality Title}', example: 'HDTV-720p' } - ]; - - const mediaInfoTokens = [ - { token: '{MediaInfo Simple}', example: 'x264 DTS' }, - { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' }, - - { token: '{MediaInfo AudioCodec}', example: 'DTS' }, - { token: '{MediaInfo AudioChannels}', example: '5.1' }, - { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]' }, - { token: '{MediaInfo SubtitleLanguages}', example: '[DE]' }, - - { token: '{MediaInfo VideoCodec}', example: 'x264' }, - { token: '{MediaInfo VideoBitDepth}', example: '10' }, - { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, - { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' } - ]; - - const releaseGroupTokens = [ - { token: '{Release Group}', example: 'Rls Grp' } - ]; - - const editionTokens = [ - { token: '{Edition Tags}', example: 'IMAX' } - ]; - - const customFormatTokens = [ - { token: '{Custom Formats}', example: 'Surround Sound x264' } - ]; - - const originalTokens = [ - { token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' }, - { token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' } - ]; - return (
{ - 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 c895cb6bc..645a0509a 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -35,9 +35,15 @@ display: flex; align-items: center; align-self: stretch; + justify-content: space-between; flex: 0 0 50%; padding: 6px 16px; background-color: #ddd; + + .footNote { + padding: 2px; + color: #aaa; + } } .lower { diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js index 29bf9d3bb..6373c11e3 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js @@ -1,8 +1,9 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; -import { sizes } from 'Helpers/Props'; +import { icons, sizes } from 'Helpers/Props'; 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 467a0337c..18ef0c2ff 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -669,6 +669,27 @@ public void should_format_audio_languages_all(string audioLanguages, string expe .Should().Be(expected); } + [TestCase("eng/deu", "", "[EN+DE]")] + [TestCase("eng/nld/deu", "", "[EN+NL+DE]")] + [TestCase("eng/deu", ":DE", "[DE]")] + [TestCase("eng/nld/deu", ":EN+NL", "[EN+NL]")] + [TestCase("eng/nld/deu", ":NL+EN", "[NL+EN]")] + [TestCase("eng/nld/deu", ":-NL", "[EN+DE]")] + [TestCase("eng/nld/deu", ":DE+", "[DE+-]")] + [TestCase("eng/nld/deu", ":DE+NO.", "[DE].")] + [TestCase("eng/nld/deu", ":-EN-", "[NL+DE]-")] + public void should_format_subtitle_languages_all(string subtitleLanguages, string format, string expected) + { + _movieFile.ReleaseGroup = null; + + GivenMediaInfoModel(subtitles: subtitleLanguages); + + _namingConfig.StandardMovieFormat = "{MediaInfo SubtitleLanguages" + format + "}End"; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(expected + "End"); + } + [TestCase(HdrFormat.None, "South.Park")] [TestCase(HdrFormat.Hlg10, "South.Park.HDR")] [TestCase(HdrFormat.Hdr10, "South.Park.HDR")] diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 8872ef20d..dc1341bdb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -38,7 +38,7 @@ public class FileNameBuilder : IBuildFileNames private readonly ICustomFormatService _formatService; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?\{(?:imdb-|edition-))?\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9|]+))?(?[-} ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?\{(?:imdb-|edition-))?\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9|+-]+(?[-} ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean|Original)?(Title|Filename)(The)?)(?::(?[a-z0-9|]+))?\})", @@ -359,24 +359,6 @@ private void AddMediaInfoTokens(Dictionary> tok var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? new List(); var subtitles = movieFile.MediaInfo.Subtitles ?? new List(); - 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 = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : 8.ToString(); var audioChannelsFormatted = audioChannels > 0 ? audioChannels.ToString("F1", CultureInfo.InvariantCulture) : @@ -391,16 +373,16 @@ private void AddMediaInfoTokens(Dictionary> tok 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 3D}"] = m => mediaInfo3D; 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(movieFile.MediaInfo); @@ -419,7 +401,7 @@ private void AddCustomFormats(Dictionary> token tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); } - private string GetLanguagesToken(List mediaInfoLanguages) + private string GetLanguagesToken(List mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { var tokens = new List(); foreach (var item in mediaInfoLanguages) @@ -448,7 +430,44 @@ private string GetLanguagesToken(List mediaInfoLanguages) } } - 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, MovieFile movieFile, Movie movie)