New: Added advanced subtitle/audio language filter to {MediaInfo ..}
closes #3367
This commit is contained in:
parent
023c8260f2
commit
b601c8bcfe
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
<FieldSet legend="Media Info">
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
mediaInfoTokens.map(({ token, example }) => {
|
||||
mediaInfoTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
|
@ -452,6 +453,7 @@ class NamingModal extends Component {
|
|||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
|
@ -461,6 +463,14 @@ class NamingModal extends Component {
|
|||
)
|
||||
}
|
||||
</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 legend="Other">
|
||||
|
|
|
@ -37,6 +37,12 @@
|
|||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
background-color: #ddd;
|
||||
justify-content: space-between;
|
||||
|
||||
.footNote {
|
||||
color: #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.lower {
|
||||
|
|
|
@ -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 {
|
|||
|
||||
<div className={styles.example}>
|
||||
{example.replace(/ /g, tokenSeparator)}
|
||||
|
||||
{
|
||||
footNote !== 0 &&
|
||||
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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<Episode> { _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")]
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace NzbDrone.Core.Organizer
|
|||
private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache;
|
||||
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);
|
||||
|
||||
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 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<string> tokens = new List<string>();
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue