Skip to content

Commit

Permalink
Support WtfNzb
Browse files Browse the repository at this point in the history
Closes #275
  • Loading branch information
theotherp committed Jul 13, 2024
1 parent 6449d86 commit f3b7b23
Show file tree
Hide file tree
Showing 15 changed files with 2,258 additions and 29 deletions.
16 changes: 12 additions & 4 deletions core/src/main/java/org/nzbhydra/downloading/FileHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,19 @@ public void updateStatusByEntity(FileDownloadEntity entity, FileDownloadStatus s


protected byte[] downloadFile(SearchResultEntity result) throws MagnetLinkRedirectException, DownloadException {
Request request = new Request.Builder().url(result.getLink()).build();
Indexer indexerByName = searchModuleProvider.getIndexerByName(result.getIndexer().getName());
Integer timeout = indexerByName.getConfig().getTimeout().orElse(configProvider.getBaseConfig().getSearching().getTimeout());
final OkHttpClient build = clientHttpRequestFactory.getOkHttpClient(request.url().uri().getHost(), timeout);
try (Response response = build.newCall(request).execute()) {
IndexerConfig indexerConfig = indexerByName.getConfig();
Integer timeout = indexerConfig.getTimeout().orElse(configProvider.getBaseConfig().getSearching().getTimeout());
Request.Builder requestBuilder = new Request.Builder().url(result.getLink());

indexerConfig.getUserAgent()
.or(() -> configProvider.getBaseConfig().getSearching().getUserAgent())
.ifPresent(s -> requestBuilder.header("User-Agent", s));

Request request = requestBuilder.build();
final OkHttpClient client = clientHttpRequestFactory.getOkHttpClient(request.url().uri().getHost(), timeout);

try (Response response = client.newCall(request).execute()) {
if (response.isRedirect()) {
return handleRedirect(result, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public <T> T get(URI uri, IndexerConfig indexerConfig, Class responseType) throw
headers.put("Content-Type", "application/xml");
headers.put("Accept", "application/xml");


if (indexerConfig.getUsername().isPresent() && indexerConfig.getPassword().isPresent()) {
headers.put("Authorization", "Basic " + BaseEncoding.base64().encode((indexerConfig.getUsername().get() + ":" + indexerConfig.getPassword().get()).getBytes()));
}
Expand Down
1 change: 0 additions & 1 deletion core/src/main/java/org/nzbhydra/indexers/Newznab.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ public Newznab(ConfigProvider configProvider, IndexerRepository indexerRepositor
this.indexerStatusRepository = indexerStatusRepository;
}


protected UriComponentsBuilder getBaseUri() {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(config.getHost()).path(config.getApiPath().orElse("/api"));
if (!Strings.isNullOrEmpty(config.getApiKey())) {
Expand Down
104 changes: 104 additions & 0 deletions core/src/main/java/org/nzbhydra/indexers/WtfNzb.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.nzbhydra.indexers;

import org.nzbhydra.config.BaseConfigHandler;
import org.nzbhydra.config.ConfigProvider;
import org.nzbhydra.config.indexer.IndexerConfig;
import org.nzbhydra.config.indexer.SearchModuleType;
import org.nzbhydra.indexers.exceptions.IndexerSearchAbortedException;
import org.nzbhydra.indexers.status.IndexerLimitRepository;
import org.nzbhydra.mapping.newznab.xml.NewznabXmlResponse;
import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot;
import org.nzbhydra.mapping.newznab.xml.Xml;
import org.nzbhydra.mediainfo.InfoProvider;
import org.nzbhydra.searching.CategoryProvider;
import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler;
import org.nzbhydra.searching.SearchResultAcceptor;
import org.nzbhydra.searching.SearchResultAcceptor.AcceptorResult;
import org.nzbhydra.searching.db.SearchResultRepository;
import org.nzbhydra.searching.dtoseventsenums.IndexerSearchResult;
import org.nzbhydra.searching.searchrequests.SearchRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.annotation.Order;
import org.springframework.oxm.Unmarshaller;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

@Component
public class WtfNzb extends Newznab {

private static final Logger logger = LoggerFactory.getLogger(WtfNzb.class);

public WtfNzb(ConfigProvider configProvider, IndexerRepository indexerRepository, SearchResultRepository searchResultRepository, IndexerApiAccessRepository indexerApiAccessRepository, IndexerApiAccessEntityShortRepository indexerApiAccessShortRepository, IndexerLimitRepository indexerStatusRepository, IndexerWebAccess indexerWebAccess, SearchResultAcceptor resultAcceptor, CategoryProvider categoryProvider, InfoProvider infoProvider, ApplicationEventPublisher eventPublisher, QueryGenerator queryGenerator, CustomQueryAndTitleMappingHandler titleMapping, Unmarshaller unmarshaller, BaseConfigHandler baseConfigHandler) {
super(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler);
}

@Override
public IndexerSearchResult search(SearchRequest searchRequest, int offset, Integer limit) {
if (searchRequest.getQuery().isEmpty() && searchRequest.getIdentifiers().isEmpty()) {
return new IndexerSearchResult(this, "Search without query and query generation not supported");
}
return super.search(searchRequest, offset, limit);
}

@Override
protected UriComponentsBuilder buildSearchUrl(SearchRequest searchRequest, Integer offset, Integer limit) throws IndexerSearchAbortedException {

UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(config.getHost()).path("/api_fast");
String query = generateQueryIfApplicable(searchRequest, searchRequest.getQuery().get());
builder.queryParam("q", query)
.queryParam("apikey", config.getApiKey())
.queryParam("r", config.getApiKey())
.queryParam("i", config.getUsername())
;
return builder;
}

protected void completeIndexerSearchResult(Xml response, IndexerSearchResult indexerSearchResult, AcceptorResult acceptorResult, SearchRequest searchRequest, int offset, Integer limit) {
NewznabXmlResponse newznabResponse = ((NewznabXmlRoot) response).getRssChannel().getNewznabResponse();
super.completeIndexerSearchResult(response, indexerSearchResult, acceptorResult, searchRequest, offset, limit);

indexerSearchResult.setTotalResultsKnown(true);
if (newznabResponse != null) {
//Doesn't seem to support paging. No idea how big the page size is
indexerSearchResult.setTotalResultsKnown(true);
indexerSearchResult.setHasMoreResults(false);
indexerSearchResult.setTotalResults(newznabResponse.getTotal());
indexerSearchResult.setPageSize(newznabResponse.getTotal());
} else {
indexerSearchResult.setTotalResults(0);
indexerSearchResult.setHasMoreResults(false);
indexerSearchResult.setOffset(0);
indexerSearchResult.setPageSize(0);
}
}

@Override
protected boolean isSwitchToTSearchNeeded(SearchRequest request) {
return true;
}


@Component
@Order(100)
public static class NewznabHandlingStrategy implements IndexerHandlingStrategy<WtfNzb> {

@Override
public boolean handlesIndexerConfig(IndexerConfig config) {
boolean isIndexerWtfNzb = config != null && config.getSearchModuleType() == SearchModuleType.WTFNZB;
if (isIndexerWtfNzb) {
logger.debug("Will use special WtfNzb limit handling for indexer with host {}", config.getHost());
}
return isIndexerWtfNzb;
}

@Override
public String getName() {
return "WTFNZB";
}


}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public byte[] encode(ILoggingEvent event) {
}

protected String removeSensitiveData(String txt) {
return txt.replaceAll("(?i)(username|apikey|password)(=|:|%3D)([^&\\s]{2,})", "$1$2<$1>")
return txt.replaceAll("(?i)(r|username|apikey|password)(=|:|%3D)([^&\\s]{2,})", "$1$2<$1>")
//Format in requests to and responses from *arr:
/*
"name": "apiKey",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.nzbhydra.indexers.NzbIndex;
import org.nzbhydra.indexers.NzbIndexApi;
import org.nzbhydra.indexers.QueryGenerator;
import org.nzbhydra.indexers.WtfNzb;
import org.nzbhydra.indexers.status.IndexerLimitRepository;
import org.nzbhydra.indexers.torznab.Torznab;
import org.nzbhydra.mediainfo.InfoProvider;
Expand Down Expand Up @@ -88,6 +89,9 @@ public Indexer instantiateIndexer(String name) {
case "NEWZNAB" -> {
return new Newznab(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler);
}
case "WTFNZB" -> {
return new WtfNzb(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, unmarshaller, baseConfigHandler);
}
case "NZBINDEX" -> {
return new NzbIndex(configProvider, indexerRepository, searchResultRepository, indexerApiAccessRepository, indexerApiAccessShortRepository, indexerStatusRepository, indexerWebAccess, resultAcceptor, categoryProvider, infoProvider, eventPublisher, queryGenerator, titleMapping, baseConfigHandler);
}
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/resources/changelog.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#@formatter:off
- version: "v7.2.4"
- version: "v7.3.0"
date: "2024-07-13"
changes:
- type: "feature"
text: "Add emdash (—) to \"umlauts\" to be replaced."
text: "Add support for the appropriately named WtfNzb. Only query based searches are supported. See #275"
final: true
- version: "v7.2.3"
date: "2024-06-10"
Expand Down
65 changes: 56 additions & 9 deletions core/src/main/resources/static/js/nzbhydra.js
Original file line number Diff line number Diff line change
Expand Up @@ -3690,7 +3690,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
});
}

if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {
if (['WTFNZB', 'NEWZNAB', 'TORZNAB', 'JACKETT_CONFIG'].includes(indexerModel.searchModuleType)) {
var hostField = {
key: 'host',
type: 'horizontalInput',
Expand All @@ -3716,7 +3716,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
);
}

if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG' || indexerModel.searchModuleType === 'NZBINDEX_API') && indexerModel.host !== 'https://feed.animetosho.org') {
if (['WTFNZB', 'NEWZNAB', 'TORZNAB', 'JACKETT_CONFIG', 'NZBINDEX_API'].includes(indexerModel.searchModuleType) && indexerModel.host !== 'https://feed.animetosho.org') {
fieldset.push(
{
key: 'apiKey',
Expand All @@ -3735,7 +3735,8 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
}
)
}
if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {

if (['NEWZNAB', 'TORZNAB'].includes(indexerModel.searchModuleType)) {
fieldset.push(
{
key: 'apiPath',
Expand All @@ -3758,7 +3759,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
)
}

if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB' || indexerModel.searchModuleType === 'JACKETT_CONFIG') {
if (['NEWZNAB', 'TORZNAB', 'JACKETT_CONFIG'].includes(indexerModel.searchModuleType)) {
fieldset.push(
{
key: 'username',
Expand All @@ -3778,6 +3779,28 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
}
}
);
}

if ('WTFNZB' === indexerModel.searchModuleType) {
fieldset.push(
{
key: 'username',
type: 'horizontalInput',
templateOptions: {
type: 'text',
required: true,
label: 'Username',
help: 'See the API help on the website. Copy the user ID from the example API request where it says i=&lt;yourUserId&gt; (e.g. ABg4Cd==)'
},
watcher: {
listener: function (field, newValue, oldValue, scope) {
if (newValue !== oldValue) {
scope.$parent.needsConnectionTest = true;
}
}
}
}
);
fieldset.push(
{
key: 'password',
Expand All @@ -3793,6 +3816,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
)
}


if (indexerModel.searchModuleType !== 'JACKETT_CONFIG') {
fieldset.push(
{
Expand Down Expand Up @@ -3833,7 +3857,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
}
);

if (indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') {
if (['NEWZNAB', 'TORZNAB'].includes(indexerModel.searchModuleType)) {
fieldset.push(
{
key: 'hitLimit',
Expand Down Expand Up @@ -3928,7 +3952,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
})
}

if (indexerModel.searchModuleType === 'NEWZNAB') {
if (['NEWZNAB', 'TORZNAB', 'WTFNZB'].includes(indexerModel.searchModuleType)) {
fieldset.push(
{
key: 'userAgent',
Expand All @@ -3944,7 +3968,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
)
}

if (indexerModel.searchModuleType === 'NEWZNAB') {
if (['NEWZNAB', 'TORZNAB'].includes(indexerModel.searchModuleType)) {
fieldset.push(
{
key: 'customParameters',
Expand All @@ -3960,7 +3984,6 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
)
}


fieldset.push(
{
key: 'preselect',
Expand Down Expand Up @@ -4044,7 +4067,7 @@ function getIndexerBoxFields(indexerModel, parentModel, isInitial, CategoriesSer
}


if ((indexerModel.searchModuleType === 'NEWZNAB' || indexerModel.searchModuleType === 'TORZNAB') && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {
if ((['NEWZNAB', 'TORZNAB'].includes(indexerModel.searchModuleType)) && !isInitial && indexerModel.searchModuleType !== 'JACKETT_CONFIG') {
fieldset.push(
{
key: 'supportedSearchIds',
Expand Down Expand Up @@ -4479,6 +4502,30 @@ angular.module('nzbhydraApp').controller('IndexerConfigSelectionBoxInstanceContr
timeout: null,
searchModuleType: "NZBINDEX_API",
username: null
},
{
allCapsChecked: true,
enabledForSearchSource: "INTERNAL",
categories: [],
configComplete: true,
downloadLimit: null,
generalMinSize: 1,
hitLimit: null,
hitLimitResetTime: null,
host: null,
loadLimitOnRandom: null,
name: "WtfNzb",
password: null,
preselect: true,
score: 0,
showOnSearch: true,
state: "ENABLED",
supportedSearchIds: [],
supportedSearchTypes: [],
timeout: null,
searchModuleType: "WTFNZB",
username: null,
userAgent: null
}
];

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/static/js/nzbhydra.js.map

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions core/src/test/java/org/nzbhydra/mapping/WtfNzbMappingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.nzbhydra.mapping;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.nzbhydra.mapping.newznab.xml.NewznabXmlChannel;
import org.nzbhydra.mapping.newznab.xml.NewznabXmlRoot;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@ExtendWith(SpringExtension.class)
public class WtfNzbMappingTest {

@BeforeEach
public void setUp() throws Exception {

}


@Test
void testMappingFromXml() throws Exception {
NewznabXmlRoot rssRoot = getRssRootFromXml("wtfnzb.xml");
NewznabXmlChannel channel = rssRoot.getRssChannel();
assertThat(channel.getItems().get(0).getPubDate()).isEqualTo(Instant.ofEpochMilli(1720154595000L));
}


private NewznabXmlRoot getRssRootFromXml(String xmlFileName) throws IOException {
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
mockServer.expect(requestTo("/api")).andRespond(withSuccess(Resources.toString(Resources.getResource(WtfNzbMappingTest.class, xmlFileName), Charsets.UTF_8), MediaType.APPLICATION_XML));

return restTemplate.getForObject("/api", NewznabXmlRoot.class);
}


}
Loading

0 comments on commit f3b7b23

Please sign in to comment.