Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tag sync support for Sonarr and Radarr #1332

Draft
wants to merge 15 commits into
base: develop
Choose a base branch
from
51 changes: 48 additions & 3 deletions src/NzbDrone.Core/Applications/Radarr/Radarr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Tags;

namespace NzbDrone.Core.Applications.Radarr
{
Expand All @@ -19,13 +20,21 @@ public class Radarr : ApplicationBase<RadarrSettings>
private readonly IRadarrV3Proxy _radarrV3Proxy;
private readonly ICached<List<RadarrIndexer>> _schemaCache;
private readonly IConfigFileProvider _configFileProvider;

public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
private readonly ITagService _tagService;

public Radarr(
ICacheManager cacheManager,
IRadarrV3Proxy radarrV3Proxy,
IConfigFileProvider configFileProvider,
IAppIndexerMapService appIndexerMapService,
Logger logger,
ITagService tagService)
: base(appIndexerMapService, logger)
{
_schemaCache = cacheManager.GetCache<List<RadarrIndexer>>(GetType());
_radarrV3Proxy = radarrV3Proxy;
_configFileProvider = configFileProvider;
_tagService = tagService;
}

public override ValidationResult Test()
Expand Down Expand Up @@ -192,7 +201,7 @@ private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, DownloadProt
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = new List<RadarrField>(),
Tags = new HashSet<int>()
Tags = Settings.SyncIndexerTags ? GetAndCreateApplicationTagIdsForIndexer(indexer) : GetExistingIndexerTags(id)
};

radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
Expand All @@ -211,5 +220,41 @@ private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, DownloadProt

return radarrIndexer;
}

private HashSet<int> GetAndCreateApplicationTagIdsForIndexer(IndexerDefinition indexer)
{
// Get Application Tag IDs
var applicationTags = _radarrV3Proxy.GetTagsFromApplication(Settings);

// Resolve Prowlarr indexer tags to labels
var indexerTagLabels = _tagService.GetTags(indexer.Tags).Select(t => t.Label);

// Determine tags from indexer which are present in application and those that need creating
var existingApplicationIndexerTags = applicationTags.Where(at => indexerTagLabels.Contains(at.Label)).ToList();
var missingTagLabels = indexerTagLabels.Except(existingApplicationIndexerTags.Select(pt => pt.Label));

// Create required new tags. If the tag already exists, it will be returned in the response so no worries about concurrency.
foreach (var tag in missingTagLabels)
{
_logger.Info("Tag '{0}' doesn't seem to exist in application so will be created.", tag);
var newTag = _radarrV3Proxy.CreateTag(Settings, tag);
existingApplicationIndexerTags.Add(newTag);
}

// Convert to int list for the indexer request
return existingApplicationIndexerTags.Select(pt => pt.Id).ToHashSet();
}

private HashSet<int> GetExistingIndexerTags(int indexerId)
{
if (indexerId == 0)
{
// Indexer doesn't exist yet so no tags to set.
return null;
}

var existingIndexer = _radarrV3Proxy.GetIndexer(indexerId, Settings);
return existingIndexer?.Tags;
}
}
}
4 changes: 3 additions & 1 deletion src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ public bool Equals(RadarrIndexer other)
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
var seedRatioCompare = seedRatio == otherSeedRatio;

var tagsCompare = (Tags ?? new HashSet<int>()).Equals(other.Tags ?? new HashSet<int>());

return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch &&
other.Name == Name &&
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && tagsCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}
3 changes: 3 additions & 0 deletions src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public RadarrSettings()
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> SyncCategories { get; set; }

[FieldDefinition(4, Label = "Sync Indexer Tags", Type = FieldType.Checkbox, HelpText = "Syncs the indexer tags within Prowlarr to Radarr.")]
public bool SyncIndexerTags { get; set; } = false;

public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
Expand Down
8 changes: 8 additions & 0 deletions src/NzbDrone.Core/Applications/Radarr/RadarrTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.Radarr
{
public class RadarrTag
{
public int Id { get; set; }
public string Label { get; set; }
}
}
17 changes: 17 additions & 0 deletions src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public interface IRadarrV3Proxy
void RemoveIndexer(int indexerId, RadarrSettings settings);
RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings);
ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings);
List<RadarrTag> GetTagsFromApplication(RadarrSettings settings);
RadarrTag CreateTag(RadarrSettings settings, string label);
}

public class RadarrV3Proxy : IRadarrV3Proxy
Expand Down Expand Up @@ -134,6 +136,21 @@ public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings se
return null;
}

public List<RadarrTag> GetTagsFromApplication(RadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/tag/detail", HttpMethod.Get);
return Execute<List<RadarrTag>>(request);
}

public RadarrTag CreateTag(RadarrSettings settings, string label)
{
var request = BuildRequest(settings, $"/api/v3/tag", HttpMethod.Post);
request.SetContent(new RadarrTag { Label = label }.ToJson());
var tag = Execute<RadarrTag>(request);
_logger.Info("Tag '{0}' created or already existed with ID '{1}'.", tag.Label, tag.Id);
return tag;
}

private HttpRequest BuildRequest(RadarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
Expand Down
51 changes: 48 additions & 3 deletions src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Tags;

namespace NzbDrone.Core.Applications.Sonarr
{
Expand All @@ -19,13 +20,21 @@ public class Sonarr : ApplicationBase<SonarrSettings>
private readonly ICached<List<SonarrIndexer>> _schemaCache;
private readonly ISonarrV3Proxy _sonarrV3Proxy;
private readonly IConfigFileProvider _configFileProvider;

public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
private readonly ITagService _tagService;

public Sonarr(
ICacheManager cacheManager,
ISonarrV3Proxy sonarrV3Proxy,
IConfigFileProvider configFileProvider,
IAppIndexerMapService appIndexerMapService,
Logger logger,
ITagService tagService)
: base(appIndexerMapService, logger)
{
_schemaCache = cacheManager.GetCache<List<SonarrIndexer>>(GetType());
_sonarrV3Proxy = sonarrV3Proxy;
_configFileProvider = configFileProvider;
_tagService = tagService;
}

public override ValidationResult Test()
Expand Down Expand Up @@ -194,7 +203,7 @@ private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, DownloadProt
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = new List<SonarrField>(),
Tags = new HashSet<int>()
Tags = Settings.SyncIndexerTags ? GetAndCreateApplicationTagIdsForIndexer(indexer) : GetExistingIndexerTags(id)
};

sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
Expand All @@ -215,5 +224,41 @@ private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, DownloadProt

return sonarrIndexer;
}

private HashSet<int> GetAndCreateApplicationTagIdsForIndexer(IndexerDefinition indexer)
{
// Get Application Tag IDs
var applicationTags = _sonarrV3Proxy.GetTagsFromApplication(Settings);

// Resolve Prowlarr indexer tags to labels
var indexerTagLabels = _tagService.GetTags(indexer.Tags).Select(t => t.Label);

// Determine tags from indexer which are present in application and those that need creating
var existingApplicationIndexerTags = applicationTags.Where(at => indexerTagLabels.Contains(at.Label)).ToList();
var missingTagLabels = indexerTagLabels.Except(existingApplicationIndexerTags.Select(pt => pt.Label));

// Create required new tags. If the tag already exists, it will be returned in the response so no worries about concurrency.
foreach (var tag in missingTagLabels)
{
_logger.Info("Tag '{0}' doesn't seem to exist in application so will be created.", tag);
var newTag = _sonarrV3Proxy.CreateTag(Settings, tag);
existingApplicationIndexerTags.Add(newTag);
}

// Convert to int list for the indexer request
return existingApplicationIndexerTags.Select(pt => pt.Id).ToHashSet();
}

private HashSet<int> GetExistingIndexerTags(int indexerId)
{
if (indexerId == 0)
{
// Indexer doesn't exist yet so no tags to set.
return null;
}

var existingIndexer = _sonarrV3Proxy.GetIndexer(indexerId, Settings);
return existingIndexer?.Tags;
}
}
}
4 changes: 3 additions & 1 deletion src/NzbDrone.Core/Applications/Sonarr/SonarrIndexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ public bool Equals(SonarrIndexer other)
var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
var seedRatioCompare = seedRatio == otherSeedRatio;

var tagsCompare = (Tags ?? new HashSet<int>()).Equals(other.Tags ?? new HashSet<int>());

return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch &&
other.Name == Name &&
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && animeCats && tagsCompare && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
}
}
}
3 changes: 3 additions & 0 deletions src/NzbDrone.Core/Applications/Sonarr/SonarrSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public SonarrSettings()
[FieldDefinition(4, Label = "Anime Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> AnimeSyncCategories { get; set; }

[FieldDefinition(5, Label = "Sync Indexer Tags", Type = FieldType.Checkbox, HelpText = "Syncs the indexer tags within Prowlarr to Sonarr.")]
public bool SyncIndexerTags { get; set; } = false;

public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
Expand Down
8 changes: 8 additions & 0 deletions src/NzbDrone.Core/Applications/Sonarr/SonarrTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.Sonarr
{
public class SonarrTag
{
public int Id { get; set; }
public string Label { get; set; }
}
}
17 changes: 17 additions & 0 deletions src/NzbDrone.Core/Applications/Sonarr/SonarrV3Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public interface ISonarrV3Proxy
void RemoveIndexer(int indexerId, SonarrSettings settings);
SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings);
ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings);
List<SonarrTag> GetTagsFromApplication(SonarrSettings settings);
SonarrTag CreateTag(SonarrSettings settings, string label);
}

public class SonarrV3Proxy : ISonarrV3Proxy
Expand Down Expand Up @@ -140,6 +142,21 @@ public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings se
return null;
}

public List<SonarrTag> GetTagsFromApplication(SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/tag/detail", HttpMethod.Get);
return Execute<List<SonarrTag>>(request);
}

public SonarrTag CreateTag(SonarrSettings settings, string label)
{
var request = BuildRequest(settings, $"/api/v3/tag", HttpMethod.Post);
request.SetContent(new SonarrTag { Label = label }.ToJson());
var tag = Execute<SonarrTag>(request);
_logger.Info("Tag '{0}' created or already existed with ID '{1}'.", tag.Label, tag.Id);
return tag;
}

private HttpRequest BuildRequest(SonarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
Expand Down