New: Added advanced subtitle/audio language filter to {MediaInfo ..}

closes #3367
This commit is contained in:
Taloth Saldono 2020-01-02 21:57:40 +01:00
parent 023c8260f2
commit b601c8bcfe
7 changed files with 120 additions and 32 deletions

View File

@ -24,6 +24,7 @@ import {
import { import {
faArrowCircleLeft as fasArrowCircleLeft, faArrowCircleLeft as fasArrowCircleLeft,
faArrowCircleRight as fasArrowCircleRight, faArrowCircleRight as fasArrowCircleRight,
faAsterisk as fasAsterisk,
faBackward as fasBackward, faBackward as fasBackward,
faBars as fasBars, faBars as fasBars,
faBolt as fasBolt, faBolt as fasBolt,
@ -138,6 +139,7 @@ export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle; export const FATAL = fasTimesCircle;
export const FILE = farFile; export const FILE = farFile;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FOOTNOTE = fasAsterisk;
export const FOLDER = farFolder; export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen; export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;

View File

@ -16,3 +16,21 @@
margin-left: 10px; margin-left: 10px;
width: 200px; 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;
}
}

View File

@ -1,8 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { sizes } from 'Helpers/Props'; import { sizes, icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
@ -90,12 +91,12 @@ const qualityTokens = [
const mediaInfoTokens = [ const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' }, { 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 AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' }, { token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]' }, { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 },
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]' }, { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 },
{ token: '{MediaInfo VideoCodec}', example: 'x264' }, { token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' }, { token: '{MediaInfo VideoBitDepth}', example: '10' },
@ -444,7 +445,7 @@ class NamingModal extends Component {
<FieldSet legend="Media Info"> <FieldSet legend="Media Info">
<div className={styles.groups}> <div className={styles.groups}>
{ {
mediaInfoTokens.map(({ token, example }) => { mediaInfoTokens.map(({ token, example, footNote }) => {
return ( return (
<NamingOption <NamingOption
key={token} key={token}
@ -452,6 +453,7 @@ class NamingModal extends Component {
value={value} value={value}
token={token} token={token}
example={example} example={example}
footNote={footNote}
tokenSeparator={tokenSeparator} tokenSeparator={tokenSeparator}
tokenCase={tokenCase} tokenCase={tokenCase}
onPress={this.onOptionPress} onPress={this.onOptionPress}
@ -461,6 +463,14 @@ class NamingModal extends Component {
) )
} }
</div> </div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<div>
MediaInfo Full/AudioLanguages/SubtitleLanguages support a <code>:EN+DE</code> suffix allowing you to filter the languages included in the filename. Use <code>-DE</code> to exclude specific languages.
Appending <code>+</code> (eg <code>:EN+</code>) will output <code>[EN]</code>/<code>[EN+--]</code>/<code>[--]</code> depending on excluded languages. For example <code>{'{'}MediaInfo Full:EN+DE{'}'}</code>.
</div>
</div>
</FieldSet> </FieldSet>
<FieldSet legend="Other"> <FieldSet legend="Other">

View File

@ -37,6 +37,12 @@
flex: 0 0 50%; flex: 0 0 50%;
padding: 6px 16px; padding: 6px 16px;
background-color: #ddd; background-color: #ddd;
justify-content: space-between;
.footNote {
color: #aaa;
padding: 2px;
}
} }
.lower { .lower {

View File

@ -1,8 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { sizes } from 'Helpers/Props'; import { sizes, icons } from 'Helpers/Props';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
import styles from './NamingOption.css'; import styles from './NamingOption.css';
class NamingOption extends Component { class NamingOption extends Component {
@ -39,6 +40,7 @@ class NamingOption extends Component {
token, token,
tokenSeparator, tokenSeparator,
example, example,
footNote,
tokenCase, tokenCase,
isFullFilename, isFullFilename,
size size
@ -60,6 +62,11 @@ class NamingOption extends Component {
<div className={styles.example}> <div className={styles.example}>
{example.replace(/ /g, tokenSeparator)} {example.replace(/ /g, tokenSeparator)}
{
footNote !== 0 &&
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
}
</div> </div>
</Link> </Link>
); );
@ -69,6 +76,7 @@ class NamingOption extends Component {
NamingOption.propTypes = { NamingOption.propTypes = {
token: PropTypes.string.isRequired, token: PropTypes.string.isRequired,
example: PropTypes.string.isRequired, example: PropTypes.string.isRequired,
footNote: PropTypes.number.isRequired,
tokenSeparator: PropTypes.string.isRequired, tokenSeparator: PropTypes.string.isRequired,
tokenCase: PropTypes.string.isRequired, tokenCase: PropTypes.string.isRequired,
isFullFilename: PropTypes.bool.isRequired, isFullFilename: PropTypes.bool.isRequired,
@ -77,6 +85,7 @@ NamingOption.propTypes = {
}; };
NamingOption.defaultProps = { NamingOption.defaultProps = {
footNote: 0,
size: sizes.SMALL, size: sizes.SMALL,
isFullFilename: false isFullFilename: false
}; };

View File

@ -796,6 +796,29 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be(expected); .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<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected + "End");
}
[TestCase(8, "BT.601 NTSC", "BT.709", "South.Park.S15E06.City.Sushi")] [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", "PQ", "South.Park.S15E06.City.Sushi.HDR")]
[TestCase(10, "BT.2020", "HLG", "South.Park.S15E06.City.Sushi.HDR")] [TestCase(10, "BT.2020", "HLG", "South.Park.S15E06.City.Sushi.HDR")]

View File

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Organizer
private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache; private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._)\]]*)\}", private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9+-]+(?<!-)))?(?<suffix>[- ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
@ -601,24 +601,6 @@ namespace NzbDrone.Core.Organizer
var audioLanguages = episodeFile.MediaInfo.AudioLanguages ?? string.Empty; var audioLanguages = episodeFile.MediaInfo.AudioLanguages ?? string.Empty;
var subtitles = episodeFile.MediaInfo.Subtitles ?? 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 videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty;
var audioChannelsFormatted = audioChannels > 0 ? var audioChannelsFormatted = audioChannels > 0 ?
audioChannels.ToString("F1", CultureInfo.InvariantCulture) : audioChannels.ToString("F1", CultureInfo.InvariantCulture) :
@ -631,15 +613,15 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; tokenHandlers["{MediaInfo Audio}"] = m => audioCodec;
tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec;
tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted; tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted;
tokenHandlers["{MediaInfo AudioLanguages}"] = m => mediaInfoAudioLanguages; tokenHandlers["{MediaInfo AudioLanguages}"] = m => GetLanguagesToken(audioLanguages, m.CustomFormat, true, true);
tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => mediaInfoAudioLanguagesAll; tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => GetLanguagesToken(audioLanguages, m.CustomFormat, false, true);
tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => mediaInfoSubtitleLanguages; tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => GetLanguagesToken(subtitles, m.CustomFormat, false, true);
tokenHandlers["{MediaInfo SubtitleLanguagesAll}"] = m => mediaInfoSubtitleLanguages; tokenHandlers["{MediaInfo SubtitleLanguagesAll}"] = m => GetLanguagesToken(subtitles, m.CustomFormat, false, true);
tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; 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] = tokenHandlers[MediaInfoVideoDynamicRangeToken] =
m => MediaInfoFormatter.FormatVideoDynamicRange(episodeFile.MediaInfo); m => MediaInfoFormatter.FormatVideoDynamicRange(episodeFile.MediaInfo);
@ -662,7 +644,7 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Preferred Words}"] = m => string.Join(" ", preferredWords); tokenHandlers["{Preferred Words}"] = m => string.Join(" ", preferredWords);
} }
private string GetLanguagesToken(string mediaInfoLanguages) private string GetLanguagesToken(string mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted)
{ {
List<string> tokens = new List<string>(); List<string> tokens = new List<string>();
foreach (var item in mediaInfoLanguages.Split('/')) 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) private void UpdateMediaInfoIfNeeded(string pattern, EpisodeFile episodeFile, Series series)