diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx index 88cc7d63c..69b571181 100644 --- a/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx +++ b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx @@ -6,6 +6,7 @@ import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; +import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { inputTypes, kinds } from 'Helpers/Props'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; @@ -25,6 +26,27 @@ import translate from 'Utilities/String/translate'; const SECTION = 'indexerOptions'; +const seedersPreferenceOptions: EnhancedSelectInputValue[] = [ + { + key: 'default', + get value() { + return translate('SeedersPreferenceDefault'); + }, + }, + { + key: 'high', + get value() { + return translate('SeedersPreferenceHigh'); + }, + }, + { + key: 'highest', + get value() { + return translate('SeedersPreferenceHighest'); + }, + }, +]; + interface IndexerOptionsProps { setChildSave: SetChildSave; onChildStateChange: OnChildStateChange; @@ -143,6 +165,19 @@ function IndexerOptions({ {...settings.rssSyncInterval} /> + + + {translate('SeedersPreference')} + + + ) : null} diff --git a/frontend/src/typings/Settings/IndexerOptions.ts b/frontend/src/typings/Settings/IndexerOptions.ts index 1eb21de6e..1abd6533c 100644 --- a/frontend/src/typings/Settings/IndexerOptions.ts +++ b/frontend/src/typings/Settings/IndexerOptions.ts @@ -3,4 +3,5 @@ export default interface IndexerOptions { retention: number; maximumSize: number; rssSyncInterval: number; + seedersPreference: string; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index 9c2120212..0b2a4de4e 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -638,5 +638,128 @@ public void ensure_download_decisions_indexer_priority_is_not_perfered_over_qual qualifiedReports.Skip(2).First().RemoteEpisode.Should().Be(remoteEpisode1); qualifiedReports.Last().RemoteEpisode.Should().Be(remoteEpisode3); } + + [Test] + public void should_prefer_quality_over_seeders_when_seeders_preference_is_default() + { + Mocker.GetMock() + .Setup(s => s.SeedersPreference) + .Returns(SeedersPreferenceType.Default); + + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.Bluray1080p), Language.English); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV), Language.English); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + torrentInfo1.Size = 200.Megabytes(); + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 1000; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteEpisode.ParsedEpisodeInfo.Quality.Quality.Should().Be(Quality.Bluray1080p); + } + + [Test] + public void should_prefer_seeders_over_quality_when_seeders_preference_is_highest() + { + Mocker.GetMock() + .Setup(s => s.SeedersPreference) + .Returns(SeedersPreferenceType.Highest); + + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.Bluray1080p), Language.English); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV), Language.English); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + torrentInfo1.Size = 200.Megabytes(); + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 1000; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo)qualifiedReports.First().RemoteEpisode.Release).Seeders.Should().Be(1000); + } + + [Test] + public void should_prefer_quality_over_seeders_when_seeders_preference_is_high() + { + Mocker.GetMock() + .Setup(s => s.SeedersPreference) + .Returns(SeedersPreferenceType.High); + + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.Bluray1080p), Language.English); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV), Language.English); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + torrentInfo1.Size = 200.Megabytes(); + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 1000; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteEpisode.ParsedEpisodeInfo.Quality.Quality.Should().Be(Quality.Bluray1080p); + } + + [Test] + public void should_prefer_seeders_over_custom_format_score_when_seeders_preference_is_high() + { + Mocker.GetMock() + .Setup(s => s.SeedersPreference) + .Returns(SeedersPreferenceType.High); + + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English); + + remoteEpisode1.CustomFormatScore = 100; + remoteEpisode2.CustomFormatScore = 0; + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + torrentInfo1.Size = 200.Megabytes(); + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 1000; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo)qualifiedReports.First().RemoteEpisode.Release).Seeders.Should().Be(1000); + } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index cf5a35529..eb960aa8e 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -124,6 +124,13 @@ public int MinimumAge set { SetValue("MinimumAge", value); } } + public SeedersPreferenceType SeedersPreference + { + get { return GetValueEnum("SeedersPreference", SeedersPreferenceType.Default); } + + set { SetValue("SeedersPreference", value); } + } + public ProperDownloadTypes DownloadPropersAndRepacks { get { return GetValueEnum("DownloadPropersAndRepacks", ProperDownloadTypes.PreferAndUpgrade); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 5ebb51b94..e76857a63 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -57,6 +57,7 @@ public interface IConfigService int RssSyncInterval { get; set; } int MaximumSize { get; set; } int MinimumAge { get; set; } + SeedersPreferenceType SeedersPreference { get; set; } ListSyncLevelType ListSyncLevel { get; set; } int ListSyncTag { get; set; } diff --git a/src/NzbDrone.Core/Configuration/SeedersPreferenceType.cs b/src/NzbDrone.Core/Configuration/SeedersPreferenceType.cs new file mode 100644 index 000000000..4984faf05 --- /dev/null +++ b/src/NzbDrone.Core/Configuration/SeedersPreferenceType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Configuration +{ + public enum SeedersPreferenceType + { + Default, + High, + Highest + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index e7a68d864..97af98937 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -27,18 +27,34 @@ public DownloadDecisionComparer(IConfigService configService, IDelayProfileServi public int Compare(DownloadDecision x, DownloadDecision y) { - var comparers = new List + var comparers = new List(); + var seedersPreference = _configService.SeedersPreference; + + if (seedersPreference == SeedersPreferenceType.Highest) { - CompareQuality, - CompareCustomFormatScore, - CompareProtocol, - CompareEpisodeCount, - CompareEpisodeNumber, - CompareIndexerPriority, - ComparePeersIfTorrent, - CompareAgeIfUsenet, - CompareSize - }; + comparers.Add(ComparePeersIfTorrent); + } + + comparers.Add(CompareQuality); + + if (seedersPreference == SeedersPreferenceType.High) + { + comparers.Add(ComparePeersIfTorrent); + } + + comparers.Add(CompareCustomFormatScore); + comparers.Add(CompareProtocol); + comparers.Add(CompareEpisodeCount); + comparers.Add(CompareEpisodeNumber); + comparers.Add(CompareIndexerPriority); + + if (seedersPreference == SeedersPreferenceType.Default) + { + comparers.Add(ComparePeersIfTorrent); + } + + comparers.Add(CompareAgeIfUsenet); + comparers.Add(CompareSize); return comparers.Select(comparer => comparer(x, y)).FirstOrDefault(result => result != 0); } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 37a93f68b..4ca8d599c 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -1832,6 +1832,11 @@ "SecretToken": "Geheimer Token", "Security": "Sicherheit", "Seeders": "Seeders", + "SeedersPreference": "Seeder-Präferenz", + "SeedersPreferenceDefault": "Standard", + "SeedersPreferenceHelpText": "Nur Torrent: Steuert, wie die Seeder-Anzahl die Release-Priorisierung beeinflusst. Standard verwendet Seeder als niedrig priorisierten Tiebreaker. Hoch priorisiert Seeder direkt nach der Qualität. Höchste macht Seeder zum wichtigsten Faktor.", + "SeedersPreferenceHigh": "Hoch (nach Qualität)", + "SeedersPreferenceHighest": "Höchste (vor Qualität)", "SelectAll": "Alles auswählen", "SelectDownloadClientModalTitle": "{modalTitle} – Wähle Download-Client", "SelectDropdown": "Auswählen...", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 67c9d14b7..89a439f44 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1908,6 +1908,11 @@ "SecretToken": "Secret Token", "Security": "Security", "Seeders": "Seeders", + "SeedersPreference": "Seeders Preference", + "SeedersPreferenceDefault": "Default", + "SeedersPreferenceHelpText": "Torrent only: Controls how seeder count affects release prioritization. Default uses seeders as a low-priority tiebreaker. High prioritizes seeders right after quality. Highest makes seeders the most important factor.", + "SeedersPreferenceHigh": "High (after quality)", + "SeedersPreferenceHighest": "Highest (before quality)", "SelectAll": "Select All", "SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client", "SelectDropdown": "Select...", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index b730ec141..26f3ffc54 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1900,6 +1900,11 @@ "SecretToken": "Token secreto", "Security": "Seguridad", "Seeders": "Semillas", + "SeedersPreference": "Preferencia de semillas", + "SeedersPreferenceDefault": "Por defecto", + "SeedersPreferenceHelpText": "Solo torrent: Controla cómo el número de semillas afecta la priorización de releases. Por defecto usa las semillas como desempate de baja prioridad. Alto prioriza las semillas justo después de la calidad. Máximo hace que las semillas sean el factor más importante.", + "SeedersPreferenceHigh": "Alto (después de calidad)", + "SeedersPreferenceHighest": "Máximo (antes de calidad)", "SelectAll": "Seleccionar todo", "SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga", "SelectDropdown": "Seleccionar...", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 6b69dfd52..577745425 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1891,6 +1891,11 @@ "SecretToken": "Jeton secret", "Security": "Sécurité", "Seeders": "Seeders", + "SeedersPreference": "Préférence de seeders", + "SeedersPreferenceDefault": "Par défaut", + "SeedersPreferenceHelpText": "Torrent uniquement : Contrôle l'impact du nombre de seeders sur la priorité des releases. Par défaut utilise les seeders comme critère de départage de faible priorité. Élevé priorise les seeders juste après la qualité. Maximum fait des seeders le facteur le plus important.", + "SeedersPreferenceHigh": "Élevé (après la qualité)", + "SeedersPreferenceHighest": "Maximum (avant la qualité)", "SelectAll": "Tout sélectionner", "SelectDownloadClientModalTitle": "{modalTitle} – Sélectionnez le client de téléchargement", "SelectDropdown": "Sélectionner...", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 284d6dfa1..5775047fe 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1899,6 +1899,11 @@ "SecretToken": "Token Secreto", "Security": "Segurança", "Seeders": "Sementes", + "SeedersPreference": "Preferência de Sementes", + "SeedersPreferenceDefault": "Padrão", + "SeedersPreferenceHelpText": "Apenas torrent: Controla como a contagem de sementes afeta a priorização de releases. Padrão usa sementes como desempate de baixa prioridade. Alto prioriza sementes logo após a qualidade. Máximo faz das sementes o fator mais importante.", + "SeedersPreferenceHigh": "Alto (após qualidade)", + "SeedersPreferenceHighest": "Máximo (antes da qualidade)", "SelectAll": "Selecionar Tudo", "SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download", "SelectDropdown": "Selecionar...", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 1ffc5bf57..1fa8de0d9 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1665,6 +1665,11 @@ "SecretToken": "密钥令牌", "Security": "安全", "Seeders": "种子", + "SeedersPreference": "种子偏好", + "SeedersPreferenceDefault": "默认", + "SeedersPreferenceHelpText": "仅限种子:控制种子数量如何影响发布优先级。默认将种子作为低优先级的决胜因素。高优先级在质量之后优先考虑种子。最高优先级使种子成为最重要的因素。", + "SeedersPreferenceHigh": "高(质量之后)", + "SeedersPreferenceHighest": "最高(质量之前)", "SelectAll": "全选", "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载客户端", "SelectDropdown": "选择…", diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs index 6082d18b1..151ddf661 100644 --- a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs @@ -9,6 +9,7 @@ public class IndexerConfigResource : RestResource public int Retention { get; set; } public int MaximumSize { get; set; } public int RssSyncInterval { get; set; } + public SeedersPreferenceType SeedersPreference { get; set; } } public static class IndexerConfigResourceMapper @@ -20,7 +21,8 @@ public static IndexerConfigResource ToResource(IConfigService model) MinimumAge = model.MinimumAge, Retention = model.Retention, MaximumSize = model.MaximumSize, - RssSyncInterval = model.RssSyncInterval + RssSyncInterval = model.RssSyncInterval, + SeedersPreference = model.SeedersPreference }; } }