diff --git a/core/src/main/java/org/nzbhydra/api/ExternalApi.java b/core/src/main/java/org/nzbhydra/api/ExternalApi.java index 97acb75d9..79ae76d52 100644 --- a/core/src/main/java/org/nzbhydra/api/ExternalApi.java +++ b/core/src/main/java/org/nzbhydra/api/ExternalApi.java @@ -16,17 +16,21 @@ import org.nzbhydra.downloading.DownloadResult; import org.nzbhydra.downloading.FileHandler; import org.nzbhydra.downloading.InvalidSearchResultIdException; +import org.nzbhydra.indexers.DetailsResult; import org.nzbhydra.logging.LoggingMarkers; import org.nzbhydra.mapping.newznab.ActionAttribute; import org.nzbhydra.mapping.newznab.NewznabParameters; import org.nzbhydra.mapping.newznab.NewznabResponse; import org.nzbhydra.mapping.newznab.OutputType; +import org.nzbhydra.mapping.newznab.json.NewznabJsonError; import org.nzbhydra.mapping.newznab.xml.NewznabXmlError; import org.nzbhydra.mediainfo.Imdb; import org.nzbhydra.searching.CategoryProvider; import org.nzbhydra.searching.CustomQueryAndTitleMappingHandler; +import org.nzbhydra.searching.DetailsProvider; import org.nzbhydra.searching.SearchResult; import org.nzbhydra.searching.Searcher; +import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; import org.nzbhydra.searching.searchrequests.SearchRequest; import org.nzbhydra.searching.searchrequests.SearchRequestFactory; import org.nzbhydra.springnative.ReflectionMarker; @@ -52,6 +56,7 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Comparator; +import java.util.List; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; @@ -99,6 +104,8 @@ public class ExternalApi { //When enabled search results will be mocked instead of indexers actually being searched. Only for configuration of external tools private static boolean inMockingMode; + @Autowired + private DetailsProvider detailsProvider; /** * External API call. @@ -139,6 +146,27 @@ public ResponseEntity api(NewznabParameters params, @PathVaria if (params.getT() == ActionAttribute.CAPS) { return capsGenerator.getCaps(params.getO(), searchType); } + if (params.getT() == ActionAttribute.DETAILS) { + DetailsResult details = detailsProvider.getDetails(params.getId()); + NewznabResponse response; + + List searchResultItems = Collections.singletonList(details.getSearchResultItem()); + if (details.isSuccessful()) { + boolean isNzb = details.getSearchResultItem().getDownloadType() == DownloadType.NZB; + if (params.getO() == OutputType.JSON) { + response = newznabJsonTransformer.transformToRoot(searchResultItems, 0, 0, isNzb); + } else { + response = newznabXmlTransformer.getRssRoot(searchResultItems, 0, 0, isNzb); + } + } else { + if (params.getO() == OutputType.JSON) { + response = new NewznabJsonError("100", details.getErrorMessage()); + } else { + response = new NewznabXmlError("100", details.getErrorMessage()); + } + } + return new ResponseEntity<>(response, null, HttpStatus.OK); + } if (Stream.of(ActionAttribute.SEARCH, ActionAttribute.BOOK, ActionAttribute.TVSEARCH, ActionAttribute.MOVIE).anyMatch(x -> x == params.getT())) { if (inMockingMode) { @@ -344,7 +372,7 @@ private SearchRequest buildBaseSearchRequest(NewznabParameters params, int searc @Data -@ReflectionMarker + @ReflectionMarker @AllArgsConstructor private static class CacheEntryValue { private final NewznabParameters params; @@ -361,8 +389,8 @@ public boolean equals(Object o) { } CacheEntryValue that = (CacheEntryValue) o; return com.google.common.base.Objects.equal(params, that.params) && - com.google.common.base.Objects.equal(lastUpdate, that.lastUpdate) && - com.google.common.base.Objects.equal(searchResult, that.searchResult); + com.google.common.base.Objects.equal(lastUpdate, that.lastUpdate) && + com.google.common.base.Objects.equal(searchResult, that.searchResult); } @Override diff --git a/core/src/main/java/org/nzbhydra/indexers/DetailsResult.java b/core/src/main/java/org/nzbhydra/indexers/DetailsResult.java new file mode 100644 index 000000000..2ecbc20f9 --- /dev/null +++ b/core/src/main/java/org/nzbhydra/indexers/DetailsResult.java @@ -0,0 +1,29 @@ +package org.nzbhydra.indexers; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.nzbhydra.searching.dtoseventsenums.SearchResultItem; +import org.nzbhydra.springnative.ReflectionMarker; + +import java.io.Serializable; + +@Data +@ReflectionMarker +@AllArgsConstructor +public class DetailsResult implements Serializable { + + private boolean successful; + private SearchResultItem searchResultItem; + private String errorMessage; + + + public static DetailsResult unsuccessful(String error) { + return new DetailsResult(false, null, error); + } + + public static DetailsResult withItem(SearchResultItem item) { + return new DetailsResult(true, item, null); + } + + +} diff --git a/core/src/main/java/org/nzbhydra/indexers/Indexer.java b/core/src/main/java/org/nzbhydra/indexers/Indexer.java index 1536c39e6..61df4c065 100644 --- a/core/src/main/java/org/nzbhydra/indexers/Indexer.java +++ b/core/src/main/java/org/nzbhydra/indexers/Indexer.java @@ -267,6 +267,10 @@ protected IndexerSearchResult searchInternal(SearchRequest searchRequest, int of public abstract NfoResult getNfo(String guid); + public DetailsResult getDetails(String guid) throws IndexerAccessException { + return DetailsResult.unsuccessful("Unsupported"); + } + //May be overwritten by specific indexer implementations protected String cleanupQuery(String query) { return query; diff --git a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessType.java b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessType.java index 709d0c274..d970326d7 100644 --- a/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessType.java +++ b/core/src/main/java/org/nzbhydra/indexers/IndexerApiAccessType.java @@ -3,5 +3,6 @@ public enum IndexerApiAccessType { NFO, NZB, - SEARCH + SEARCH, + DETAILS } diff --git a/core/src/main/java/org/nzbhydra/indexers/Newznab.java b/core/src/main/java/org/nzbhydra/indexers/Newznab.java index 13c85a016..bd79530be 100644 --- a/core/src/main/java/org/nzbhydra/indexers/Newznab.java +++ b/core/src/main/java/org/nzbhydra/indexers/Newznab.java @@ -425,6 +425,26 @@ public NfoResult getNfo(String guid) { } } + @Override + public DetailsResult getDetails(String guid) throws IndexerAccessException { + UriComponentsBuilder baseUri = getBaseUri().queryParam("t", "details").queryParam("id", guid); + Xml xml = null; + try { + xml = getAndStoreResultToDatabase(baseUri.build().toUri(), IndexerApiAccessType.DETAILS); + } catch (IndexerAccessException e) { + return DetailsResult.unsuccessful(e.getMessage()); + } + if (xml instanceof NewznabXmlError) { + handleRssError((NewznabXmlError) xml, baseUri.toUriString()); + } + NewznabXmlRoot rssRoot = (NewznabXmlRoot) xml; + List searchResultItems = getSearchResultItems(rssRoot, new SearchRequest()); + if (searchResultItems.size() != 1) { + return DetailsResult.unsuccessful("Didn't find exactly one result for ID"); + } + return DetailsResult.withItem(searchResultItems.get(0)); + } + protected void handleRssError(NewznabXmlError response, String url) throws IndexerAccessException { if (Stream.of("100", "101", "102").anyMatch(x -> x.equals(response.getCode())) && !(response.getDescription() != null && response.getDescription().contains("Hits Limit Reached"))) { throw new IndexerAuthException(String.format("Indexer refused authentication. Error code: %s. Description: %s", response.getCode(), response.getDescription())); diff --git a/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java b/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java index f45079911..0f97a76b5 100644 --- a/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java +++ b/core/src/main/java/org/nzbhydra/searching/CategoryProvider.java @@ -169,7 +169,7 @@ public Category fromSearchNewznabCategories(List cats, Category default * @return The matching configured category or "All" if none is found */ public Category fromResultNewznabCategories(List cats) { - if (cats == null || cats.size() == 0) { + if (cats == null || cats.isEmpty()) { logger.debug(LoggingMarkers.CATEGORY_MAPPING, "Empty newznab categories -> N/A"); return naCategory; } diff --git a/core/src/main/java/org/nzbhydra/searching/DetailsProvider.java b/core/src/main/java/org/nzbhydra/searching/DetailsProvider.java new file mode 100644 index 000000000..2c134375b --- /dev/null +++ b/core/src/main/java/org/nzbhydra/searching/DetailsProvider.java @@ -0,0 +1,51 @@ +/* + * (C) Copyright 2024 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.searching; + +import org.nzbhydra.indexers.DetailsResult; +import org.nzbhydra.indexers.Indexer; +import org.nzbhydra.indexers.exceptions.IndexerAccessException; +import org.nzbhydra.searching.db.SearchResultEntity; +import org.nzbhydra.searching.db.SearchResultRepository; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class DetailsProvider { + + private final SearchResultRepository searchResultRepository; + private final SearchModuleProvider searchModuleProvider; + + public DetailsProvider(SearchResultRepository searchResultRepository, SearchModuleProvider searchModuleProvider) { + this.searchResultRepository = searchResultRepository; + this.searchModuleProvider = searchModuleProvider; + } + + public DetailsResult getDetails(String resultId) { + Optional searchResult = searchResultRepository.findById(Long.parseLong(resultId)); + if (searchResult.isEmpty()) { + return null; + } + Indexer indexer = searchModuleProvider.getIndexerByName(searchResult.get().getIndexer().getName()); + try { + return indexer.getDetails(searchResult.get().getIndexerGuid()); + } catch (IndexerAccessException e) { + return DetailsResult.unsuccessful(e.getMessage()); + } + } +} diff --git a/core/src/main/resources/changelog.yaml b/core/src/main/resources/changelog.yaml index 3ea33f119..054ee49fc 100644 --- a/core/src/main/resources/changelog.yaml +++ b/core/src/main/resources/changelog.yaml @@ -1,4 +1,8 @@ #@formatter:off +- version: "v6.2.0" + changes: + - type: "feature" + text: "Support DETAILS function from Newznab API spec. See #942" - version: "v6.1.1" changes: - type: "fix"