mirror of
https://github.com/Sonarr/Sonarr
synced 2026-01-26 01:11:47 +01:00
New: Selecting multiple indexers per release profile
This commit is contained in:
parent
bee7e4325f
commit
3bbc744493
12 changed files with 179 additions and 62 deletions
|
|
@ -40,9 +40,9 @@ function createIndexersSelector(includeAny: boolean) {
|
|||
|
||||
export interface IndexerSelectInputProps {
|
||||
name: string;
|
||||
value: number;
|
||||
value: number | number[];
|
||||
includeAny?: boolean;
|
||||
onChange: (change: EnhancedSelectInputChanged<number>) => void;
|
||||
onChange: (change: EnhancedSelectInputChanged<number | number[]>) => void;
|
||||
}
|
||||
|
||||
function IndexerSelectInput({
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function EditReleaseProfileModalContent({
|
|||
saveProvider,
|
||||
} = useManageReleaseProfile(id ?? 0);
|
||||
|
||||
const { name, enabled, required, ignored, tags, excludedTags, indexerId } =
|
||||
const { name, enabled, required, ignored, indexerIds, tags, excludedTags } =
|
||||
item;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
|
@ -136,13 +136,12 @@ function EditReleaseProfileModalContent({
|
|||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_SELECT}
|
||||
name="indexerId"
|
||||
name="indexerIds"
|
||||
helpText={translate('ReleaseProfileIndexerHelpText')}
|
||||
helpTextWarning={translate(
|
||||
'ReleaseProfileIndexerHelpTextWarning'
|
||||
)}
|
||||
{...indexerId}
|
||||
includeAny={true}
|
||||
{...indexerIds}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
|
|||
enabled = true,
|
||||
required = [],
|
||||
ignored = [],
|
||||
indexerIds = [],
|
||||
tags,
|
||||
excludedTags,
|
||||
indexerId = 0,
|
||||
tagList,
|
||||
indexerList,
|
||||
} = props;
|
||||
|
|
@ -53,8 +53,7 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
|
|||
deleteReleaseProfile();
|
||||
}, [deleteReleaseProfile]);
|
||||
|
||||
const indexer =
|
||||
indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
|
||||
const indexers = indexerList.filter((i) => indexerIds.includes(i.id));
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -103,11 +102,11 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
|
|||
</Label>
|
||||
)}
|
||||
|
||||
{indexer ? (
|
||||
<Label kind={kinds.INFO} outline={true}>
|
||||
{indexers.map((indexer) => (
|
||||
<Label key={indexer.id} kind={kinds.INFO} outline={true}>
|
||||
{indexer.name}
|
||||
</Label>
|
||||
) : null}
|
||||
))}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModal
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface ReleaseProfileModel extends ModelBase {
|
|||
enabled: boolean;
|
||||
required: string[];
|
||||
ignored: string[];
|
||||
indexerId: number;
|
||||
indexerIds: number[];
|
||||
tags: number[];
|
||||
excludedTags: number[];
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ const NEW_RELEASE_PROFILE: ReleaseProfileModel = {
|
|||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
indexerId: 0,
|
||||
indexerIds: [],
|
||||
tags: [],
|
||||
excludedTags: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration;
|
||||
|
||||
[TestFixture]
|
||||
public class release_profile_indexer_idsFixture : MigrationTest<release_profile_indexer_ids>
|
||||
{
|
||||
[Test]
|
||||
public void should_convert_default_value_for_indexer_id_to_list()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Name = "Profile",
|
||||
Enabled = true,
|
||||
Required = "[]",
|
||||
Ignored = "[]",
|
||||
IndexerId = 0,
|
||||
Tags = "[]",
|
||||
ExcludedTags = "[]",
|
||||
});
|
||||
});
|
||||
|
||||
var releaseProfiles = db.Query<ReleaseProfile224>("SELECT \"Id\", \"Name\", \"IndexerIds\" FROM \"ReleaseProfiles\"");
|
||||
|
||||
releaseProfiles.Should().HaveCount(1);
|
||||
releaseProfiles.First().Name.Should().Be("Profile");
|
||||
releaseProfiles.First().IndexerIds.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_convert_single_value_for_indexer_id_to_list()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Name = "Profile",
|
||||
Enabled = true,
|
||||
Required = "[]",
|
||||
Ignored = "[]",
|
||||
IndexerId = 42,
|
||||
Tags = "[]",
|
||||
ExcludedTags = "[]",
|
||||
});
|
||||
});
|
||||
|
||||
var releaseProfiles = db.Query<ReleaseProfile224>("SELECT \"Id\", \"Name\", \"IndexerIds\" FROM \"ReleaseProfiles\"");
|
||||
|
||||
releaseProfiles.Should().HaveCount(1);
|
||||
releaseProfiles.First().Name.Should().Be("Profile");
|
||||
releaseProfiles.First().IndexerIds.Should().BeEquivalentTo(new List<int> { 42 });
|
||||
}
|
||||
}
|
||||
|
||||
internal class ReleaseProfile224
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(224)]
|
||||
public class release_profile_indexer_ids : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ReleaseProfiles").AddColumn("IndexerIds").AsString().WithDefaultValue("[]");
|
||||
|
||||
Execute.WithConnection(MigrateIndexerIds);
|
||||
|
||||
Delete.Column("IndexerId").FromTable("ReleaseProfiles");
|
||||
}
|
||||
|
||||
private void MigrateIndexerIds(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updated = new List<object>();
|
||||
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT \"Id\", \"IndexerId\" FROM \"ReleaseProfiles\"";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var indexerId = reader.GetInt32(1);
|
||||
|
||||
var indexerIds = new List<int>();
|
||||
|
||||
if (indexerId > 0)
|
||||
{
|
||||
indexerIds.Add(indexerId);
|
||||
}
|
||||
|
||||
updated.Add(new
|
||||
{
|
||||
Id = id,
|
||||
IndexerIds = indexerIds.ToJson()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
conn.Execute(
|
||||
"UPDATE \"ReleaseProfiles\" SET \"IndexerIds\" = @IndexerIds WHERE \"Id\" = @Id",
|
||||
updated,
|
||||
transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ public class ReleaseProfile : ModelBase
|
|||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public HashSet<int> ExcludedTags { get; set; }
|
||||
|
||||
|
|
@ -18,9 +18,9 @@ public ReleaseProfile()
|
|||
Enabled = true;
|
||||
Required = new List<string>();
|
||||
Ignored = new List<string>();
|
||||
IndexerIds = new List<int>();
|
||||
Tags = new HashSet<int>();
|
||||
ExcludedTags = new HashSet<int>();
|
||||
IndexerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
|
|
@ -21,12 +20,10 @@ public interface IReleaseProfileService
|
|||
public class ReleaseProfileService : IReleaseProfileService
|
||||
{
|
||||
private readonly IRestrictionRepository _repo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ReleaseProfileService(IRestrictionRepository repo, Logger logger)
|
||||
public ReleaseProfileService(IRestrictionRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> All()
|
||||
|
|
@ -55,7 +52,8 @@ public List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId)
|
|||
{
|
||||
return AllForTags(tagIds)
|
||||
.Where(r => r.Enabled)
|
||||
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
|
||||
.Where(r => r.IndexerIds.Contains(indexerId) || r.IndexerIds.Empty())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public ReleaseProfile Get(int id)
|
||||
|
|
|
|||
|
|
@ -15,57 +15,53 @@ namespace Sonarr.Api.V3.Profiles.Release
|
|||
[V3ApiController]
|
||||
public class ReleaseProfileController : RestController<ReleaseProfileResource>
|
||||
{
|
||||
private readonly IReleaseProfileService _profileService;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly ITagService _tagService;
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
|
||||
public ReleaseProfileController(IReleaseProfileService profileService, IIndexerFactory indexerFactory, ITagService tagService)
|
||||
public ReleaseProfileController(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory, ITagService tagService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_indexerFactory = indexerFactory;
|
||||
_tagService = tagService;
|
||||
_releaseProfileService = releaseProfileService;
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty())
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' or 'Must not contain' is required");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
||||
if (restriction.MapRequired().Any(t => t.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' should not contain whitespaces or an empty string");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' should not contain whitespaces or an empty string");
|
||||
}
|
||||
|
||||
if (restriction.MapIgnored().Any(t => t.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Ignored), "'Must not contain' should not contain whitespaces or an empty string");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Ignored), "'Must not contain' should not contain whitespaces or an empty string");
|
||||
}
|
||||
|
||||
if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId))
|
||||
if (restriction.Enabled && restriction.IndexerId != 0 && !indexerFactory.Exists(restriction.IndexerId))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.IndexerId), "Indexer does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
SharedValidator.RuleFor(d => d.Tags.Intersect(d.ExcludedTags))
|
||||
.Empty()
|
||||
.WithName("ExcludedTags")
|
||||
.WithMessage(d => $"'{string.Join(", ", _tagService.GetTags(d.Tags.Intersect(d.ExcludedTags)).Select(t => t.Label))}' cannot be in both 'Tags' and 'Excluded Tags'");
|
||||
.WithMessage(d => $"'{string.Join(", ", tagService.GetTags(d.Tags.Intersect(d.ExcludedTags)).Select(t => t.Label))}' cannot be in both 'Tags' and 'Excluded Tags'");
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
model = _releaseProfileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
_releaseProfileService.Delete(id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
|
|
@ -73,20 +69,20 @@ public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileReso
|
|||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
_releaseProfileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
protected override ReleaseProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
return _releaseProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<ReleaseProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
return _releaseProfileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
|||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? new List<string>(),
|
||||
Ignored = model.Ignored ?? new List<string>(),
|
||||
IndexerId = model.IndexerId,
|
||||
IndexerId = model.IndexerIds.FirstOrDefault(0),
|
||||
Tags = new HashSet<int>(model.Tags),
|
||||
ExcludedTags = new HashSet<int>(model.ExcludedTags)
|
||||
};
|
||||
|
|
@ -62,7 +62,7 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
|||
Enabled = resource.Enabled,
|
||||
Required = resource.MapRequired(),
|
||||
Ignored = resource.MapIgnored(),
|
||||
IndexerId = resource.IndexerId,
|
||||
IndexerIds = new List<int> { resource.IndexerId },
|
||||
Tags = new HashSet<int>(resource.Tags),
|
||||
ExcludedTags = new HashSet<int>(resource.ExcludedTags)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,57 +13,56 @@ namespace Sonarr.Api.V5.Profiles.Release;
|
|||
[V5ApiController]
|
||||
public class ReleaseProfileController : RestController<ReleaseProfileResource>
|
||||
{
|
||||
private readonly IReleaseProfileService _profileService;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly ITagService _tagService;
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
|
||||
public ReleaseProfileController(IReleaseProfileService profileService, IIndexerFactory indexerFactory, ITagService tagService)
|
||||
public ReleaseProfileController(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory, ITagService tagService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_indexerFactory = indexerFactory;
|
||||
_tagService = tagService;
|
||||
_releaseProfileService = releaseProfileService;
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Required.Empty() && restriction.Ignored.Empty())
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' or 'Must not contain' is required");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
||||
if (restriction.Required.Any(t => t.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Required), "'Must contain' should not contain whitespaces or an empty string");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' should not contain whitespaces or an empty string");
|
||||
}
|
||||
|
||||
if (restriction.Ignored.Any(t => t.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.Ignored), "'Must not contain' should not contain whitespaces or an empty string");
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Ignored), "'Must not contain' should not contain whitespaces or an empty string");
|
||||
}
|
||||
|
||||
if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId))
|
||||
if (restriction is { Enabled: true, IndexerIds.Count: > 0 })
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
|
||||
foreach (var indexerId in restriction.IndexerIds.Where(indexerId => !indexerFactory.Exists(indexerId)))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfileResource.IndexerIds), $"Indexer does not exist: {indexerId}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SharedValidator.RuleFor(d => d.Tags.Intersect(d.ExcludedTags))
|
||||
.Empty()
|
||||
.WithName("ExcludedTags")
|
||||
.WithMessage(d => $"'{string.Join(", ", _tagService.GetTags(d.Tags.Intersect(d.ExcludedTags)).Select(t => t.Label))}' cannot be in both 'Tags' and 'Excluded Tags'");
|
||||
.WithMessage(d => $"'{string.Join(", ", tagService.GetTags(d.Tags.Intersect(d.ExcludedTags)).Select(t => t.Label))}' cannot be in both 'Tags' and 'Excluded Tags'");
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
model = _releaseProfileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public ActionResult DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
_releaseProfileService.Delete(id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
|
@ -73,19 +72,19 @@ public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileReso
|
|||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
_releaseProfileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
protected override ReleaseProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
return _releaseProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<ReleaseProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
return _releaseProfileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class ReleaseProfileResource : RestResource
|
|||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; } = [];
|
||||
public List<string> Ignored { get; set; } = [];
|
||||
public int IndexerId { get; set; }
|
||||
public List<int> IndexerIds { get; set; } = [];
|
||||
public HashSet<int> Tags { get; set; } = [];
|
||||
public HashSet<int> ExcludedTags { get; set; } = [];
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
|||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? [],
|
||||
Ignored = model.Ignored ?? [],
|
||||
IndexerId = model.IndexerId,
|
||||
IndexerIds = model.IndexerIds ?? [],
|
||||
Tags = model.Tags ?? [],
|
||||
ExcludedTags = model.ExcludedTags ?? [],
|
||||
};
|
||||
|
|
@ -40,7 +40,7 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
|||
Enabled = resource.Enabled,
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
IndexerId = resource.IndexerId,
|
||||
IndexerIds = resource.IndexerIds,
|
||||
Tags = resource.Tags,
|
||||
ExcludedTags = resource.ExcludedTags
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue