Skip to content

Commit

Permalink
New: Quality Preferred Setting
Browse files Browse the repository at this point in the history
Co-authored-by: Qstick <qstick@gmail.com>
Closes #724
  • Loading branch information
markus101 committed Dec 10, 2022
1 parent b2b9172 commit d08f33a
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 45 deletions.
Expand Up @@ -31,7 +31,7 @@
background-color: var(--sliderAccentColor);
box-shadow: 0 0 0 #000;

&:nth-child(odd) {
&:nth-child(3n+1) {
background-color: #ddd;
}
}
Expand All @@ -56,7 +56,7 @@
.megabytesPerMinute {
display: flex;
justify-content: space-between;
flex: 0 0 250px;
flex: 0 0 400px;
}

.sizeInput {
Expand Down
81 changes: 70 additions & 11 deletions frontend/src/Settings/Quality/Definition/QualityDefinition.js
Expand Up @@ -51,7 +51,8 @@ class QualityDefinition extends Component {

this.state = {
sliderMinSize: getSliderValue(props.minSize, slider.min),
sliderMaxSize: getSliderValue(props.maxSize, slider.max)
sliderMaxSize: getSliderValue(props.maxSize, slider.max),
sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3))
};
}

Expand Down Expand Up @@ -93,27 +94,31 @@ class QualityDefinition extends Component {
//
// Listeners

onSliderChange = ([sliderMinSize, sliderMaxSize]) => {
onSliderChange = ([sliderMinSize, sliderPreferredSize, sliderMaxSize]) => {
this.setState({
sliderMinSize,
sliderMaxSize
sliderMaxSize,
sliderPreferredSize
});

this.props.onSizeChange({
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
preferredSize: sliderPreferredSize === (slider.max - 3) ? null : roundNumber(Math.pow(sliderPreferredSize, 1.1)),
maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1))
});
};

onAfterSliderChange = () => {
const {
minSize,
maxSize
maxSize,
preferredSize
} = this.props;

this.setState({
sliderMiSize: getSliderValue(minSize, slider.min),
sliderMaxSize: getSliderValue(maxSize, slider.max)
sliderMaxSize: getSliderValue(maxSize, slider.max),
sliderPreferredSize: getSliderValue(preferredSize, (slider.max - 3)) // fix
});
};

Expand All @@ -126,7 +131,22 @@ class QualityDefinition extends Component {

this.props.onSizeChange({
minSize,
maxSize: this.props.maxSize
maxSize: this.props.maxSize,
preferredSize: this.props.preferredSize
});
};

onPreferredSizeChange = ({ value }) => {
const preferredSize = value === (MAX - 3) ? null : getValue(value);

this.setState({
sliderPreferredSize: getSliderValue(preferredSize, slider.preferred)
});

this.props.onSizeChange({
minSize: this.props.minSize,
maxSize: this.props.maxSize,
preferredSize
});
};

Expand All @@ -139,7 +159,8 @@ class QualityDefinition extends Component {

this.props.onSizeChange({
minSize: this.props.minSize,
maxSize
maxSize,
preferredSize: this.props.preferredSize
});
};

Expand All @@ -153,18 +174,23 @@ class QualityDefinition extends Component {
title,
minSize,
maxSize,
preferredSize,
advancedSettings,
onTitleChange
} = this.props;

const {
sliderMinSize,
sliderMaxSize
sliderMaxSize,
sliderPreferredSize
} = this.state;

const minBytes = minSize * 1024 * 1024;
const minSixty = `${formatBytes(minBytes * 60)}/h`;

const preferredBytes = preferredSize * 1024 * 1024;
const preferredSixty = preferredBytes ? `${formatBytes(preferredBytes * 60)}/h` : 'Unlimited';

const maxBytes = maxSize && maxSize * 1024 * 1024;
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/h` : 'Unlimited';

Expand All @@ -188,9 +214,10 @@ class QualityDefinition extends Component {
min={slider.min}
max={slider.max}
step={slider.step}
minDistance={MIN_DISTANCE * 5}
value={[sliderMinSize, sliderMaxSize]}
minDistance={3}
value={[sliderMinSize, sliderPreferredSize, sliderMaxSize]}
withTracks={true}
allowCross={false}
snapDragDisabled={true}
renderThumb={this.thumbRenderer}
renderTrack={this.trackRenderer}
Expand All @@ -215,6 +242,22 @@ class QualityDefinition extends Component {
/>
</div>

<div>
<Popover
anchor={
<Label kind={kinds.SUCCESS}>{preferredSixty}</Label>
}
title="Preferred Size"
body={
<QualityDefinitionLimits
bytes={preferredBytes}
message="No limit for any runtime"
/>
}
position={tooltipPositions.BOTTOM}
/>
</div>

<div>
<Popover
anchor={
Expand Down Expand Up @@ -244,13 +287,28 @@ class QualityDefinition extends Component {
name={`${id}.min`}
value={minSize || MIN}
min={MIN}
max={maxSize ? maxSize - MIN_DISTANCE : MAX - MIN_DISTANCE}
max={preferredSize ? preferredSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
onChange={this.onMinSizeChange}
/>
</div>

<div>
Preferred

<NumberInput
className={styles.sizeInput}
name={`${id}.min`}
value={preferredSize || MAX - 5}
min={MIN}
max={maxSize ? maxSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
onChange={this.onPreferredSizeChange}
/>
</div>

<div>
Max

Expand Down Expand Up @@ -278,6 +336,7 @@ QualityDefinition.propTypes = {
title: PropTypes.string.isRequired,
minSize: PropTypes.number,
maxSize: PropTypes.number,
preferredSize: PropTypes.number,
advancedSettings: PropTypes.bool.isRequired,
onTitleChange: PropTypes.func.isRequired,
onSizeChange: PropTypes.func.isRequired
Expand Down
Expand Up @@ -23,11 +23,12 @@ class QualityDefinitionConnector extends Component {
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
};

onSizeChange = ({ minSize, maxSize }) => {
onSizeChange = ({ minSize, maxSize, preferredSize }) => {
const {
id,
minSize: currentMinSize,
maxSize: currentMaxSize
maxSize: currentMaxSize,
preferredSize: currentPreferredSize
} = this.props;

if (minSize !== currentMinSize) {
Expand All @@ -37,6 +38,10 @@ class QualityDefinitionConnector extends Component {
if (maxSize !== currentMaxSize) {
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
}

if (preferredSize !== currentPreferredSize) {
this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize });
}
};

//
Expand All @@ -57,6 +62,7 @@ QualityDefinitionConnector.propTypes = {
id: PropTypes.number.isRequired,
minSize: PropTypes.number,
maxSize: PropTypes.number,
preferredSize: PropTypes.number,
setQualityDefinitionValue: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
Expand Down
Expand Up @@ -15,7 +15,6 @@
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Test.Languages;
using NzbDrone.Core.Tv;

namespace NzbDrone.Core.Test.DecisionEngineTests
Expand All @@ -27,6 +26,17 @@ public class PrioritizeDownloadDecisionFixture : CoreTest<DownloadDecisionPriori
public void Setup()
{
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);

Mocker.GetMock<IQualityDefinitionService>()
.Setup(s => s.Get(It.IsAny<Quality>()))
.Returns(new QualityDefinition { PreferredSize = null });
}

private void GivenPreferredSize(double? size)
{
Mocker.GetMock<IQualityDefinitionService>()
.Setup(s => s.Get(It.IsAny<Quality>()))
.Returns(new QualityDefinition { PreferredSize = size });
}

private Episode GivenEpisode(int id)
Expand Down Expand Up @@ -54,6 +64,7 @@ private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel qu
remoteEpisode.Release.IndexerPriority = indexerPriority;

remoteEpisode.Series = Builder<Series>.CreateNew()
.With(e => e.Runtime = 60)
.With(e => e.QualityProfile = new QualityProfile
{
Items = Qualities.QualityFixture.GetDefaultQualities()
Expand Down Expand Up @@ -161,6 +172,44 @@ public void should_order_by_age_then_largest_rounded_to_200mb()
qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeHdLargeYoung);
}

[Test]
public void should_order_by_closest_to_preferred_size_if_both_under()
{
// 200 MB/Min * 60 Min Runtime = 12000 MB
GivenPreferredSize(200);

var remoteEpisodeSmall = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 1200.Megabytes(), age: 1);
var remoteEpisodeLarge = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 10000.Megabytes(), age: 1);

var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisodeSmall));
decisions.Add(new DownloadDecision(remoteEpisodeLarge));

var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeLarge);
}

[Test]
public void should_order_by_closest_to_preferred_size_if_preferred_is_in_between()
{
// 46 MB/Min * 60 Min Runtime = 6900 MB
GivenPreferredSize(46);

var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 500.Megabytes(), age: 1);
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 2000.Megabytes(), age: 1);
var remoteEpisode3 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 3000.Megabytes(), age: 1);
var remoteEpisode4 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 5000.Megabytes(), age: 1);

var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisode1));
decisions.Add(new DownloadDecision(remoteEpisode2));
decisions.Add(new DownloadDecision(remoteEpisode3));
decisions.Add(new DownloadDecision(remoteEpisode4));

var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisode3);
}

[Test]
public void should_order_by_youngest()
{
Expand Down
@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;

namespace NzbDrone.Core.Datastore.Migration
{
[Migration(181)]
public class quality_definition_preferred_size : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("QualityDefinitions").AddColumn("PreferredSize").AsDouble().Nullable();

Execute.Sql("UPDATE QualityDefinitions SET PreferredSize = MaxSize - 5 WHERE MaxSize > 5");
}
}
}
24 changes: 21 additions & 3 deletions src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs
Expand Up @@ -14,14 +14,16 @@ public class DownloadDecisionComparer : IComparer<DownloadDecision>
{
private readonly IConfigService _configService;
private readonly IDelayProfileService _delayProfileService;
private readonly IQualityDefinitionService _qualityDefinitionService;

public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y);
public delegate int CompareDelegate<TSubject, TValue>(DownloadDecision x, DownloadDecision y);

public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService)
public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService, IQualityDefinitionService qualityDefinitionService)
{
_configService = configService;
_delayProfileService = delayProfileService;
_qualityDefinitionService = qualityDefinitionService;
}

public int Compare(DownloadDecision x, DownloadDecision y)
Expand Down Expand Up @@ -180,9 +182,25 @@ private int CompareAgeIfUsenet(DownloadDecision x, DownloadDecision y)

private int CompareSize(DownloadDecision x, DownloadDecision y)
{
// TODO: Is smaller better? Smaller for usenet could mean no par2 files.
var sizeCompare = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode =>
{
var preferredSize = _qualityDefinitionService.Get(remoteEpisode.ParsedEpisodeInfo.Quality.Quality).PreferredSize;
// If no value for preferred it means unlimited so fallback to sort largest is best
if (preferredSize.HasValue && remoteEpisode.Series.Runtime > 0)
{
var preferredMovieSize = remoteEpisode.Series.Runtime * preferredSize.Value.Megabytes();
// Calculate closest to the preferred size
return Math.Abs((remoteEpisode.Release.Size - preferredMovieSize).Round(200.Megabytes())) * (-1);
}
else
{
return remoteEpisode.Release.Size.Round(200.Megabytes());
}
});

return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Release.Size.Round(200.Megabytes()));
return sizeCompare;
}
}
}

0 comments on commit d08f33a

Please sign in to comment.