mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-05 11:30:51 +02:00
New: Setting to allow for grabbing season packs even if some episodes already meet cutoff
Closes #6378
This commit is contained in:
parent
1610e54650
commit
cf6b21aef6
15 changed files with 436 additions and 62 deletions
19
frontend/src/Components/Form/FloatInput.tsx
Normal file
19
frontend/src/Components/Form/FloatInput.tsx
Normal 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;
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,6 @@ export default interface MediaManagement {
|
|||
extraFileExtensions: string;
|
||||
userRejectedExtensions: string;
|
||||
enableMediaInfo: boolean;
|
||||
seasonPackUpgrade: string;
|
||||
seasonPackUpgradeThreshold: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -74,5 +74,6 @@ public enum DownloadRejectionReason
|
|||
DiskCustomFormatCutoffMet,
|
||||
DiskCustomFormatScore,
|
||||
DiskCustomFormatScoreIncrement,
|
||||
DiskUpgradesNotAllowed
|
||||
DiskUpgradesNotAllowed,
|
||||
DiskNotUpgrade
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
9
src/NzbDrone.Core/MediaFiles/SeasonPackUpgradeType.cs
Executable file
9
src/NzbDrone.Core/MediaFiles/SeasonPackUpgradeType.cs
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public enum SeasonPackUpgradeType
|
||||
{
|
||||
All = 0,
|
||||
Threshold = 1,
|
||||
Any = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue