New: Setting to allow for grabbing season packs even if some episodes already meet cutoff

Closes #6378
This commit is contained in:
sparky3387 2025-09-28 08:40:07 +10:00 committed by GitHub
parent 1610e54650
commit cf6b21aef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 436 additions and 62 deletions

View file

@ -0,0 +1,19 @@
import React from 'react';
import NumberInput, { NumberInputChanged } from './NumberInput';
export interface FloatInputProps {
name: string;
value?: number | null;
min?: number;
max?: number;
step?: number;
placeholder?: string;
className?: string;
onChange: (change: NumberInputChanged) => void;
}
function FloatInput(props: FloatInputProps) {
return <NumberInput {...props} isFloat={true} />;
}
export default FloatInput;

View file

@ -7,6 +7,7 @@ import translate from 'Utilities/String/translate';
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
import CheckInput, { CheckInputProps } from './CheckInput';
import FloatInput, { FloatInputProps } from './FloatInput';
import { FormInputButtonProps } from './FormInputButton';
import FormInputHelpText from './FormInputHelpText';
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
@ -65,7 +66,7 @@ const componentMap: Record<InputType, ElementType> = {
downloadClientSelect: DownloadClientSelectInput,
dynamicSelect: ProviderDataSelectInput,
file: TextInput,
float: NumberInput,
float: FloatInput,
indexerFlagsSelect: IndexerFlagsSelectInput,
indexerSelect: IndexerSelectInput,
keyValueList: KeyValueListInput,
@ -110,7 +111,7 @@ type PickProps<V, C extends InputType> = C extends 'text'
: C extends 'file'
? TextInputProps
: C extends 'float'
? TextInputProps
? FloatInputProps
: C extends 'indexerFlagsSelect'
? IndexerFlagsSelectInputProps
: C extends 'indexerSelect'

View file

@ -24,13 +24,17 @@ function parseValue(
return newValue;
}
export interface NumberInputChanged extends InputChanged<number | null> {
isFloat?: boolean;
}
export interface NumberInputProps
extends Omit<TextInputProps, 'value' | 'onChange'> {
value?: number | null;
min?: number;
max?: number;
isFloat?: boolean;
onChange: (input: InputChanged<number | null>) => void;
onChange: (change: NumberInputChanged) => void;
}
function NumberInput({
@ -50,11 +54,14 @@ function NumberInput({
const handleChange = useCallback(
({ name, value: newValue }: InputChanged<string>) => {
setValue(newValue);
const parsedValue = parseValue(newValue, isFloat, min, max);
setValue(parsedValue == null ? '' : parsedValue.toString());
onChange({
name,
value: parseValue(newValue, isFloat, min, max),
value: parsedValue,
isFloat,
});
},
[isFloat, min, max, onChange, setValue]
@ -75,6 +82,7 @@ function NumberInput({
onChange({
name,
value: parsedValue,
isFloat,
});
isFocused.current = false;

View file

@ -116,6 +116,27 @@ const fileDateOptions: EnhancedSelectInputValue<string>[] = [
},
];
const seasonPackUpgradeOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'all',
get value() {
return translate('All');
},
},
{
key: 'threshold',
get value() {
return translate('Threshold');
},
},
{
key: 'any',
get value() {
return translate('Any');
},
},
];
function MediaManagement() {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
@ -379,6 +400,82 @@ function MediaManagement() {
{...settings.userRejectedExtensions}
/>
</FormGroup>
{showAdvancedSettings && (
<>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>
{translate('SeasonPackUpgradeAllowLabel')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="seasonPackUpgrade"
helpText={translate('SeasonPackUpgradeAllowHelpText')}
helpTextWarning={
settings.seasonPackUpgrade.value === 'any'
? translate('SeasonPackUpgradeAllowAnyWarning')
: undefined
}
values={seasonPackUpgradeOptions}
onChange={handleInputChange}
{...settings.seasonPackUpgrade}
/>
</FormGroup>
{settings.seasonPackUpgrade.value === 'threshold' && (
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>
{translate('SeasonPackUpgradeThresholdLabel')}
</FormLabel>
<FormInputGroup
type={inputTypes.FLOAT}
name="seasonPackUpgradeThreshold"
unit="%"
step={0.01}
min={0}
max={100}
helpTexts={[
translate('SeasonPackUpgradeThresholdHelpText'),
translate(
'SeasonPackUpgradeThresholdHelpTextExample',
{
numberEpisodes: 2,
totalEpisodes: 8,
count: Math.ceil((100 * 2) / 8),
}
),
translate(
'SeasonPackUpgradeThresholdHelpTextExample',
{
numberEpisodes: 3,
totalEpisodes: 12,
count: Math.ceil((100 * 3) / 12),
}
),
translate(
'SeasonPackUpgradeThresholdHelpTextExample',
{
numberEpisodes: 6,
totalEpisodes: 24,
count: Math.ceil((100 * 6) / 24),
}
),
]}
onChange={handleInputChange}
{...settings.seasonPackUpgradeThreshold}
/>
</FormGroup>
)}
</>
)}
</FieldSet>
) : null}

View file

@ -5,7 +5,7 @@ import updateSectionState from 'Utilities/State/updateSectionState';
function createSetSettingValueReducer(section) {
return (state, { payload }) => {
if (section === payload.section) {
const { name, value } = payload;
const { name, value, isFloat } = payload;
const newState = getSectionState(state, section);
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
@ -15,7 +15,12 @@ function createSetSettingValueReducer(section) {
let parsedValue = null;
if (_.isNumber(currentValue) && value != null) {
parsedValue = parseInt(value);
// Use isFloat property to determine parsing method
if (isFloat) {
parsedValue = parseFloat(value);
} else {
parsedValue = parseInt(value);
}
} else {
parsedValue = value;
}

View file

@ -20,4 +20,6 @@ export default interface MediaManagement {
extraFileExtensions: string;
userRejectedExtensions: string;
enableMediaInfo: boolean;
seasonPackUpgrade: string;
seasonPackUpgradeThreshold: number;
}

View file

@ -85,6 +85,10 @@ public void config_properties_should_write_and_read_using_same_key()
{
value = DateTime.Now.Millisecond;
}
else if (propertyInfo.PropertyType == typeof(double))
{
value = (double)DateTime.Now.Millisecond;
}
else if (propertyInfo.PropertyType == typeof(bool))
{
value = true;

View file

@ -7,6 +7,7 @@
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
@ -437,5 +438,102 @@ public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_f
Subject.IsSatisfiedBy(_parseResultSingle, new()).Accepted.Should().BeFalse();
}
[Test]
public void should_reject_season_pack_when_mode_is_all_and_not_all_are_upgradable()
{
GivenProfile(new QualityProfile
{
Cutoff = Quality.Bluray1080p.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
UpgradeAllowed = true
});
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.SeasonPackUpgrade)
.Returns(SeasonPackUpgradeType.All);
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
_parseResultMulti.Episodes = new List<Episode>
{
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 2 }
};
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
result.Accepted.Should().BeFalse();
}
[Test]
public void should_reject_for_season_pack_not_meeting_threshold()
{
GivenProfile(new QualityProfile
{
Cutoff = Quality.Bluray1080p.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
UpgradeAllowed = true
});
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.SeasonPackUpgrade)
.Returns(SeasonPackUpgradeType.Threshold);
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.SeasonPackUpgradeThreshold)
.Returns(90);
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
_parseResultMulti.Episodes = new List<Episode>
{
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 2 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 3 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 4 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 5 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 6 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 7 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 8 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 9 },
new Episode { EpisodeFile = null, EpisodeFileId = 0 }
};
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
result.Accepted.Should().BeFalse();
result.Reason.Should().Be(DownloadRejectionReason.DiskNotUpgrade);
}
[Test]
public void should_accept_season_pack_when_mode_is_any_and_at_least_one_upgradable()
{
GivenProfile(new QualityProfile
{
Cutoff = Quality.Bluray1080p.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
UpgradeAllowed = true
});
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.SeasonPackUpgrade)
.Returns(SeasonPackUpgradeType.Any);
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
_parseResultMulti.Episodes = new List<Episode>
{
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 2 }
};
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
result.Accepted.Should().BeTrue();
}
}
}

View file

@ -263,6 +263,18 @@ public string UserRejectedExtensions
set { SetValue("UserRejectedExtensions", value); }
}
public SeasonPackUpgradeType SeasonPackUpgrade
{
get { return GetValueEnum("SeasonPackUpgrade", SeasonPackUpgradeType.All); }
set { SetValue("SeasonPackUpgrade", value); }
}
public double SeasonPackUpgradeThreshold
{
get { return GetValueDouble("SeasonPackUpgradeThreshold", 100.0); }
set { SetValue("SeasonPackUpgradeThreshold", value); }
}
public bool SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }
@ -417,6 +429,11 @@ private int GetValueInt(string key, int defaultValue = 0)
return Convert.ToInt32(GetValue(key, defaultValue));
}
private double GetValueDouble(string key, double defaultValue = 0)
{
return Convert.ToDouble(GetValue(key, defaultValue), CultureInfo.InvariantCulture);
}
private T GetValueEnum<T>(string key, T defaultValue)
{
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
@ -454,6 +471,11 @@ private void SetValue(string key, int value)
SetValue(key, value.ToString());
}
private void SetValue(string key, double value)
{
SetValue(key, value.ToString(CultureInfo.InvariantCulture));
}
private void SetValue(string key, Enum value)
{
SetValue(key, value.ToString().ToLower());

View file

@ -43,6 +43,10 @@ public interface IConfigService
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
string UserRejectedExtensions { get; set; }
// Season Pack Upgrade (Media Management)
SeasonPackUpgradeType SeasonPackUpgrade { get; set; }
double SeasonPackUpgradeThreshold { get; set; }
// Permissions (Media Management)
bool SetPermissionsLinux { get; set; }
string ChmodFolder { get; set; }

View file

@ -74,5 +74,6 @@ public enum DownloadRejectionReason
DiskCustomFormatCutoffMet,
DiskCustomFormatScore,
DiskCustomFormatScoreIncrement,
DiskUpgradesNotAllowed
DiskUpgradesNotAllowed,
DiskNotUpgrade
}

