From bd20ebfad7f016ef0449b436c8b51c2761eeaef5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Mon, 11 Aug 2025 06:06:20 +0200 Subject: [PATCH] New: Indexer option for Season Pack Seed Ratio --- .../IndexerTests/SeedConfigProviderFixture.cs | 67 +++++++++++++++++++ .../220_enable_season_pack_seeding_goal.cs | 66 ++++++++++++++++++ .../Indexers/SeasonPackSeedGoal.cs | 11 +++ .../Indexers/SeedConfigProvider.cs | 8 ++- .../Indexers/SeedCriteriaSettings.cs | 17 ++++- src/NzbDrone.Core/Localization/Core/en.json | 10 ++- 6 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/220_enable_season_pack_seeding_goal.cs create mode 100644 src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs index 0dc71467e..203d9f2a9 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs @@ -59,6 +59,7 @@ public void should_not_return_config_for_invalid_indexer() public void should_return_season_time_for_season_packs() { var settings = new TorznabSettings(); + settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal; settings.SeedCriteria.SeasonPackSeedTime = 10; Mocker.GetMock() @@ -85,5 +86,71 @@ public void should_return_season_time_for_season_packs() result.Should().NotBeNull(); result.SeedTime.Should().Be(TimeSpan.FromMinutes(10)); } + + [Test] + public void should_return_season_ratio_for_season_packs_when_set() + { + var settings = new TorznabSettings(); + settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal; + settings.SeedCriteria.SeedRatio = 1.0; + settings.SeedCriteria.SeasonPackSeedRatio = 10.0; + + Mocker.GetMock() + .Setup(v => v.GetSettings(It.IsAny())) + .Returns(new CachedIndexerSettings + { + FailDownloads = new HashSet { FailDownloads.Executables }, + SeedCriteriaSettings = settings.SeedCriteria + }); + + var result = Subject.GetSeedConfiguration(new RemoteEpisode + { + Release = new ReleaseInfo + { + DownloadProtocol = DownloadProtocol.Torrent, + IndexerId = 1 + }, + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + FullSeason = true + } + }); + + result.Should().NotBeNull(); + result.Ratio.Should().Be(10.0); + } + + [Test] + public void should_return_standard_ratio_for_season_packs_when_not_set() + { + var settings = new TorznabSettings(); + settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseStandardSeedGoal; + settings.SeedCriteria.SeedRatio = 1.0; + settings.SeedCriteria.SeasonPackSeedRatio = 10.0; + + Mocker.GetMock() + .Setup(v => v.GetSettings(It.IsAny())) + .Returns(new CachedIndexerSettings + { + FailDownloads = new HashSet { FailDownloads.Executables }, + SeedCriteriaSettings = settings.SeedCriteria + }); + + var result = Subject.GetSeedConfiguration(new RemoteEpisode + { + Release = new ReleaseInfo + { + DownloadProtocol = DownloadProtocol.Torrent, + IndexerId = 1 + }, + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + FullSeason = true + } + }); + + result.Should().NotBeNull(); + result.Ratio.Should().Be(1.0); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/220_enable_season_pack_seeding_goal.cs b/src/NzbDrone.Core/Datastore/Migration/220_enable_season_pack_seeding_goal.cs new file mode 100644 index 000000000..bc17da6d2 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/220_enable_season_pack_seeding_goal.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(229)] + public class enable_season_pack_seeding_goal : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(SetSeasonPackSeedingGoal); + } + + private void SetSeasonPackSeedingGoal(IDbConnection conn, IDbTransaction tran) + { + var updatedIndexers = new List(); + + using var selectCommand = conn.CreateCommand(); + + selectCommand.Transaction = tran; + selectCommand.CommandText = "SELECT * FROM \"Indexers\""; + + using var reader = selectCommand.ExecuteReader(); + + while (reader.Read()) + { + var idIndex = reader.GetOrdinal("Id"); + var settingsIndex = reader.GetOrdinal("Settings"); + + var id = reader.GetInt32(idIndex); + var settings = Json.Deserialize>(reader.GetString(settingsIndex)); + + if (settings.TryGetValue("seedCriteria", out var seedCriteriaToken) && seedCriteriaToken is JObject seedCriteria) + { + if (seedCriteria?["seasonPackSeedTime"] != null) + { + seedCriteria["seasonPackSeedGoal"] = 1; + + if (seedCriteria["seedRatio"] != null) + { + seedCriteria["seasonPackSeedRatio"] = seedCriteria["seedRatio"]; + } + + updatedIndexers.Add(new + { + Settings = settings.ToJson(), + Id = id, + }); + } + } + } + + if (updatedIndexers.Any()) + { + var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updatedIndexers, transaction: tran); + } + } + } +} diff --git a/src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs b/src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs new file mode 100644 index 000000000..7141a0a1e --- /dev/null +++ b/src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Indexers; + +public enum SeasonPackSeedGoal +{ + [FieldOption(Label = "IndexerSettingsSeasonPackSeedGoalUseStandardGoals")] + UseStandardSeedGoal = 0, + [FieldOption(Label = "IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals")] + UseSeasonPackSeedGoal = 1 +} diff --git a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs index 29c995037..f4043fed0 100644 --- a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs +++ b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs @@ -49,12 +49,16 @@ public TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSea return null; } + var useSeasonPackSeedGoal = (SeasonPackSeedGoal)seedCriteria.SeasonPackSeedGoal == SeasonPackSeedGoal.UseSeasonPackSeedGoal; + var seedConfig = new TorrentSeedConfiguration { - Ratio = seedCriteria.SeedRatio + Ratio = (fullSeason && useSeasonPackSeedGoal) + ? seedCriteria.SeasonPackSeedRatio + : seedCriteria.SeedRatio }; - var seedTime = fullSeason ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime; + var seedTime = (fullSeason && useSeasonPackSeedGoal) ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime; if (seedTime.HasValue) { seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value); diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs index 144b69f1a..2a673ff6e 100644 --- a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -17,6 +17,10 @@ public SeedCriteriaSettingsValidator(double seedRatioMinimum = 0.0, int seedTime .When(c => c.SeedTime.HasValue) .AsWarning().WithMessage("Should be greater than zero"); + RuleFor(c => c.SeasonPackSeedRatio).GreaterThan(0.0) + .When(c => c.SeasonPackSeedRatio.HasValue) + .AsWarning().WithMessage("Should be greater than zero"); + RuleFor(c => c.SeasonPackSeedTime).GreaterThan(0) .When(c => c.SeasonPackSeedTime.HasValue) .AsWarning().WithMessage("Should be greater than zero"); @@ -27,6 +31,11 @@ public SeedCriteriaSettingsValidator(double seedRatioMinimum = 0.0, int seedTime .When(c => c.SeedRatio > 0.0) .AsWarning() .WithMessage($"Under {seedRatioMinimum} leads to H&R"); + + RuleFor(c => c.SeasonPackSeedRatio).GreaterThanOrEqualTo(seedRatioMinimum) + .When(c => c.SeasonPackSeedRatio > 0.0) + .AsWarning() + .WithMessage($"Under {seedRatioMinimum} leads to H&R"); } if (seedTimeMinimum != 0) @@ -55,7 +64,13 @@ public class SeedCriteriaSettings : PropertywiseEquatable [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)] public int? SeedTime { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "IndexerSettingsSeasonPackSeedTimeHelpText", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Select, Label = "IndexerSettingsSeasonPackSeedGoal", SelectOptions = typeof(SeasonPackSeedGoal), HelpText = "IndexerSettingsSeasonPackSeedGoalHelpText", Advanced = true)] + public int SeasonPackSeedGoal { get; set; } + + [FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsSeasonPackSeedRatio", HelpText = "IndexerSettingsSeasonPackSeedRatioHelpText", Advanced = true)] + public double? SeasonPackSeedRatio { get; set; } + + [FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsSeasonPackSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeasonPackSeedTimeHelpText", Advanced = true)] public int? SeasonPackSeedTime { get; set; } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index fb01f2422..bda8d7c56 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1025,8 +1025,14 @@ "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", - "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", - "IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season-pack torrent should be seeded before stopping, empty uses the download client's default", + "IndexerSettingsSeasonPackSeedGoal": "Seeding Goal for Season Packs", + "IndexerSettingsSeasonPackSeedGoalHelpText": "Choose whether to use different seeding goals for season packs", + "IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Use Standard Goals", + "IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Use Season Pack Goals", + "IndexerSettingsSeasonPackSeedRatio": "Season Pack Seed Ratio", + "IndexerSettingsSeasonPackSeedRatioHelpText": "The ratio a season pack torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules", + "IndexerSettingsSeasonPackSeedTime": "Season Pack Seed Time", + "IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season pack torrent should be seeded before stopping, empty uses the download client's default", "IndexerSettingsSeedRatio": "Seed Ratio", "IndexerSettingsSeedRatioHelpText": "The ratio a torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules", "IndexerSettingsSeedTime": "Seed Time",