Add NZB search results to torbox and show basic status
@ -1,18 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All system tests v1Migration profile" type="JUnit" factoryName="JUnit">
|
||||
<module name="system"/>
|
||||
<option name="MAIN_CLASS_NAME" value=""/>
|
||||
<option name="METHOD_NAME" value=""/>
|
||||
<option name="TEST_OBJECT" value="directory"/>
|
||||
<option name="VM_PARAMETERS"
|
||||
value="-Djunit.jupiter.extensions.autodetection.enabled=true -Dspring.profiles.active=systemtest,v1Migration -Dnzbhydra.port=5077 -Dnzbhydra.mockUrl=http://mockserver:5080 -Dnzbhydra.host.external=http://core:5076"/>
|
||||
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$"/>
|
||||
<envs>
|
||||
<env name="nzbhydra.name" value="v1Migration"/>
|
||||
</envs>
|
||||
<dir value="$PROJECT_DIR$/tests/system/src/test/java/org/nzbhydra"/>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true"/>
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -18,6 +18,7 @@ package org.nzbhydra.downloading.downloaders;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.nzbhydra.springnative.ReflectionMarker;
|
||||
@ -28,6 +29,7 @@ import java.time.Instant;
|
||||
@ReflectionMarker
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class DownloaderEntry {
|
||||
protected String nzbId;
|
||||
protected String nzbName;
|
||||
|
||||
@ -22,6 +22,7 @@ import org.nzbhydra.downloading.FileHandler;
|
||||
import org.nzbhydra.downloading.IndexerSpecificDownloadExceptions;
|
||||
import org.nzbhydra.downloading.downloaders.nzbget.NzbGet;
|
||||
import org.nzbhydra.downloading.downloaders.sabnzbd.Sabnzbd;
|
||||
import org.nzbhydra.downloading.downloaders.torbox.Torbox;
|
||||
import org.nzbhydra.searching.db.SearchResultRepository;
|
||||
import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory;
|
||||
import org.nzbhydra.webaccess.Ssl;
|
||||
@ -58,6 +59,9 @@ public class DownloaderInstatiator {
|
||||
case SABNZBD -> {
|
||||
return new Sabnzbd(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider, restTemplate, requestFactory);
|
||||
}
|
||||
case TORBOX -> {
|
||||
return new Torbox(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider, requestFactory);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Unable to instantiate " + downloaderType);
|
||||
}
|
||||
|
||||
@ -21,9 +21,6 @@ import org.nzbhydra.config.BaseConfig;
|
||||
import org.nzbhydra.config.ConfigChangedEvent;
|
||||
import org.nzbhydra.config.ConfigProvider;
|
||||
import org.nzbhydra.config.downloading.DownloaderConfig;
|
||||
import org.nzbhydra.downloading.DownloaderType;
|
||||
import org.nzbhydra.downloading.downloaders.nzbget.NzbGet;
|
||||
import org.nzbhydra.downloading.downloaders.sabnzbd.Sabnzbd;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@ -34,20 +31,11 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class DownloaderProvider implements InitializingBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DownloaderProvider.class);
|
||||
|
||||
private static final Map<DownloaderType, Class<? extends Downloader>> downloaderClasses = new HashMap<>();
|
||||
|
||||
static {
|
||||
downloaderClasses.put(DownloaderType.SABNZBD, Sabnzbd.class);
|
||||
downloaderClasses.put(DownloaderType.NZBGET, NzbGet.class);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ConfigProvider configProvider;
|
||||
@Autowired
|
||||
|
||||
@ -18,8 +18,10 @@ package org.nzbhydra.downloading.downloaders.torbox;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.nzbhydra.GenericResponse;
|
||||
import org.nzbhydra.config.ConfigProvider;
|
||||
import org.nzbhydra.downloading.DownloaderType;
|
||||
import org.nzbhydra.downloading.FileDownloadStatus;
|
||||
import org.nzbhydra.downloading.FileHandler;
|
||||
import org.nzbhydra.downloading.IndexerSpecificDownloadExceptions;
|
||||
@ -27,6 +29,7 @@ import org.nzbhydra.downloading.downloaders.Downloader;
|
||||
import org.nzbhydra.downloading.downloaders.DownloaderEntry;
|
||||
import org.nzbhydra.downloading.downloaders.DownloaderStatus;
|
||||
import org.nzbhydra.downloading.downloaders.torbox.mapping.AddUDlResponse;
|
||||
import org.nzbhydra.downloading.downloaders.torbox.mapping.UsenetListResponse;
|
||||
import org.nzbhydra.downloading.exceptions.DownloaderException;
|
||||
import org.nzbhydra.searching.db.SearchResultRepository;
|
||||
import org.nzbhydra.webaccess.HydraOkHttp3ClientHttpRequestFactory;
|
||||
@ -44,7 +47,9 @@ import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@ -54,11 +59,18 @@ public class Torbox extends Downloader {
|
||||
// TODO sist 14.12.2024: Try and parse error responses
|
||||
// TODO sist 14.12.2024: Exclude nzbs.in
|
||||
// TODO sist 14.12.2024: Check if bypass_cache for status check is OK and which limit should be used
|
||||
// TODO sist 14.12.2024: Cache calls to usenet/torrents list so that we only make one call for history, status and queue
|
||||
// TODO sist 14.12.2024: What are possible values for download_state?
|
||||
|
||||
private static final String HOST = "torbox.app";
|
||||
private static final String BASE_URL = "https://api.torbox.app/v1/api";
|
||||
private static final String BASE_URL = "https://torbox.app";
|
||||
private static final String BASE_API_URL = "https://api.torbox.app/v1/api";
|
||||
public static final Duration CACHE_TIME = Duration.ofSeconds(5);
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private Instant lastUpdate = Instant.MIN;
|
||||
private final List<DownloaderEntry> lastDownloaderEntries = new ArrayList<>();
|
||||
|
||||
|
||||
public Torbox(FileHandler nzbHandler, SearchResultRepository searchResultRepository, ApplicationEventPublisher applicationEventPublisher, IndexerSpecificDownloadExceptions indexerSpecificDownloadExceptions, ConfigProvider configProvider, HydraOkHttp3ClientHttpRequestFactory requestFactory) {
|
||||
super(nzbHandler, searchResultRepository, applicationEventPublisher, indexerSpecificDownloadExceptions, configProvider);
|
||||
@ -143,26 +155,87 @@ public class Torbox extends Downloader {
|
||||
|
||||
@Override
|
||||
public DownloaderStatus getStatus() throws DownloaderException {
|
||||
return null;
|
||||
return DownloaderStatus.builder()
|
||||
.downloaderName("Torbox")
|
||||
.downloaderType(DownloaderType.TORBOX)
|
||||
.state(DownloaderStatus.State.IDLE)
|
||||
.url(BASE_URL)
|
||||
.elementsInQueue(0)
|
||||
.downloadingRatesInKilobytes(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DownloaderEntry> getHistory(Instant earliestDownload) throws DownloaderException {
|
||||
return List.of();
|
||||
return getDownloaderEntries()
|
||||
.stream()
|
||||
.filter(x -> x.getStatus().equals("failed") || x.getStatus().equals("completed"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DownloaderEntry> getQueue(Instant earliestDownload) throws DownloaderException {
|
||||
return List.of();
|
||||
return getDownloaderEntries()
|
||||
.stream()
|
||||
.filter(x -> !x.getStatus().equals("failed") && !x.getStatus().equals("completed"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<DownloaderEntry> getDownloaderEntries() throws DownloaderException {
|
||||
if (lastUpdate.isAfter(Instant.now().minus(CACHE_TIME))) {
|
||||
return lastDownloaderEntries;
|
||||
}
|
||||
log.debug("Loading usenet list from torbox");
|
||||
UriComponentsBuilder url = getBaseUrl()
|
||||
.path("/usenet/mylist")
|
||||
.queryParam("bypass_cache", true);
|
||||
try {
|
||||
ResponseEntity<UsenetListResponse> response = restTemplate.getForEntity(url.toUriString(), UsenetListResponse.class);
|
||||
UsenetListResponse body = response.getBody();
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
List<DownloaderEntry> downloaderEntries = body.getData().stream().map(entry -> DownloaderEntry.builder()
|
||||
.nzbId(String.valueOf(entry.getId()))
|
||||
.nzbName(entry.getName())
|
||||
.time(entry.getCreated_at())
|
||||
.status(entry.getDownload_state())
|
||||
.build())
|
||||
.toList();
|
||||
lastUpdate = Instant.now();
|
||||
lastDownloaderEntries.clear();
|
||||
lastDownloaderEntries.addAll(downloaderEntries);
|
||||
|
||||
return downloaderEntries;
|
||||
}
|
||||
log.error("Loading usenet list from torbox failed. Error: {}. Details:\n{}", body.getError(), body.getDetail());
|
||||
throw new DownloaderException("Error loading usenet list from torbox. Error: " + body.getError());
|
||||
|
||||
} catch (RestClientException e) {
|
||||
throw new DownloaderException("Error loading usenet list from torbox", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FileDownloadStatus getDownloadStatusFromDownloaderEntry(DownloaderEntry entry, StatusCheckType statusCheckType) {
|
||||
return null;
|
||||
switch (entry.getStatus()) {
|
||||
case "completed" -> {
|
||||
return FileDownloadStatus.CONTENT_DOWNLOAD_SUCCESSFUL;
|
||||
}
|
||||
case "failed" -> {
|
||||
return FileDownloadStatus.CONTENT_DOWNLOAD_ERROR;
|
||||
}
|
||||
case "downloading" -> {
|
||||
return FileDownloadStatus.NZB_ADDED;
|
||||
}
|
||||
default -> {
|
||||
return FileDownloadStatus.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private UriComponentsBuilder getBaseUrl() {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_URL);
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_API_URL);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2583,7 +2583,14 @@ function downloaderStatusFooter() {
|
||||
}
|
||||
|
||||
$scope.foo = downloaderStatus;
|
||||
$scope.foo.downloaderImage = downloaderStatus.downloaderType === 'NZBGET' ? 'nzbgetlogo' : 'sabnzbdlogo';
|
||||
if (downloaderStatus.downloaderType === 'NZBGET') {
|
||||
$scope.foo.downloaderImage = 'nzbgetlogo';
|
||||
}
|
||||
if (downloaderStatus.downloaderType === 'TORBOX') {
|
||||
$scope.foo.downloaderImage = 'torboxlogo';
|
||||
} else {
|
||||
$scope.foo.downloaderImage = 'sabnzbdlogo';
|
||||
}
|
||||
$scope.foo.url = downloaderStatus.url;
|
||||
//We need to splice the variable with the rates because it's watched by angular and when overwriting it we would lose the watch and it wouldn't be updated
|
||||
var maxEntriesHistory = 200;
|
||||
@ -3220,13 +3227,13 @@ function connectionTest() {
|
||||
if ($scope.type === "downloader") {
|
||||
url = "internalapi/test_downloader";
|
||||
params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};
|
||||
if ($scope.downloader === "SABNZBD") {
|
||||
params.apiKey = $scope.data.apiKey;
|
||||
params.url = $scope.data.url;
|
||||
} else {
|
||||
if ($scope.downloader === "NZBGET") {
|
||||
params.host = $scope.data.host;
|
||||
params.port = $scope.data.port;
|
||||
params.ssl = $scope.data.ssl;
|
||||
} else {
|
||||
params.apiKey = $scope.data.apiKey;
|
||||
params.url = $scope.data.url;
|
||||
}
|
||||
} else if ($scope.data.type === "newznab") {
|
||||
url = "internalapi/test_newznab";
|
||||
@ -3517,6 +3524,16 @@ addableNzb.$inject = ["DebugService"];angular
|
||||
.module('nzbhydraApp')
|
||||
.directive('addableNzb', addableNzb);
|
||||
|
||||
function getCssClass(downloaderType) {
|
||||
if (downloaderType === "SABNZBD") {
|
||||
return "sabnzbd";
|
||||
} else if (downloaderType === "TORBOX") {
|
||||
return "torbox";
|
||||
} else {
|
||||
return "nzbget";
|
||||
}
|
||||
}
|
||||
|
||||
function addableNzb(DebugService) {
|
||||
controller.$inject = ["$scope", "NzbDownloadService", "growl"];
|
||||
return {
|
||||
@ -3533,7 +3550,7 @@ function addableNzb(DebugService) {
|
||||
if (!_.isNullOrEmpty($scope.downloader.iconCssClass)) {
|
||||
$scope.cssClass = "fa fa-" + $scope.downloader.iconCssClass.replace("fa-", "").replace("fa ", "");
|
||||
} else {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd" : "nzbget";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType);
|
||||
}
|
||||
|
||||
$scope.add = function () {
|
||||
@ -3546,16 +3563,16 @@ function addableNzb(DebugService) {
|
||||
}], $scope.alwaysAsk).then(function (response) {
|
||||
if (response !== "dismissed") {
|
||||
if (response.data.successful && (response.data.addedIds != null && response.data.addedIds.indexOf(Number($scope.searchresult.searchResultId)) > -1)) {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-success" : "nzbget-success";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-success";
|
||||
} else {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-error" : "nzbget-error";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-error";
|
||||
growl.error(response.data.message);
|
||||
}
|
||||
} else {
|
||||
$scope.cssClass = originalClass;
|
||||
}
|
||||
}, function () {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-error" : "nzbget-error";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-error";
|
||||
growl.error("An unexpected error occurred while trying to contact NZBHydra or add the NZB.");
|
||||
})
|
||||
};
|
||||
@ -4919,6 +4936,14 @@ angular
|
||||
nzbAccessType: "REDIRECT",
|
||||
iconCssClass: "",
|
||||
downloadType: "NZB"
|
||||
},
|
||||
{
|
||||
downloaderType: "TORBOX",
|
||||
name: "Torbox",
|
||||
nzbAddingType: "UPLOAD",
|
||||
nzbAccessType: "PROXY",
|
||||
iconCssClass: "",
|
||||
downloadType: "NZB"
|
||||
}
|
||||
];
|
||||
|
||||
@ -5016,10 +5041,11 @@ angular
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
}]);
|
||||
fieldset.push({
|
||||
key: 'url',
|
||||
type: 'horizontalInput',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'URL',
|
||||
@ -5034,13 +5060,14 @@ angular
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
);
|
||||
|
||||
|
||||
if (model.downloaderType === "SABNZBD") {
|
||||
if (model.downloaderType === "SABNZBD" || model.downloaderType === "TORBOX") {
|
||||
fieldset.push({
|
||||
key: 'apiKey',
|
||||
type: 'horizontalInput',
|
||||
showFor: ["SABNZBD", "TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'API Key'
|
||||
@ -5086,43 +5113,47 @@ angular
|
||||
})
|
||||
}
|
||||
|
||||
fieldset = _.union(fieldset, [
|
||||
fieldset.push(
|
||||
{
|
||||
key: 'defaultCategory',
|
||||
type: 'horizontalInput',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'Default category',
|
||||
help: 'When adding NZBs this category will be used instead of asking for the category. Write "Use original category", "Use no category" or "Use mapped category" to not be asked.',
|
||||
placeholder: 'Ask when downloading'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'nzbAddingType',
|
||||
type: 'horizontalSelect',
|
||||
templateOptions: {
|
||||
type: 'select',
|
||||
label: 'NZB adding type',
|
||||
options: [
|
||||
{name: 'Send link', value: 'SEND_LINK'},
|
||||
{name: 'Upload NZB', value: 'UPLOAD'}
|
||||
],
|
||||
help: "How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.",
|
||||
tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +
|
||||
'<br>Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',
|
||||
advanced: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'addPaused',
|
||||
type: 'horizontalSwitch',
|
||||
templateOptions: {
|
||||
type: 'switch',
|
||||
label: 'Add paused',
|
||||
help: 'Add NZBs paused',
|
||||
advanced: true
|
||||
}
|
||||
},
|
||||
});
|
||||
fieldset.push({
|
||||
key: 'nzbAddingType',
|
||||
type: 'horizontalSelect',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'select',
|
||||
label: 'NZB adding type',
|
||||
options: [
|
||||
{name: 'Send link', value: 'SEND_LINK'},
|
||||
{name: 'Upload NZB', value: 'UPLOAD'}
|
||||
],
|
||||
help: "How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.",
|
||||
tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +
|
||||
'<br>Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset.push({
|
||||
key: 'addPaused',
|
||||
type: 'horizontalSwitch',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'switch',
|
||||
label: 'Add paused',
|
||||
help: 'Add NZBs paused',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset.push(
|
||||
{
|
||||
key: 'iconCssClass',
|
||||
type: 'horizontalInput',
|
||||
@ -5134,9 +5165,16 @@ angular
|
||||
tooltip: 'If you have multiple downloaders of the same type you can select an icon from the Font Awesome library. This icon will be shown in the search results and the NZB download history instead of the default downloader icon.',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset = fieldset.filter(function (field) {
|
||||
if (field.showFor) {
|
||||
return field.showFor.includes(model.downloaderType);
|
||||
}
|
||||
]);
|
||||
|
||||
if (field.hideFor) {
|
||||
return !field.hideFor.includes(model.downloaderType);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return fieldset;
|
||||
}
|
||||
}
|
||||
@ -7939,7 +7977,7 @@ function ConfigFields($injector) {
|
||||
wrapper: 'fieldset',
|
||||
templateOptions: {
|
||||
label: 'General',
|
||||
tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd). Torrent downloaders are not supported.'
|
||||
tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd, torbox). Torrent downloaders are not supported.'
|
||||
},
|
||||
fieldGroup: [
|
||||
{
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"detail": "Torrent list retrieved successfully.",
|
||||
"data": [
|
||||
{
|
||||
"id": 364792,
|
||||
"auth_id": "1668bedb-8f02-491d-ac8d-a606158c505b",
|
||||
"server": 16,
|
||||
"hash": "3f9aac158c7de8dfcab171ea58a17aabdf7fbc93",
|
||||
"name": "ubuntu-24.10-desktop-amd64",
|
||||
"magnet": null,
|
||||
"size": 5665497088,
|
||||
"active": false,
|
||||
"created_at": "2024-12-14T16:32:01Z",
|
||||
"updated_at": "2024-12-14T16:32:01Z",
|
||||
"download_state": "cached",
|
||||
"seeds": 0,
|
||||
"peers": 0,
|
||||
"ratio": 0,
|
||||
"progress": 1,
|
||||
"download_speed": 0,
|
||||
"upload_speed": 0,
|
||||
"eta": 0,
|
||||
"torrent_file": false,
|
||||
"expires_at": null,
|
||||
"download_present": true,
|
||||
"files": [
|
||||
{
|
||||
"id": 0,
|
||||
"md5": "ea0baab241893d4f2b679e7179009e1c",
|
||||
"hash": "3f9aac158c7de8dfcab171ea58a17aabdf7fbc93",
|
||||
"name": "ubuntu-24.10-desktop-amd64/ubuntu-24.10-desktop-amd64.iso",
|
||||
"size": 5665497088,
|
||||
"s3_path": "3f9aac158c7de8dfcab171ea58a17aabdf7fbc93/ubuntu-24.10-desktop-amd64/ubuntu-24.10-desktop-amd64.iso",
|
||||
"mimetype": "application/x-iso9660-image",
|
||||
"short_name": "ubuntu-24.10-desktop-amd64.iso",
|
||||
"absolute_path": "/completed/3f9aac158c7de8dfcab171ea58a17aabdf7fbc93/ubuntu-24.10-desktop-amd64/ubuntu-24.10-desktop-amd64.iso"
|
||||
}
|
||||
],
|
||||
"download_path": "3f9aac158c7de8dfcab171ea58a17aabdf7fbc93",
|
||||
"inactive_check": 0,
|
||||
"availability": 0,
|
||||
"download_finished": true,
|
||||
"tracker": null,
|
||||
"total_uploaded": 0,
|
||||
"total_downloaded": 0,
|
||||
"cached": true,
|
||||
"owner": "1668bedb-8f02-491d-ac8d-a606158c505b",
|
||||
"seed_torrent": false,
|
||||
"allow_zipped": true,
|
||||
"long_term_seeding": false,
|
||||
"tracker_message": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"detail": "Usenet downloads list retrieved successfully.",
|
||||
"data": [
|
||||
{
|
||||
"id": 27949,
|
||||
"created_at": "2024-12-14T10:27:00Z",
|
||||
"updated_at": "2024-12-14T10:27:00Z",
|
||||
"auth_id": "408f0af4-f476-4fb9-9f13-c62dbb1f4d45",
|
||||
"name": "Die.Hamburger.Purzelschachtel",
|
||||
"hash": "6b531e077cd6b8b86e190dc9614e6807",
|
||||
"download_state": "failed",
|
||||
"download_speed": 0,
|
||||
"original_url": null,
|
||||
"eta": 0,
|
||||
"progress": 0,
|
||||
"size": 444711567,
|
||||
"download_id": "SABnzbd_nzo_1q30bevu",
|
||||
"files": [],
|
||||
"active": false,
|
||||
"cached": false,
|
||||
"download_present": false,
|
||||
"download_finished": false,
|
||||
"expires_at": null,
|
||||
"server": 18
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
core/ui-src/img/checkmark.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
core/ui-src/img/redcross.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
core/ui-src/img/torbox.png
Normal file
|
After Width: | Height: | Size: 620 B |
18
core/ui-src/img/torbox.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 1500 1500" style="enable-background:new 0 0 1500 1500;" xml:space="preserve" width="100px" height="100px">
|
||||
<style type="text/css">
|
||||
.st0{fill:#00444D;}
|
||||
.st1{fill:#34BA90;}
|
||||
.st2{fill:#52A153;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<polygon class="st0" points="749.99,749.99 749.99,1191.96 367.25,970.97 367.25,529.01 "/>
|
||||
<polygon class="st1" points="1132.75,529.01 1132.75,970.97 749.99,1191.96 749.99,749.99 872.87,679.05 956.71,630.66 "/>
|
||||
<polygon class="st2" points="1132.75,529.01 749.99,749.99 367.25,529.01 749.99,308.04 "/>
|
||||
<polygon class="st3" points="1043.04,739.36 958.66,1057.08 952.4,851.84 839.71,915.39 872.87,679.05 956.71,630.66
|
||||
931.81,799.21 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 939 B |
BIN
core/ui-src/img/torboxerror.png
Normal file
|
After Width: | Height: | Size: 812 B |
BIN
core/ui-src/img/torboxlogo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
core/ui-src/img/torboxsuccess.png
Normal file
|
After Width: | Height: | Size: 835 B |
@ -1783,7 +1783,7 @@ function ConfigFields($injector) {
|
||||
wrapper: 'fieldset',
|
||||
templateOptions: {
|
||||
label: 'General',
|
||||
tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd). Torrent downloaders are not supported.'
|
||||
tooltip: 'Hydra allows sending NZB search results directly to downloaders (NZBGet, sabnzbd, torbox). Torrent downloaders are not supported.'
|
||||
},
|
||||
fieldGroup: [
|
||||
{
|
||||
|
||||
@ -44,6 +44,14 @@ angular
|
||||
nzbAccessType: "REDIRECT",
|
||||
iconCssClass: "",
|
||||
downloadType: "NZB"
|
||||
},
|
||||
{
|
||||
downloaderType: "TORBOX",
|
||||
name: "Torbox",
|
||||
nzbAddingType: "UPLOAD",
|
||||
nzbAccessType: "PROXY",
|
||||
iconCssClass: "",
|
||||
downloadType: "NZB"
|
||||
}
|
||||
];
|
||||
|
||||
@ -141,10 +149,11 @@ angular
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
}]);
|
||||
fieldset.push({
|
||||
key: 'url',
|
||||
type: 'horizontalInput',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'URL',
|
||||
@ -159,13 +168,14 @@ angular
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
);
|
||||
|
||||
|
||||
if (model.downloaderType === "SABNZBD") {
|
||||
if (model.downloaderType === "SABNZBD" || model.downloaderType === "TORBOX") {
|
||||
fieldset.push({
|
||||
key: 'apiKey',
|
||||
type: 'horizontalInput',
|
||||
showFor: ["SABNZBD", "TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'API Key'
|
||||
@ -211,43 +221,47 @@ angular
|
||||
})
|
||||
}
|
||||
|
||||
fieldset = _.union(fieldset, [
|
||||
fieldset.push(
|
||||
{
|
||||
key: 'defaultCategory',
|
||||
type: 'horizontalInput',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'Default category',
|
||||
help: 'When adding NZBs this category will be used instead of asking for the category. Write "Use original category", "Use no category" or "Use mapped category" to not be asked.',
|
||||
placeholder: 'Ask when downloading'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'nzbAddingType',
|
||||
type: 'horizontalSelect',
|
||||
templateOptions: {
|
||||
type: 'select',
|
||||
label: 'NZB adding type',
|
||||
options: [
|
||||
{name: 'Send link', value: 'SEND_LINK'},
|
||||
{name: 'Upload NZB', value: 'UPLOAD'}
|
||||
],
|
||||
help: "How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.",
|
||||
tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +
|
||||
'<br>Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',
|
||||
advanced: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'addPaused',
|
||||
type: 'horizontalSwitch',
|
||||
templateOptions: {
|
||||
type: 'switch',
|
||||
label: 'Add paused',
|
||||
help: 'Add NZBs paused',
|
||||
advanced: true
|
||||
}
|
||||
},
|
||||
});
|
||||
fieldset.push({
|
||||
key: 'nzbAddingType',
|
||||
type: 'horizontalSelect',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'select',
|
||||
label: 'NZB adding type',
|
||||
options: [
|
||||
{name: 'Send link', value: 'SEND_LINK'},
|
||||
{name: 'Upload NZB', value: 'UPLOAD'}
|
||||
],
|
||||
help: "How NZBs are added to the downloader, either by sending a link to the NZB or by uploading the NZB data.",
|
||||
tooltip: 'You can select if you want to upload the NZB to the downloader or send a Hydra link. The downloader will do the download itself. This is a matter of taste, but adding a link and redirecting the downloader is the fastest way.' +
|
||||
'<br>Usually the links are determined using the URL via which you call it in your browser. If your downloader cannot access NZBHydra using that URL you can set a specific URL to be used in the main downloading config.',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset.push({
|
||||
key: 'addPaused',
|
||||
type: 'horizontalSwitch',
|
||||
hideFor: ["TORBOX"],
|
||||
templateOptions: {
|
||||
type: 'switch',
|
||||
label: 'Add paused',
|
||||
help: 'Add NZBs paused',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset.push(
|
||||
{
|
||||
key: 'iconCssClass',
|
||||
type: 'horizontalInput',
|
||||
@ -259,9 +273,16 @@ angular
|
||||
tooltip: 'If you have multiple downloaders of the same type you can select an icon from the Font Awesome library. This icon will be shown in the search results and the NZB download history instead of the default downloader icon.',
|
||||
advanced: true
|
||||
}
|
||||
});
|
||||
fieldset = fieldset.filter(function (field) {
|
||||
if (field.showFor) {
|
||||
return field.showFor.includes(model.downloaderType);
|
||||
}
|
||||
]);
|
||||
|
||||
if (field.hideFor) {
|
||||
return !field.hideFor.includes(model.downloaderType);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return fieldset;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,16 @@ angular
|
||||
.module('nzbhydraApp')
|
||||
.directive('addableNzb', addableNzb);
|
||||
|
||||
function getCssClass(downloaderType) {
|
||||
if (downloaderType === "SABNZBD") {
|
||||
return "sabnzbd";
|
||||
} else if (downloaderType === "TORBOX") {
|
||||
return "torbox";
|
||||
} else {
|
||||
return "nzbget";
|
||||
}
|
||||
}
|
||||
|
||||
function addableNzb(DebugService) {
|
||||
return {
|
||||
templateUrl: 'static/html/directives/addable-nzb.html',
|
||||
@ -17,7 +27,7 @@ function addableNzb(DebugService) {
|
||||
if (!_.isNullOrEmpty($scope.downloader.iconCssClass)) {
|
||||
$scope.cssClass = "fa fa-" + $scope.downloader.iconCssClass.replace("fa-", "").replace("fa ", "");
|
||||
} else {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd" : "nzbget";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType);
|
||||
}
|
||||
|
||||
$scope.add = function () {
|
||||
@ -30,16 +40,16 @@ function addableNzb(DebugService) {
|
||||
}], $scope.alwaysAsk).then(function (response) {
|
||||
if (response !== "dismissed") {
|
||||
if (response.data.successful && (response.data.addedIds != null && response.data.addedIds.indexOf(Number($scope.searchresult.searchResultId)) > -1)) {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-success" : "nzbget-success";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-success";
|
||||
} else {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-error" : "nzbget-error";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-error";
|
||||
growl.error(response.data.message);
|
||||
}
|
||||
} else {
|
||||
$scope.cssClass = originalClass;
|
||||
}
|
||||
}, function () {
|
||||
$scope.cssClass = $scope.downloader.downloaderType === "SABNZBD" ? "sabnzbd-error" : "nzbget-error";
|
||||
$scope.cssClass = getCssClass($scope.downloader.downloaderType) + "-error";
|
||||
growl.error("An unexpected error occurred while trying to contact NZBHydra or add the NZB.");
|
||||
})
|
||||
};
|
||||
|
||||
@ -43,13 +43,13 @@ function connectionTest() {
|
||||
if ($scope.type === "downloader") {
|
||||
url = "internalapi/test_downloader";
|
||||
params = {name: $scope.downloader, username: $scope.data.username, password: $scope.data.password};
|
||||
if ($scope.downloader === "SABNZBD") {
|
||||
params.apiKey = $scope.data.apiKey;
|
||||
params.url = $scope.data.url;
|
||||
} else {
|
||||
if ($scope.downloader === "NZBGET") {
|
||||
params.host = $scope.data.host;
|
||||
params.port = $scope.data.port;
|
||||
params.ssl = $scope.data.ssl;
|
||||
} else {
|
||||
params.apiKey = $scope.data.apiKey;
|
||||
params.url = $scope.data.url;
|
||||
}
|
||||
} else if ($scope.data.type === "newznab") {
|
||||
url = "internalapi/test_newznab";
|
||||
|
||||
@ -123,7 +123,14 @@ function downloaderStatusFooter() {
|
||||
}
|
||||
|
||||
$scope.foo = downloaderStatus;
|
||||
$scope.foo.downloaderImage = downloaderStatus.downloaderType === 'NZBGET' ? 'nzbgetlogo' : 'sabnzbdlogo';
|
||||
if (downloaderStatus.downloaderType === 'NZBGET') {
|
||||
$scope.foo.downloaderImage = 'nzbgetlogo';
|
||||
}
|
||||
if (downloaderStatus.downloaderType === 'TORBOX') {
|
||||
$scope.foo.downloaderImage = 'torboxlogo';
|
||||
} else {
|
||||
$scope.foo.downloaderImage = 'sabnzbdlogo';
|
||||
}
|
||||
$scope.foo.url = downloaderStatus.url;
|
||||
//We need to splice the variable with the rates because it's watched by angular and when overwriting it we would lose the watch and it wouldn't be updated
|
||||
var maxEntriesHistory = 200;
|
||||
|
||||
@ -37,6 +37,18 @@
|
||||
background-image: url('../../img/nzbgeterror.png');
|
||||
}
|
||||
|
||||
.torbox {
|
||||
background-image: url('../../img/torbox.png');
|
||||
}
|
||||
|
||||
.torbox-success {
|
||||
background-image: url('../../img/torboxsuccess.png');
|
||||
}
|
||||
|
||||
.torbox-error {
|
||||
background-image: url('../../img/torboxerror.png');
|
||||
}
|
||||
|
||||
.growl-container > .icon {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
@ -19,6 +19,7 @@ package org.nzbhydra.downloading;
|
||||
public enum DownloaderType {
|
||||
|
||||
SABNZBD,
|
||||
NZBGET
|
||||
NZBGET,
|
||||
TORBOX
|
||||
|
||||
}
|
||||
|
||||
@ -19,7 +19,9 @@ package org.nzbhydra.downloading.downloaders;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.Iterables;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.nzbhydra.downloading.DownloaderType;
|
||||
import org.nzbhydra.springnative.ReflectionMarker;
|
||||
|
||||
@ -28,6 +30,9 @@ import java.util.List;
|
||||
|
||||
@Data
|
||||
@ReflectionMarker
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class DownloaderStatus {
|
||||
|
||||
public enum State {
|
||||
@ -48,6 +53,7 @@ public class DownloaderStatus {
|
||||
|
||||
private String downloadRateFormatted;
|
||||
private long downloadRateInKilobytes;
|
||||
@Builder.Default
|
||||
private List<Long> downloadingRatesInKilobytes = new ArrayList<>();
|
||||
private Long lastDownloadRate;
|
||||
|
||||
|
||||