View file

@ -1,6 +1,8 @@
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
@ -8,13 +10,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public class UpgradeDiskSpecification : IDownloadDecisionEngineSpecification
{
private readonly UpgradableSpecification _upgradableSpecification;
private readonly IConfigService _configService;
private readonly ICustomFormatCalculationService _formatService;
private readonly Logger _logger;
public UpgradeDiskSpecification(UpgradableSpecification upgradableSpecification,
IConfigService configService,
ICustomFormatCalculationService formatService,
Logger logger)
{
_configService = configService;
_upgradableSpecification = upgradableSpecification;
_formatService = formatService;
_logger = logger;
@ -27,66 +32,155 @@ public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, Release
{
var qualityProfile = subject.Series.QualityProfile.Value;
if (subject.ParsedEpisodeInfo.FullSeason)
{
var totalEpisodesInPack = subject.Episodes.Count;
if (totalEpisodesInPack == 0)
{
// Should not happen, but good to guard against it.
return DownloadSpecDecision.Accept();
}
// Count missing episodes as upgradable
var missingEpisodesCount = subject.Episodes.Count(c => c.EpisodeFileId == 0);
var upgradedCount = missingEpisodesCount;
_logger.Debug("{0} episodes are missing from disk and are considered upgradable.", upgradedCount);
// Filter for episodes that already exist on disk to check for quality upgrades
var existingEpisodeFiles = subject.Episodes.Where(c => c.EpisodeFileId != 0)
.Select(c => c.EpisodeFile.Value)
.ToList();
// If all episodes in the pack are missing, accept it immediately.
if (!existingEpisodeFiles.Any())
{
_logger.Debug("All episodes in season pack are missing, accepting.");
return DownloadSpecDecision.Accept();
}
// Check if any of the existing files can also be upgraded
foreach (var file in existingEpisodeFiles)
{
_logger.Debug("Comparing file quality with report. Existing file is {0}.", file.Quality);
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
file.Quality,
_formatService.ParseCustomFormat(file),
subject.ParsedEpisodeInfo.Quality))
{
_logger.Debug("Cutoff already met for existing file, not an upgrade.");
continue;
}
var customFormats = _formatService.ParseCustomFormat(file);
var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile,
file.Quality,
customFormats,
subject.ParsedEpisodeInfo.Quality,
subject.CustomFormats);
if (upgradeableRejectReason == UpgradeableRejectReason.None)
{
_logger.Debug("Existing episode is upgradable.");
upgradedCount++;
}
}
var seasonPackUpgrade = _configService.SeasonPackUpgrade;
var seasonPackUpgradeThreshold = _configService.SeasonPackUpgradeThreshold;
_logger.Debug("Total upgradable episodes: {0} out of {1}. Season import setting: {2}, Threshold: {3}%", upgradedCount, totalEpisodesInPack, seasonPackUpgrade, seasonPackUpgradeThreshold);
var upgradablePercentage = (double)upgradedCount / totalEpisodesInPack * 100;
if (seasonPackUpgrade == SeasonPackUpgradeType.Any)
{
if (upgradedCount > 0)
{
return DownloadSpecDecision.Accept();
}
}
else
{
var threshold = seasonPackUpgrade == SeasonPackUpgradeType.All
? 100.0
: _configService.SeasonPackUpgradeThreshold;
if (upgradablePercentage >= threshold)
{
return DownloadSpecDecision.Accept();
}
}
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskNotUpgrade, $"Season pack does not meet the upgrade criteria. Upgradable: {upgradedCount}/{totalEpisodesInPack} ({upgradablePercentage:0.##}%), Mode: {seasonPackUpgrade}, Threshold: {seasonPackUpgradeThreshold}%");
}
foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
{
if (file == null)
var decision = CheckUpgradeSpecification(file, qualityProfile, subject);
if (decision != null)
{
_logger.Debug("File is no longer available, skipping this file.");
continue;
}
_logger.Debug("Comparing file quality with report. Existing file is {0}.", file.Quality);
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
file.Quality,
_formatService.ParseCustomFormat(file),
subject.ParsedEpisodeInfo.Quality))
{
_logger.Debug("Cutoff already met, rejecting.");
var cutoff = qualityProfile.UpgradeAllowed ? qualityProfile.Cutoff : qualityProfile.FirststAllowedQuality().Id;
var qualityCutoff = qualityProfile.Items[qualityProfile.GetIndex(cutoff).Index];
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file meets cutoff: {0}", qualityCutoff);
}
var customFormats = _formatService.ParseCustomFormat(file);
var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile,
file.Quality,
customFormats,
subject.ParsedEpisodeInfo.Quality,
subject.CustomFormats);
switch (upgradeableRejectReason)
{
case UpgradeableRejectReason.None:
continue;
case UpgradeableRejectReason.BetterQuality:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherPreference, "Existing file on disk is of equal or higher preference: {0}", file.Quality);
case UpgradeableRejectReason.BetterRevision:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherRevision, "Existing file on disk is of equal or higher revision: {0}", file.Quality.Revision);
case UpgradeableRejectReason.QualityCutoff:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]);
case UpgradeableRejectReason.CustomFormatCutoff:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatCutoffMet, "Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore);
case UpgradeableRejectReason.CustomFormatScore:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScore, "Existing file on disk has a equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats));
case UpgradeableRejectReason.MinCustomFormatScore:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScoreIncrement, "Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore);
case UpgradeableRejectReason.UpgradesNotAllowed:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskUpgradesNotAllowed, "Existing file on disk and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name);
return decision;
}
}
return DownloadSpecDecision.Accept();
}
private DownloadSpecDecision CheckUpgradeSpecification(NzbDrone.Core.MediaFiles.EpisodeFile file, NzbDrone.Core.Profiles.Qualities.QualityProfile qualityProfile, RemoteEpisode subject)
{
if (file == null)
{
_logger.Debug("File is no longer available, skipping this file.");
return null;
}
_logger.Debug("Comparing file quality with report. Existing file is {0}.", file.Quality);
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
file.Quality,
_formatService.ParseCustomFormat(file),
subject.ParsedEpisodeInfo.Quality))
{
_logger.Debug("Cutoff already met, rejecting.");
var cutoff = qualityProfile.UpgradeAllowed ? qualityProfile.Cutoff : qualityProfile.FirststAllowedQuality().Id;
var qualityCutoff = qualityProfile.Items[qualityProfile.GetIndex(cutoff).Index];
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file meets cutoff: {0}", qualityCutoff);
}
var customFormats = _formatService.ParseCustomFormat(file);
var upgradeableRejectReason = _upgradableSpecification.IsUpgradable(qualityProfile,
file.Quality,
customFormats,
subject.ParsedEpisodeInfo.Quality,
subject.CustomFormats);
switch (upgradeableRejectReason)
{
case UpgradeableRejectReason.None:
return null;
case UpgradeableRejectReason.BetterQuality:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherPreference, "Existing file on disk is of equal or higher preference: {0}", file.Quality);
case UpgradeableRejectReason.BetterRevision:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskHigherRevision, "Existing file on disk is of equal or higher revision: {0}", file.Quality.Revision);
case UpgradeableRejectReason.QualityCutoff:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCutoffMet, "Existing file on disk meets quality cutoff: {0}", qualityProfile.Items[qualityProfile.GetIndex(qualityProfile.Cutoff).Index]);
case UpgradeableRejectReason.CustomFormatCutoff:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatCutoffMet, "Existing file on disk meets Custom Format cutoff: {0}", qualityProfile.CutoffFormatScore);
case UpgradeableRejectReason.CustomFormatScore:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScore, "Existing file on disk has a equal or higher Custom Format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats));
case UpgradeableRejectReason.MinCustomFormatScore:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskCustomFormatScoreIncrement, "Existing file on disk has Custom Format score within Custom Format score increment: {0}", qualityProfile.MinUpgradeFormatScore);
case UpgradeableRejectReason.UpgradesNotAllowed:
return DownloadSpecDecision.Reject(DownloadRejectionReason.DiskUpgradesNotAllowed, "Existing file on disk and Quality Profile '{0}' does not allow upgrades", qualityProfile.Name);
}
return null;
}
}
}

View file

@ -1858,6 +1858,12 @@
"SeasonFinale": "Season Finale",
"SeasonFolder": "Season Folder",
"SeasonFolderFormat": "Season Folder Format",
"SeasonPackUpgradeAllowAnyWarning": "Allow a season pack if it upgrades any episode. This applies to all sources of automatic grabs.",
"SeasonPackUpgradeAllowHelpText": "Require a season pack to be a quality or custom format upgrade for all episodes",
"SeasonPackUpgradeAllowLabel": "Allow Season Pack Upgrades",
"SeasonPackUpgradeThresholdHelpText": "Require a season pack to be an upgrade for at least X percent of episodes.",
"SeasonPackUpgradeThresholdHelpTextExample": "{numberEpisodes} of {totalEpisodes} episodes: {count}%",
"SeasonPackUpgradeThresholdLabel": "Season Pack Upgrade Threshold",
"SeasonInformation": "Season Information",
"SeasonNumber": "Season Number",
"SeasonNumberToken": "Season {seasonNumber}",

View file

@ -0,0 +1,9 @@
namespace NzbDrone.Core.MediaFiles
{
public enum SeasonPackUpgradeType
{
All = 0,
Threshold = 1,
Any = 2
}
}

View file

@ -31,6 +31,8 @@ public class MediaManagementConfigResource : RestResource
public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; }
public string UserRejectedExtensions { get; set; }
public SeasonPackUpgradeType SeasonPackUpgrade { get; set; }
public double SeasonPackUpgradeThreshold { get; set; }
}
public static class MediaManagementConfigResourceMapper
@ -61,7 +63,9 @@ public static MediaManagementConfigResource ToResource(IConfigService model)
ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo,
UserRejectedExtensions = model.UserRejectedExtensions
UserRejectedExtensions = model.UserRejectedExtensions,
SeasonPackUpgrade = model.SeasonPackUpgrade,
SeasonPackUpgradeThreshold = model.SeasonPackUpgradeThreshold
};
}
}