Add syncing with external tools

This commit is contained in:
TheOtherP 2025-09-18 09:47:54 +02:00
parent cc6fd534b4
commit 717e2f12ab
13 changed files with 1826 additions and 6 deletions

View File

@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Read(//c/Users/strat/IdeaProjects/nzbhydra2/**)",
"Bash(mvn:*)"
],
"deny": [],
"ask": []
}
}

View File

@ -5,12 +5,11 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.nzbhydra.GenericResponse;
import org.nzbhydra.config.FileSystemBrowser.DirectoryListingRequest;
import org.nzbhydra.config.FileSystemBrowser.FileSystemEntry;
import org.nzbhydra.config.indexer.IndexerConfig;
import org.nzbhydra.config.safeconfig.SafeConfig;
import org.nzbhydra.config.validation.BaseConfigValidator;
import org.nzbhydra.config.validation.ConfigValidationResult;
import org.nzbhydra.externaltools.ExternalToolsSyncService;
import org.nzbhydra.indexers.IndexerEntity;
import org.nzbhydra.indexers.IndexerRepository;
import org.nzbhydra.springnative.ReflectionMarker;
@ -54,6 +53,8 @@ public class ConfigWeb {
private BaseConfigValidator baseConfigValidator;
@Autowired
private BaseConfigHandler baseConfigHandler;
@Autowired
private ExternalToolsSyncService externalToolsSyncService;
private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter();
@Secured({"ROLE_ADMIN"})
@ -80,14 +81,33 @@ public class ConfigWeb {
}
logger.info("Received new config");
newConfig = baseConfigValidator.prepareForSaving(configProvider.getBaseConfig(), newConfig);
ConfigValidationResult result = baseConfigValidator.validateConfig(configProvider.getBaseConfig(), newConfig, newConfig);
BaseConfig oldConfig = configProvider.getBaseConfig();
newConfig = baseConfigValidator.prepareForSaving(oldConfig, newConfig);
ConfigValidationResult result = baseConfigValidator.validateConfig(oldConfig, newConfig, newConfig);
if (result.isOk()) {
handleRenamedIndexers(newConfig);
// Detect which indexers changed before replacing config
Set<String> changedIndexers = externalToolsSyncService.detectChangedIndexers(
oldConfig.getIndexers(),
newConfig.getIndexers()
);
baseConfigHandler.replace(newConfig);
baseConfigHandler.save(true);
result.setNewConfig(configProvider.getBaseConfig());
// Sync to external tools if enabled and indexers changed
if (configProvider.getBaseConfig().getExternalTools().isSyncOnConfigChange() && !changedIndexers.isEmpty()) {
logger.info("Indexers changed, syncing to external tools");
try {
ExternalToolsSyncService.SyncResult syncResult = externalToolsSyncService.syncTools(changedIndexers);
logger.info("External tools sync completed: {} successful, {} failed",
syncResult.getSuccessCount(), syncResult.getFailureCount());
} catch (Exception e) {
logger.error("Error syncing to external tools", e);
}
}
}
return result;
}
@ -141,7 +161,7 @@ public class ConfigWeb {
@Secured({"ROLE_USER"})
@RequestMapping(value = "/internalapi/config/folderlisting", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public FileSystemEntry getDirectoryListing(@RequestBody DirectoryListingRequest request) {
public FileSystemBrowser.FileSystemEntry getDirectoryListing(@RequestBody FileSystemBrowser.DirectoryListingRequest request) {
return fileSystemBrowser.getDirectoryListing(request);
}

View File

@ -0,0 +1,272 @@
/*
* (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nzbhydra.externaltools;
import org.nzbhydra.config.ConfigProvider;
import org.nzbhydra.config.ExternalToolConfig;
import org.nzbhydra.config.ExternalToolsConfig;
import org.nzbhydra.config.indexer.IndexerConfig;
import org.nzbhydra.notifications.NotificationEntity;
import org.nzbhydra.notifications.NotificationMessageType;
import org.nzbhydra.notifications.NotificationRepository;
import org.nzbhydra.web.UrlCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Component
public class ExternalToolsSyncService {
private static final Logger logger = LoggerFactory.getLogger(ExternalToolsSyncService.class);
@Autowired
private ExternalTools externalTools;
@Autowired
private ConfigProvider configProvider;
@Autowired
private UrlCalculator urlCalculator;
@Autowired
private NotificationRepository notificationRepository;
/**
* Sync all indexers to all configured external tools
*
* @return Summary of sync results
*/
public SyncResult syncAllTools() {
return syncTools(null);
}
/**
* Sync specific indexers to all configured external tools
*
* @param changedIndexers Set of indexer names that were changed, or null to sync all
* @return Summary of sync results
*/
public SyncResult syncTools(Set<String> changedIndexers) {
ExternalToolsConfig config = configProvider.getBaseConfig().getExternalTools();
if (!config.isSyncOnConfigChange()) {
logger.debug("External tools sync is disabled");
return new SyncResult(0, 0, new ArrayList<>());
}
List<ExternalToolConfig> enabledTools = config.getExternalTools().stream()
.filter(ExternalToolConfig::isEnabled)
.toList();
if (enabledTools.isEmpty()) {
logger.debug("No enabled external tools configured");
return new SyncResult(0, 0, new ArrayList<>());
}
logger.info("Starting sync to {} external tools", enabledTools.size());
int successCount = 0;
int failureCount = 0;
List<String> messages = new ArrayList<>();
for (ExternalToolConfig tool : enabledTools) {
try {
boolean success = syncToTool(tool, changedIndexers);
if (success) {
successCount++;
messages.add("Successfully synced to " + tool.getName());
} else {
failureCount++;
messages.addAll(externalTools.getMessages());
}
} catch (Exception e) {
failureCount++;
logger.error("Failed to sync to {}: {}", tool.getName(), e.getMessage(), e);
messages.add("Failed to sync to " + tool.getName() + ": " + e.getMessage());
}
}
// Create notification
createNotification(successCount, failureCount, messages);
return new SyncResult(successCount, failureCount, messages);
}
private boolean syncToTool(ExternalToolConfig tool, Set<String> changedIndexers) throws IOException {
logger.info("Syncing to {} ({})", tool.getName(), tool.getType());
// Convert tool config to AddRequest
AddRequest request = buildAddRequest(tool, changedIndexers);
// Use existing ExternalTools logic
boolean success = externalTools.addNzbhydraAsIndexer(request);
if (success) {
logger.info("Successfully synced to {}", tool.getName());
} else {
logger.warn("Failed to sync to {}", tool.getName());
}
return success;
}
private AddRequest buildAddRequest(ExternalToolConfig tool, Set<String> changedIndexers) {
AddRequest request = new AddRequest();
// Map tool type to ExternalTool enum
AddRequest.ExternalTool externalToolType = switch (tool.getType()) {
case SONARR -> AddRequest.ExternalTool.Sonarr;
case RADARR -> AddRequest.ExternalTool.Radarr;
case LIDARR -> AddRequest.ExternalTool.Lidarr;
case READARR -> AddRequest.ExternalTool.Readarr;
};
request.setExternalTool(externalToolType);
// Set connection details
request.setXdarrHost(tool.getHost());
request.setXdarrApiKey(tool.getApiKey());
request.setNzbhydraHost(urlCalculator.getRequestBasedUriBuilder().build().toUriString());
request.setNzbhydraName(tool.getNzbhydraName());
// Set sync type
AddRequest.AddType addType = tool.getSyncType() == ExternalToolConfig.SyncType.SINGLE
? AddRequest.AddType.SINGLE
: AddRequest.AddType.PER_INDEXER;
request.setAddType(addType);
// If we're in PER_INDEXER mode and have changed indexers, only sync those
if (addType == AddRequest.AddType.PER_INDEXER && changedIndexers != null && !changedIndexers.isEmpty()) {
// This will be handled by the ExternalTools class by filtering indexers
request.setAdditionalParameters("indexers=" + String.join(",", changedIndexers));
}
// Set configuration options
request.setConfigureForUsenet(tool.isConfigureForUsenet());
request.setConfigureForTorrents(tool.isConfigureForTorrents());
request.setAddDisabledIndexers(tool.isAddDisabledIndexers());
request.setUseHydraPriorities(tool.isUseHydraPriorities());
request.setPriority(tool.getPriority());
// Set RSS and search settings
request.setEnableRss(tool.isEnableRss());
request.setEnableAutomaticSearch(tool.isEnableAutomaticSearch());
request.setEnableInteractiveSearch(tool.isEnableInteractiveSearch());
// Set categories
request.setCategories(tool.getCategories());
request.setAnimeCategories(tool.getAnimeCategories());
// Set additional settings
request.setAdditionalParameters(tool.getAdditionalParameters());
request.setMinimumSeeders(tool.getMinimumSeeders());
request.setSeedRatio(tool.getSeedRatio());
request.setSeedTime(tool.getSeedTime());
request.setSeasonPackSeedTime(tool.getSeasonPackSeedTime());
request.setDiscographySeedTime(tool.getDiscographySeedTime());
request.setEarlyDownloadLimit(tool.getEarlyDownloadLimit());
request.setRemoveYearFromSearchString(tool.isRemoveYearFromSearchString());
return request;
}
private void createNotification(int successCount, int failureCount, List<String> messages) {
String title = "External Tools Sync";
String body;
if (failureCount == 0 && successCount > 0) {
body = String.format("Successfully synced to %d external tool(s)", successCount);
} else if (successCount == 0 && failureCount > 0) {
body = String.format("Failed to sync to %d external tool(s). Check logs for details.", failureCount);
} else {
body = String.format("Synced to %d tool(s), %d failed. Check logs for details.", successCount, failureCount);
}
NotificationEntity notification = new NotificationEntity();
notification.setTime(Instant.now());
notification.setTitle(title);
notification.setBody(body);
notification.setUrls(null);
notification.setDisplayed(false);
notification.setMessageType(failureCount > 0
? NotificationMessageType.FAILURE
: NotificationMessageType.SUCCESS);
notificationRepository.save(notification);
}
public static class SyncResult {
private final int successCount;
private final int failureCount;
private final List<String> messages;
public SyncResult(int successCount, int failureCount, List<String> messages) {
this.successCount = successCount;
this.failureCount = failureCount;
this.messages = messages;
}
public int getSuccessCount() {
return successCount;
}
public int getFailureCount() {
return failureCount;
}
public List<String> getMessages() {
return messages;
}
}
/**
* Detect which indexers have changed between old and new config
*/
public Set<String> detectChangedIndexers(List<IndexerConfig> oldIndexers, List<IndexerConfig> newIndexers) {
Set<String> changed = new HashSet<>();
// Check for added or modified indexers
for (IndexerConfig newIndexer : newIndexers) {
IndexerConfig oldIndexer = oldIndexers.stream()
.filter(i -> i.getName().equals(newIndexer.getName()))
.findFirst()
.orElse(null);
if (oldIndexer == null) {
// New indexer
changed.add(newIndexer.getName());
} else if (!oldIndexer.equals(newIndexer)) {
// Modified indexer
changed.add(newIndexer.getName());
}
}
// Check for removed indexers
for (IndexerConfig oldIndexer : oldIndexers) {
boolean exists = newIndexers.stream()
.anyMatch(i -> i.getName().equals(oldIndexer.getName()));
if (!exists) {
changed.add(oldIndexer.getName());
}
}
return changed;
}
}

View File

@ -33,6 +33,8 @@ public class ExternalToolsWeb {
@Autowired
private IndexerRepository indexerRepository;
private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter();
@Autowired
private ExternalToolsSyncService externalToolsSyncService;
@Secured({"ROLE_ADMIN"})
@RequestMapping(value = "/internalapi/externalTools/getDialogInfo", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ -56,5 +58,52 @@ public class ExternalToolsWeb {
return externalTools.getMessages();
}
@Secured({"ROLE_ADMIN"})
@RequestMapping(value = "/internalapi/externalTools/syncAll", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ExternalToolsSyncService.SyncResult syncAllTools() {
return externalToolsSyncService.syncAllTools();
}
@Secured({"ROLE_ADMIN"})
@RequestMapping(value = "/internalapi/externalTools/testConnection", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ConnectionTestResult testConnection(@RequestBody AddRequest addRequest) {
try {
// Just test the connection without actually syncing
boolean success = externalTools.addNzbhydraAsIndexer(buildTestRequest(addRequest));
return new ConnectionTestResult(success, success ? "Connection successful" : "Connection failed");
} catch (Exception e) {
return new ConnectionTestResult(false, "Error: " + e.getMessage());
}
}
private AddRequest buildTestRequest(AddRequest request) {
// Create a test request that only checks connection without modifying anything
AddRequest testRequest = new AddRequest();
testRequest.setExternalTool(request.getExternalTool());
testRequest.setXdarrHost(request.getXdarrHost());
testRequest.setXdarrApiKey(request.getXdarrApiKey());
testRequest.setAddType(AddRequest.AddType.DELETE_ONLY); // Only delete, don't add
testRequest.setNzbhydraName("TEST_CONNECTION_" + System.currentTimeMillis());
return testRequest;
}
public static class ConnectionTestResult {
private final boolean successful;
private final String message;
public ConnectionTestResult(boolean successful, String message) {
this.successful = successful;
this.message = message;
}
public boolean isSuccessful() {
return successful;
}
public String getMessage() {
return message;
}
}
}

View File

@ -0,0 +1,19 @@
<div class="modal-header">
<h3 class="modal-title">External Tool Configuration</h3>
</div>
<div class="modal-body">
<form class="form-horizontal config-form" name="form">
<formly-form model="model" fields="fields" options="options" form="form">
</formly-form>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-warning" type="button" ng-click="testConnection()" ng-show="needsConnectionTest || isInitial">
Test connection
</button>
<button class="btn btn-primary" type="button" ng-click="obSubmit()">OK</button>
<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>
<button class="btn btn-danger" type="button" ng-click="deleteEntry()" ng-if="!isInitial">Delete</button>
</div>

View File

@ -0,0 +1,62 @@
<div class="form-horizontal config-section">
<div class="form-group">
<label class="control-label col-md-2">External tools</label>
<div class="col-md-10">
<p>Configure external tools (Sonarr, Radarr, Lidarr, Readarr) for automatic indexer synchronization.</p>
<p>When indexers are added, removed or modified in NZBHydra, they will be automatically synced to all configured external tools.</p>
</div>
</div>
<div class="form-group" ng-if="!model.externalTools || model.externalTools.length === 0">
<label class="control-label col-md-2"></label>
<div class="col-md-10">
<p><i>No external tools configured</i></p>
</div>
</div>
<div ng-repeat="entry in model.externalTools" class="form-group">
<label class="control-label col-md-2"></label>
<div class="col-md-8">
<div class="btn btn-default" ng-click="showBox(entry, model.externalTools)" style="width: 100%; text-align: left">
<span ng-class="entry.enabled ? 'glyphicon-ok' : 'glyphicon-remove'"
class="glyphicon"
style="color: gray"
ng-style="{'color': entry.enabled ? 'green' : 'red'}"></span>
<span style="margin-left: 10px">{{ entry.name }} ({{ entry.type }})</span>
<span class="pull-right">{{ entry.host }}</span>
</div>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-danger" ng-click="model.externalTools.splice($index, 1); form.$setDirty(true)">
<span class="glyphicon glyphicon-remove"></span>
</button>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2"></label>
<div class="col-md-10">
<div class="btn-group" uib-dropdown dropdown-append-to-body>
<button type="button" class="btn btn-default" uib-dropdown-toggle>
Add external tool <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu">
<li role="menuitem" ng-repeat="preset in presets">
<a href="" ng-click="addEntry(model.externalTools, preset)">{{ preset.name }}</a>
</li>
<li class="divider"></li>
<li role="menuitem">
<a href="" ng-click="addEntry(model.externalTools)">Custom</a>
</li>
</ul>
</div>
<button type="button" class="btn btn-info" ng-click="syncAll()" style="margin-left: 10px">
<span class="glyphicon glyphicon-refresh"></span> Sync All Now
</button>
</div>
</div>
</div>

View File

@ -4978,6 +4978,608 @@ function IndexerCheckBeforeCloseService($q, ModalService, IndexerConfigBoxServic
}
}
/*
* (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
angular
.module('nzbhydraApp')
.config(["formlyConfigProvider", function config(formlyConfigProvider) {
formlyConfigProvider.setType({
name: 'externalToolConfig',
templateUrl: 'static/html/config/external-tool-config.html',
controller: function ($scope, $uibModal, growl, localStorageService, $http) {
$scope.formOptions = {formState: $scope.formState};
$scope._showBox = _showBox;
$scope.showBox = showBox;
$scope.isInitial = false;
$scope.presets = [
{
name: "Sonarr",
type: "SONARR",
host: "http://localhost:8989",
categories: "5030,5040",
syncType: "PER_INDEXER"
},
{
name: "Radarr",
type: "RADARR",
host: "http://localhost:7878",
categories: "2000",
syncType: "PER_INDEXER"
},
{
name: "Lidarr",
type: "LIDARR",
host: "http://localhost:8686",
categories: "3000",
syncType: "PER_INDEXER"
},
{
name: "Readarr",
type: "READARR",
host: "http://localhost:8787",
categories: "7020,8010",
syncType: "PER_INDEXER"
}
];
function _showBox(model, parentModel, isInitial, callback) {
var modalInstance = $uibModal.open({
templateUrl: 'static/html/config/external-tool-config-box.html',
controller: 'ExternalToolConfigBoxInstanceController',
size: 'lg',
resolve: {
model: function () {
model.showAdvanced = localStorageService.get("showAdvanced");
return model;
},
fields: function () {
return getExternalToolBoxFields(model, parentModel, isInitial);
},
isInitial: function () {
return isInitial
},
parentModel: function () {
return parentModel;
},
data: function () {
return $scope.options.data;
}
}
});
modalInstance.result.then(function (returnedModel) {
$scope.form.$setDirty(true);
if (angular.isDefined(callback)) {
callback(true, returnedModel);
}
}, function () {
if (angular.isDefined(callback)) {
callback(false);
}
});
}
function showBox(model, parentModel) {
$scope._showBox(model, parentModel, false)
}
$scope.syncAll = function () {
growl.info("Starting sync to all external tools...");
$http.post('internalapi/externalTools/syncAll').then(
function (response) {
var result = response.data;
if (result.failureCount === 0) {
growl.success("Successfully synced to " + result.successCount + " external tool(s)");
} else if (result.successCount === 0) {
growl.error("Failed to sync to all " + result.failureCount + " external tool(s)");
} else {
growl.warning("Synced to " + result.successCount + " tool(s), " + result.failureCount + " failed");
}
},
function (error) {
growl.error("Error syncing to external tools: " + error.data);
}
);
};
$scope.addEntry = function (entriesCollection, preset) {
var model = angular.copy({
enabled: true,
syncType: "PER_INDEXER",
configureForUsenet: true,
configureForTorrents: false,
enableRss: true,
enableAutomaticSearch: true,
enableInteractiveSearch: true,
useHydraPriorities: true,
priority: 25,
nzbhydraName: "NZBHydra2"
});
if (angular.isDefined(preset)) {
_.extend(model, preset);
}
$scope.isInitial = true;
$scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {
if (isSubmitted) {
entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);
}
});
};
function getExternalToolBoxFields(model, parentModel, isInitial) {
var fieldset = [];
fieldset.push({
key: 'enabled',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enabled'
}
});
fieldset.push({
key: 'name',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Name',
required: true,
help: 'Unique name for this external tool instance'
},
validators: {
uniqueName: {
expression: function (viewValue) {
if (isInitial || viewValue !== model.name) {
return _.pluck(parentModel, "name").indexOf(viewValue) === -1;
}
return true;
},
message: '"External tool \\"" + $viewValue + "\\" already exists"'
}
}
});
fieldset.push({
key: 'type',
type: 'horizontalSelect',
templateOptions: {
type: 'select',
label: 'Type',
required: true,
options: [
{name: 'Sonarr', value: 'SONARR'},
{name: 'Radarr', value: 'RADARR'},
{name: 'Lidarr', value: 'LIDARR'},
{name: 'Readarr', value: 'READARR'}
]
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
// Update default categories based on type
switch (newValue) {
case 'SONARR':
model.categories = "5030,5040";
break;
case 'RADARR':
model.categories = "2000";
break;
case 'LIDARR':
model.categories = "3000";
break;
case 'READARR':
model.categories = "7020,8010";
break;
}
}
}
}
});
fieldset.push({
key: 'host',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Host URL',
help: 'URL with scheme and port (e.g., http://localhost:8989)',
required: true
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
scope.$parent.needsConnectionTest = true;
}
}
}
});
fieldset.push({
key: 'apiKey',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'API Key',
help: 'API key for the external tool'
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
scope.$parent.needsConnectionTest = true;
}
}
}
});
fieldset.push({
key: 'syncType',
type: 'horizontalSelect',
templateOptions: {
type: 'select',
label: 'Sync Type',
options: [
{name: 'Single entry for all indexers', value: 'SINGLE'},
{name: 'Separate entry per indexer', value: 'PER_INDEXER'}
],
help: 'Whether to create one entry for all indexers or separate entries'
}
});
fieldset.push({
key: 'nzbhydraName',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'NZBHydra Name',
help: 'Name prefix used in the external tool',
required: true
}
});
fieldset.push({
key: 'configureForUsenet',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Configure for Usenet',
help: 'Sync Usenet indexers'
}
});
fieldset.push({
key: 'configureForTorrents',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Configure for Torrents',
help: 'Sync torrent indexers'
}
});
fieldset.push({
key: 'addDisabledIndexers',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Add disabled indexers',
help: 'Also sync indexers that are disabled in NZBHydra',
advanced: true
}
});
fieldset.push({
key: 'useHydraPriorities',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Use Hydra priorities',
help: 'Map NZBHydra indexer priorities to the external tool'
}
});
fieldset.push({
key: 'priority',
type: 'horizontalInput',
hideExpression: 'model.useHydraPriorities && model.syncType === "PER_INDEXER"',
templateOptions: {
type: 'number',
label: 'Default Priority',
help: 'Priority to use when not using Hydra priorities (1-50, lower is better)',
placeholder: '25'
}
});
fieldset.push({
key: 'enableRss',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable RSS',
help: 'Enable RSS sync in the external tool'
}
});
fieldset.push({
key: 'enableAutomaticSearch',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable automatic search',
help: 'Enable automatic search in the external tool'
}
});
fieldset.push({
key: 'enableInteractiveSearch',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable interactive search',
help: 'Enable interactive (manual) search in the external tool'
}
});
fieldset.push({
key: 'categories',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Categories',
help: 'Comma-separated newznab category IDs',
advanced: true
}
});
if (model.type === 'SONARR') {
fieldset.push({
key: 'animeCategories',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Anime categories',
help: 'Comma-separated newznab category IDs for anime',
advanced: true
}
});
}
if (model.type === 'RADARR') {
fieldset.push({
key: 'removeYearFromSearchString',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Remove year from search',
help: 'Remove year from movie search queries',
advanced: true
}
});
}
if (model.type === 'LIDARR' || model.type === 'READARR') {
fieldset.push({
key: 'earlyDownloadLimit',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Early download limit',
advanced: true
}
});
}
fieldset.push({
key: 'additionalParameters',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Additional parameters',
help: 'Additional URL parameters to send to the indexer',
advanced: true
}
});
// Torrent-specific fields
if (model.configureForTorrents) {
fieldset.push({
key: 'minimumSeeders',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Minimum seeders',
help: 'Minimum number of seeders',
advanced: true
}
});
fieldset.push({
key: 'seedRatio',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Seed ratio',
advanced: true
}
});
fieldset.push({
key: 'seedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Seed time',
advanced: true
}
});
if (model.type === 'SONARR') {
fieldset.push({
key: 'seasonPackSeedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Season pack seed time',
advanced: true
}
});
}
if (model.type === 'LIDARR' || model.type === 'READARR') {
fieldset.push({
key: 'discographySeedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Discography seed time',
advanced: true
}
});
}
}
return fieldset;
}
}
});
}]);
angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceController', ["$scope", "$q", "$uibModalInstance", "$http", "model", "fields", "isInitial", "parentModel", "data", "growl", "blockUI", function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl, blockUI) {
$scope.model = model;
$scope.fields = fields;
$scope.isInitial = isInitial;
$scope.spinnerActive = false;
$scope.needsConnectionTest = false;
$scope.obSubmit = function () {
if ($scope.form.$valid) {
checkConnection().then(function () {
$uibModalInstance.close($scope.model);
});
} else {
growl.error("Config invalid. Please check your settings.");
angular.forEach($scope.form.$error, function (error) {
angular.forEach(error, function (field) {
field.$setTouched();
});
});
}
};
$scope.cancel = function () {
$uibModalInstance.dismiss();
};
$scope.deleteEntry = function () {
parentModel.splice(parentModel.indexOf(model), 1);
$uibModalInstance.close($scope);
};
$scope.reset = function () {
if (angular.isDefined(data.resetFunction)) {
$scope.options.resetModel();
$scope.options.resetModel();
}
};
$scope.testConnection = function () {
$scope.spinnerActive = true;
blockUI.start("Testing connection...");
var testRequest = {
externalTool: model.type === 'SONARR' ? 'Sonarr' :
model.type === 'RADARR' ? 'Radarr' :
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
xdarrHost: model.host,
xdarrApiKey: model.apiKey,
addType: 'DELETE_ONLY' // Just test connection
};
$http.post("internalapi/externalTools/testConnection", testRequest).then(
function (response) {
blockUI.reset();
$scope.spinnerActive = false;
if (response.data.successful) {
growl.info("Connection test successful");
} else {
growl.error("Connection test failed: " + response.data.message);
}
},
function (error) {
blockUI.reset();
$scope.spinnerActive = false;
growl.error("Connection test failed: " + (error.data ? error.data.message : "Unknown error"));
}
);
};
function checkConnection() {
var deferred = $q.defer();
if (!$scope.isInitial && !$scope.needsConnectionTest) {
deferred.resolve();
} else {
$scope.spinnerActive = true;
blockUI.start("Testing connection...");
var testRequest = {
externalTool: model.type === 'SONARR' ? 'Sonarr' :
model.type === 'RADARR' ? 'Radarr' :
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
xdarrHost: model.host,
xdarrApiKey: model.apiKey,
addType: 'DELETE_ONLY'
};
$http.post("internalapi/externalTools/testConnection", testRequest).then(
function (response) {
blockUI.reset();
$scope.spinnerActive = false;
if (response.data.successful) {
growl.info("Connection test successful");
deferred.resolve();
} else {
growl.error("Connection test failed: " + response.data.message);
deferred.reject();
}
},
function (error) {
blockUI.reset();
$scope.spinnerActive = false;
growl.error("Connection test failed: " + (error.data ? error.data.message : "Unknown error"));
deferred.reject();
}
);
}
return deferred.promise;
}
$scope.$on("modal.closing", function (targetScope, reason) {
if (reason === "backdrop click") {
$scope.reset($scope);
}
});
}]);
/*
* (C) Copyright 2017 TheOtherP (theotherp@posteo.net)
*
@ -8224,6 +8826,28 @@ function ConfigFields($injector) {
}
],
externalTools: [
{
wrapper: 'fieldset',
templateOptions: {label: 'External Tool Sync Settings'},
fieldGroup: [
{
key: 'syncOnConfigChange',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Sync on config change',
help: 'Automatically sync indexers to external tools when configuration is saved'
}
}
]
},
{
type: 'externalToolConfig',
data: {}
}
],
indexers: [
{
type: "indexers",

File diff suppressed because one or more lines are too long

View File

@ -1931,6 +1931,28 @@ function ConfigFields($injector) {
}
],
externalTools: [
{
wrapper: 'fieldset',
templateOptions: {label: 'External Tool Sync Settings'},
fieldGroup: [
{
key: 'syncOnConfigChange',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Sync on config change',
help: 'Automatically sync indexers to external tools when configuration is saved'
}
}
]
},
{
type: 'externalToolConfig',
data: {}
}
],
indexers: [
{
type: "indexers",

View File

@ -0,0 +1,602 @@
/*
* (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
angular
.module('nzbhydraApp')
.config(function config(formlyConfigProvider) {
formlyConfigProvider.setType({
name: 'externalToolConfig',
templateUrl: 'static/html/config/external-tool-config.html',
controller: function ($scope, $uibModal, growl, localStorageService, $http) {
$scope.formOptions = {formState: $scope.formState};
$scope._showBox = _showBox;
$scope.showBox = showBox;
$scope.isInitial = false;
$scope.presets = [
{
name: "Sonarr",
type: "SONARR",
host: "http://localhost:8989",
categories: "5030,5040",
syncType: "PER_INDEXER"
},
{
name: "Radarr",
type: "RADARR",
host: "http://localhost:7878",
categories: "2000",
syncType: "PER_INDEXER"
},
{
name: "Lidarr",
type: "LIDARR",
host: "http://localhost:8686",
categories: "3000",
syncType: "PER_INDEXER"
},
{
name: "Readarr",
type: "READARR",
host: "http://localhost:8787",
categories: "7020,8010",
syncType: "PER_INDEXER"
}
];
function _showBox(model, parentModel, isInitial, callback) {
var modalInstance = $uibModal.open({
templateUrl: 'static/html/config/external-tool-config-box.html',
controller: 'ExternalToolConfigBoxInstanceController',
size: 'lg',
resolve: {
model: function () {
model.showAdvanced = localStorageService.get("showAdvanced");
return model;
},
fields: function () {
return getExternalToolBoxFields(model, parentModel, isInitial);
},
isInitial: function () {
return isInitial
},
parentModel: function () {
return parentModel;
},
data: function () {
return $scope.options.data;
}
}
});
modalInstance.result.then(function (returnedModel) {
$scope.form.$setDirty(true);
if (angular.isDefined(callback)) {
callback(true, returnedModel);
}
}, function () {
if (angular.isDefined(callback)) {
callback(false);
}
});
}
function showBox(model, parentModel) {
$scope._showBox(model, parentModel, false)
}
$scope.syncAll = function () {
growl.info("Starting sync to all external tools...");
$http.post('internalapi/externalTools/syncAll').then(
function (response) {
var result = response.data;
if (result.failureCount === 0) {
growl.success("Successfully synced to " + result.successCount + " external tool(s)");
} else if (result.successCount === 0) {
growl.error("Failed to sync to all " + result.failureCount + " external tool(s)");
} else {
growl.warning("Synced to " + result.successCount + " tool(s), " + result.failureCount + " failed");
}
},
function (error) {
growl.error("Error syncing to external tools: " + error.data);
}
);
};
$scope.addEntry = function (entriesCollection, preset) {
var model = angular.copy({
enabled: true,
syncType: "PER_INDEXER",
configureForUsenet: true,
configureForTorrents: false,
enableRss: true,
enableAutomaticSearch: true,
enableInteractiveSearch: true,
useHydraPriorities: true,
priority: 25,
nzbhydraName: "NZBHydra2"
});
if (angular.isDefined(preset)) {
_.extend(model, preset);
}
$scope.isInitial = true;
$scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {
if (isSubmitted) {
entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);
}
});
};
function getExternalToolBoxFields(model, parentModel, isInitial) {
var fieldset = [];
fieldset.push({
key: 'enabled',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enabled'
}
});
fieldset.push({
key: 'name',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Name',
required: true,
help: 'Unique name for this external tool instance'
},
validators: {
uniqueName: {
expression: function (viewValue) {
if (isInitial || viewValue !== model.name) {
return _.pluck(parentModel, "name").indexOf(viewValue) === -1;
}
return true;
},
message: '"External tool \\"" + $viewValue + "\\" already exists"'
}
}
});
fieldset.push({
key: 'type',
type: 'horizontalSelect',
templateOptions: {
type: 'select',
label: 'Type',
required: true,
options: [
{name: 'Sonarr', value: 'SONARR'},
{name: 'Radarr', value: 'RADARR'},
{name: 'Lidarr', value: 'LIDARR'},
{name: 'Readarr', value: 'READARR'}
]
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
// Update default categories based on type
switch (newValue) {
case 'SONARR':
model.categories = "5030,5040";
break;
case 'RADARR':
model.categories = "2000";
break;
case 'LIDARR':
model.categories = "3000";
break;
case 'READARR':
model.categories = "7020,8010";
break;
}
}
}
}
});
fieldset.push({
key: 'host',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Host URL',
help: 'URL with scheme and port (e.g., http://localhost:8989)',
required: true
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
scope.$parent.needsConnectionTest = true;
}
}
}
});
fieldset.push({
key: 'apiKey',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'API Key',
help: 'API key for the external tool'
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
scope.$parent.needsConnectionTest = true;
}
}
}
});
fieldset.push({
key: 'syncType',
type: 'horizontalSelect',
templateOptions: {
type: 'select',
label: 'Sync Type',
options: [
{name: 'Single entry for all indexers', value: 'SINGLE'},
{name: 'Separate entry per indexer', value: 'PER_INDEXER'}
],
help: 'Whether to create one entry for all indexers or separate entries'
}
});
fieldset.push({
key: 'nzbhydraName',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'NZBHydra Name',
help: 'Name prefix used in the external tool',
required: true
}
});
fieldset.push({
key: 'configureForUsenet',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Configure for Usenet',
help: 'Sync Usenet indexers'
}
});
fieldset.push({
key: 'configureForTorrents',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Configure for Torrents',
help: 'Sync torrent indexers'
}
});
fieldset.push({
key: 'addDisabledIndexers',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Add disabled indexers',
help: 'Also sync indexers that are disabled in NZBHydra',
advanced: true
}
});
fieldset.push({
key: 'useHydraPriorities',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Use Hydra priorities',
help: 'Map NZBHydra indexer priorities to the external tool'
}
});
fieldset.push({
key: 'priority',
type: 'horizontalInput',
hideExpression: 'model.useHydraPriorities && model.syncType === "PER_INDEXER"',
templateOptions: {
type: 'number',
label: 'Default Priority',
help: 'Priority to use when not using Hydra priorities (1-50, lower is better)',
placeholder: '25'
}
});
fieldset.push({
key: 'enableRss',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable RSS',
help: 'Enable RSS sync in the external tool'
}
});
fieldset.push({
key: 'enableAutomaticSearch',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable automatic search',
help: 'Enable automatic search in the external tool'
}
});
fieldset.push({
key: 'enableInteractiveSearch',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Enable interactive search',
help: 'Enable interactive (manual) search in the external tool'
}
});
fieldset.push({
key: 'categories',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Categories',
help: 'Comma-separated newznab category IDs',
advanced: true
}
});
if (model.type === 'SONARR') {
fieldset.push({
key: 'animeCategories',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Anime categories',
help: 'Comma-separated newznab category IDs for anime',
advanced: true
}
});
}
if (model.type === 'RADARR') {
fieldset.push({
key: 'removeYearFromSearchString',
type: 'horizontalSwitch',
templateOptions: {
type: 'switch',
label: 'Remove year from search',
help: 'Remove year from movie search queries',
advanced: true
}
});
}
if (model.type === 'LIDARR' || model.type === 'READARR') {
fieldset.push({
key: 'earlyDownloadLimit',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Early download limit',
advanced: true
}
});
}
fieldset.push({
key: 'additionalParameters',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Additional parameters',
help: 'Additional URL parameters to send to the indexer',
advanced: true
}
});
// Torrent-specific fields
if (model.configureForTorrents) {
fieldset.push({
key: 'minimumSeeders',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Minimum seeders',
help: 'Minimum number of seeders',
advanced: true
}
});
fieldset.push({
key: 'seedRatio',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Seed ratio',
advanced: true
}
});
fieldset.push({
key: 'seedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Seed time',
advanced: true
}
});
if (model.type === 'SONARR') {
fieldset.push({
key: 'seasonPackSeedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Season pack seed time',
advanced: true
}
});
}
if (model.type === 'LIDARR' || model.type === 'READARR') {
fieldset.push({
key: 'discographySeedTime',
type: 'horizontalInput',
templateOptions: {
type: 'text',
label: 'Discography seed time',
advanced: true
}
});
}
}
return fieldset;
}
}
});
});
angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceController', function ($scope, $q, $uibModalInstance, $http, model, fields, isInitial, parentModel, data, growl, blockUI) {
$scope.model = model;
$scope.fields = fields;
$scope.isInitial = isInitial;
$scope.spinnerActive = false;
$scope.needsConnectionTest = false;
$scope.obSubmit = function () {
if ($scope.form.$valid) {
checkConnection().then(function () {
$uibModalInstance.close($scope.model);
});
} else {
growl.error("Config invalid. Please check your settings.");
angular.forEach($scope.form.$error, function (error) {
angular.forEach(error, function (field) {
field.$setTouched();
});
});
}
};
$scope.cancel = function () {
$uibModalInstance.dismiss();
};
$scope.deleteEntry = function () {
parentModel.splice(parentModel.indexOf(model), 1);
$uibModalInstance.close($scope);
};
$scope.reset = function () {
if (angular.isDefined(data.resetFunction)) {
$scope.options.resetModel();
$scope.options.resetModel();
}
};
$scope.testConnection = function () {
$scope.spinnerActive = true;
blockUI.start("Testing connection...");
var testRequest = {
externalTool: model.type === 'SONARR' ? 'Sonarr' :
model.type === 'RADARR' ? 'Radarr' :
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
xdarrHost: model.host,
xdarrApiKey: model.apiKey,
addType: 'DELETE_ONLY' // Just test connection
};
$http.post("internalapi/externalTools/testConnection", testRequest).then(
function (response) {
blockUI.reset();
$scope.spinnerActive = false;
if (response.data.successful) {
growl.info("Connection test successful");
} else {
growl.error("Connection test failed: " + response.data.message);
}
},
function (error) {
blockUI.reset();
$scope.spinnerActive = false;
growl.error("Connection test failed: " + (error.data ? error.data.message : "Unknown error"));
}
);
};
function checkConnection() {
var deferred = $q.defer();
if (!$scope.isInitial && !$scope.needsConnectionTest) {
deferred.resolve();
} else {
$scope.spinnerActive = true;
blockUI.start("Testing connection...");
var testRequest = {
externalTool: model.type === 'SONARR' ? 'Sonarr' :
model.type === 'RADARR' ? 'Radarr' :
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
xdarrHost: model.host,
xdarrApiKey: model.apiKey,
addType: 'DELETE_ONLY'
};
$http.post("internalapi/externalTools/testConnection", testRequest).then(
function (response) {
blockUI.reset();
$scope.spinnerActive = false;
if (response.data.successful) {
growl.info("Connection test successful");
deferred.resolve();
} else {
growl.error("Connection test failed: " + response.data.message);
deferred.reject();
}
},
function (error) {
blockUI.reset();
$scope.spinnerActive = false;
growl.error("Connection test failed: " + (error.data ? error.data.message : "Unknown error"));
deferred.reject();
}
);
}
return deferred.promise;
}
$scope.$on("modal.closing", function (targetScope, reason) {
if (reason === "backdrop click") {
$scope.reset($scope);
}
});
});

View File

@ -59,6 +59,8 @@ public class BaseConfig {
private NotificationConfig notificationConfig = new NotificationConfig();
@NestedConfigurationProperty
private EmbyConfig emby = new EmbyConfig();
@NestedConfigurationProperty
private ExternalToolsConfig externalTools = new ExternalToolsConfig();
@DiffIgnore

View File

@ -0,0 +1,103 @@
/*
* (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nzbhydra.config;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.nzbhydra.config.sensitive.SensitiveData;
import org.nzbhydra.externaltools.AddRequest;
import org.nzbhydra.springnative.ReflectionMarker;
@Data
@ReflectionMarker
public class ExternalToolConfig {
@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum ExternalToolType {
SONARR,
RADARR,
LIDARR,
READARR
}
@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum SyncType {
SINGLE,
PER_INDEXER
}
private String name;
private ExternalToolType type = ExternalToolType.SONARR;
private String host;
@SensitiveData
private String apiKey;
private boolean enabled = true;
private SyncType syncType = AddRequest.AddType.SINGLE.name().equals("SINGLE") ? SyncType.SINGLE : SyncType.PER_INDEXER;
private String nzbhydraName = "NZBHydra2";
// Sync settings
private boolean configureForUsenet = true;
private boolean configureForTorrents = false;
private boolean addDisabledIndexers = false;
private boolean useHydraPriorities = true;
private Integer priority = 25;
// RSS and search settings
private boolean enableRss = true;
private boolean enableAutomaticSearch = true;
private boolean enableInteractiveSearch = true;
// Category settings
private String categories = "";
private String animeCategories = "";
// Additional settings
private String additionalParameters = "";
private String minimumSeeders = "1";
private String seedRatio = "";
private String seedTime = "";
private String seasonPackSeedTime = "";
private String discographySeedTime = "";
private String earlyDownloadLimit = "";
private boolean removeYearFromSearchString = false;
// Validation and lifecycle methods can be implemented if needed
public void prepareForSaving() {
// Ensure host doesn't end with slash
if (host != null && host.endsWith("/")) {
host = host.substring(0, host.length() - 1);
}
// Set default categories based on type if not set
if (categories == null || categories.isEmpty()) {
switch (type) {
case SONARR:
categories = "5030,5040";
break;
case RADARR:
categories = "2000";
break;
case LIDARR:
categories = "3000";
break;
case READARR:
categories = "7020,8010";
break;
}
}
}
}

View File

@ -0,0 +1,35 @@
/*
* (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nzbhydra.config;
import lombok.Data;
import org.nzbhydra.springnative.ReflectionMarker;
import java.util.ArrayList;
import java.util.List;
@Data
@ReflectionMarker
public class ExternalToolsConfig {
private List<ExternalToolConfig> externalTools = new ArrayList<>();
private boolean syncOnConfigChange = true;
public void prepareForSaving() {
externalTools.forEach(ExternalToolConfig::prepareForSaving);
}
}