diff --git a/frontend/src/Settings/General/HostSettings.js b/frontend/src/Settings/General/HostSettings.js index 99255b0d5..1514f81fc 100644 --- a/frontend/src/Settings/General/HostSettings.js +++ b/frontend/src/Settings/General/HostSettings.js @@ -22,6 +22,7 @@ function HostSettings(props) { urlBase, instanceName, applicationUrl, + indexerNameTemplate, enableSsl, sslPort, sslCertPath, @@ -105,6 +106,25 @@ function HostSettings(props) { /> + + {translate('IndexerNameTemplate')} + + + + + { + private Mock _configService; + + [SetUp] + public void Setup() + { + _configService = Mocker.GetMock(); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void FormatIndexerName_should_return_empty_when_invalid_input(string indexerName) + { + _configService.Setup(s => s.IndexerNameTemplate).Returns("{name} ({instance})"); + + var result = Subject.FormatIndexerName(indexerName, "Prowlarr"); + + result.Should().Be(""); + } + + [Test] + public void FormatIndexerName_should_format_with_template() + { + _configService.Setup(s => s.IndexerNameTemplate).Returns("{name} ({instance})"); + + var result = Subject.FormatIndexerName("MyIndexer", "MyProwlarr"); + + result.Should().Be("MyIndexer (MyProwlarr)"); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void FormatIndexerName_should_use_fallback_instance_name(string instanceName) + { + _configService.Setup(s => s.IndexerNameTemplate).Returns("{name} ({instance})"); + + var result = Subject.FormatIndexerName("MyIndexer", instanceName); + + result.Should().Be($"MyIndexer ({DefaultInstanceName})"); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void FormatIndexerName_should_return_original_name_when_no_template(string template) + { + _configService.Setup(s => s.IndexerNameTemplate).Returns(template); + + var result = Subject.FormatIndexerName("MyIndexer", "MyProwlarr"); + + result.Should().Be("MyIndexer"); + } + + [Test] + public void FormatIndexerName_should_handle_custom_template() + { + _configService.Setup(s => s.IndexerNameTemplate).Returns("[{instance}] {name}"); + + var result = Subject.FormatIndexerName("MyIndexer", "MyProwlarr"); + + result.Should().Be("[MyProwlarr] MyIndexer"); + } + } +} diff --git a/src/NzbDrone.Core/Applications/IndexerNameTemplateService.cs b/src/NzbDrone.Core/Applications/IndexerNameTemplateService.cs new file mode 100644 index 000000000..204bb8f79 --- /dev/null +++ b/src/NzbDrone.Core/Applications/IndexerNameTemplateService.cs @@ -0,0 +1,46 @@ +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Applications +{ + public static class IndexerNameTemplateDefaults + { + public const string DefaultTemplate = "{name} ({instance})"; + public const string DefaultInstanceName = "Prowlarr"; + } + + public interface IIndexerNameTemplateService + { + string FormatIndexerName(string indexerName, string instanceName); + } + + public class IndexerNameTemplateService : IIndexerNameTemplateService + { + private readonly IConfigService _configService; + + public IndexerNameTemplateService(IConfigService configService) + { + _configService = configService; + } + + public string FormatIndexerName(string indexerName, string instanceName) + { + if (string.IsNullOrWhiteSpace(indexerName)) + { + return string.Empty; + } + + var template = _configService.IndexerNameTemplate; + + if (string.IsNullOrWhiteSpace(template)) + { + return indexerName; + } + + var finalInstanceName = !string.IsNullOrWhiteSpace(instanceName) ? instanceName : IndexerNameTemplateDefaults.DefaultInstanceName; + + return template + .Replace("{name}", indexerName) + .Replace("{instance}", finalInstanceName); + } + } +} diff --git a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs index 108972d6e..5838937b3 100644 --- a/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs +++ b/src/NzbDrone.Core/Applications/LazyLibrarian/LazyLibrarian.cs @@ -15,12 +15,14 @@ public class LazyLibrarian : ApplicationBase private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _lazyLibrarianV1Proxy = lazyLibrarianV1Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -155,8 +157,8 @@ private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer var lazyLibrarianIndexer = new LazyLibrarianIndexer { - Name = originalName ?? $"{indexer.Name} (Prowlarr)", - Altername = $"{indexer.Name} (Prowlarr)", + Name = originalName ?? _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), + Altername = originalName ?? _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), diff --git a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs index d305b48d2..d6b96d652 100644 --- a/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs +++ b/src/NzbDrone.Core/Applications/Lidarr/Lidarr.cs @@ -21,13 +21,15 @@ public class Lidarr : ApplicationBase private readonly ILidarrV1Proxy _lidarrV1Proxy; private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Lidarr(ICacheManager cacheManager, ILidarrV1Proxy lidarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _lidarrV1Proxy = lidarrV1Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -247,7 +249,7 @@ private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, IndexerCapab var lidarrIndexer = new LidarrIndexer { Id = id, - Name = $"{indexer.Name} (Prowlarr)", + Name = _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, diff --git a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs index e9fd9ffe7..5674d56de 100644 --- a/src/NzbDrone.Core/Applications/Mylar/Mylar.cs +++ b/src/NzbDrone.Core/Applications/Mylar/Mylar.cs @@ -15,12 +15,14 @@ public class Mylar : ApplicationBase private readonly IMylarV3Proxy _mylarV3Proxy; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Mylar(IMylarV3Proxy mylarV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _mylarV3Proxy = mylarV3Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -155,8 +157,8 @@ private MylarIndexer BuildMylarIndexer(IndexerDefinition indexer, IndexerCapabil var mylarIndexer = new MylarIndexer { - Name = originalName ?? $"{indexer.Name} (Prowlarr)", - Altername = $"{indexer.Name} (Prowlarr)", + Name = originalName ?? _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), + Altername = originalName ?? _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api", Apikey = _configFileProvider.ApiKey, Categories = string.Join(",", indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())), diff --git a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs index 6266f4ffa..90a85cf9a 100644 --- a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs +++ b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs @@ -21,13 +21,15 @@ public class Radarr : ApplicationBase private readonly IRadarrV3Proxy _radarrV3Proxy; private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Radarr(ICacheManager cacheManager, IRadarrV3Proxy radarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _radarrV3Proxy = radarrV3Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -245,7 +247,7 @@ private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, IndexerCapab var radarrIndexer = new RadarrIndexer { Id = id, - Name = $"{indexer.Name} (Prowlarr)", + Name = _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, diff --git a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs index efcc1eca0..480efbaf3 100644 --- a/src/NzbDrone.Core/Applications/Readarr/Readarr.cs +++ b/src/NzbDrone.Core/Applications/Readarr/Readarr.cs @@ -21,13 +21,15 @@ public class Readarr : ApplicationBase private readonly ICached> _schemaCache; private readonly IReadarrV1Proxy _readarrV1Proxy; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Readarr(ICacheManager cacheManager, IReadarrV1Proxy readarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _readarrV1Proxy = readarrV1Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -241,7 +243,7 @@ private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, IndexerCap var readarrIndexer = new ReadarrIndexer { Id = id, - Name = $"{indexer.Name} (Prowlarr)", + Name = _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, diff --git a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs index bf58b85c5..2460055e7 100644 --- a/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs +++ b/src/NzbDrone.Core/Applications/Sonarr/Sonarr.cs @@ -21,13 +21,15 @@ public class Sonarr : ApplicationBase private readonly ICached> _schemaCache; private readonly ISonarrV3Proxy _sonarrV3Proxy; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Sonarr(ICacheManager cacheManager, ISonarrV3Proxy sonarrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _sonarrV3Proxy = sonarrV3Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -253,7 +255,7 @@ private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, IndexerCapab var sonarrIndexer = new SonarrIndexer { Id = id, - Name = $"{indexer.Name} (Prowlarr)", + Name = _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, diff --git a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs index 0c149fc7c..1b47beac6 100644 --- a/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs +++ b/src/NzbDrone.Core/Applications/Whisparr/Whisparr.cs @@ -21,13 +21,15 @@ public class Whisparr : ApplicationBase private readonly IWhisparrV3Proxy _whisparrV3Proxy; private readonly ICached> _schemaCache; private readonly IConfigFileProvider _configFileProvider; + private readonly IIndexerNameTemplateService _indexerNameTemplateService; - public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger) + public Whisparr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, IIndexerNameTemplateService indexerNameTemplateService, Logger logger) : base(appIndexerMapService, indexerFactory, logger) { _schemaCache = cacheManager.GetCache>(GetType()); _whisparrV3Proxy = whisparrV3Proxy; _configFileProvider = configFileProvider; + _indexerNameTemplateService = indexerNameTemplateService; } public override ValidationResult Test() @@ -232,7 +234,7 @@ private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, IndexerC var whisparrIndexer = new WhisparrIndexer { Id = id, - Name = $"{indexer.Name} (Prowlarr)", + Name = _indexerNameTemplateService.FormatIndexerName(indexer.Name, _configFileProvider.InstanceName), EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 27a953823..bc90d3640 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -88,6 +88,12 @@ public bool LogIndexerResponse set { SetValue("LogIndexerResponse", value); } } + public string IndexerNameTemplate + { + get { return GetValue("indexernametemplate", Applications.IndexerNameTemplateDefaults.DefaultTemplate); } + set { SetValue("indexernametemplate", value); } + } + public int FirstDayOfWeek { get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 5fa2ed005..c986c6944 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -53,6 +53,7 @@ public interface IConfigService // Indexers bool LogIndexerResponse { get; set; } + string IndexerNameTemplate { get; set; } CertificateValidationType CertificateValidation { get; } string ApplicationUrl { get; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a789c6046..70d1905a6 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -384,6 +384,9 @@ "IndexerMTeamTpSettingsApiKeyHelpText": "API Key from the Site (Found in User Control Panel => Security => Laboratory)", "IndexerMTeamTpSettingsFreeleechOnlyHelpText": "Search freeleech releases only", "IndexerName": "Indexer Name", + "IndexerNameTemplate": "Indexer Name Template", + "IndexerNameTemplateHelpText": "Template to customize how indexer names appear in external applications. Use {name} for the indexer name and {instance} for the instance name. Default: {name} ({instance})", + "IndexerNameTemplateHelpTextWarning": "Template does not contain {name} - indexer names may not be distinguishable", "IndexerNebulanceSettingsApiKeyHelpText": "API Key from User Settings > Api Keys. Key must have List and Download permissions", "IndexerNewznabSettingsAdditionalParametersHelpText": "Additional Newznab parameters", "IndexerNewznabSettingsApiKeyHelpText": "Site API Key", diff --git a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs index 4bd0cd10a..062d6b6d5 100644 --- a/src/Prowlarr.Api.V1/Config/HostConfigResource.cs +++ b/src/Prowlarr.Api.V1/Config/HostConfigResource.cs @@ -47,6 +47,7 @@ public class HostConfigResource : RestResource public int BackupRetention { get; set; } public int HistoryCleanupDays { get; set; } public bool TrustCgnatIpAddresses { get; set; } + public string IndexerNameTemplate { get; set; } } public static class HostConfigResourceMapper @@ -92,7 +93,8 @@ public static HostConfigResource ToResource(this IConfigFileProvider model, ICon BackupInterval = configService.BackupInterval, BackupRetention = configService.BackupRetention, ApplicationUrl = configService.ApplicationUrl, - HistoryCleanupDays = configService.HistoryCleanupDays + HistoryCleanupDays = configService.HistoryCleanupDays, + IndexerNameTemplate = configService.IndexerNameTemplate }; } }