mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-25 22:59:10 -04:00
Cleanup Search UI, Newznab Caps API
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class Newznab : HttpIndexerBase<NewznabSettings>
|
||||
{
|
||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||
|
||||
public override string Name => "Newznab";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize;
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new NewznabRequestGenerator(_capabilitiesProvider)
|
||||
{
|
||||
PageSize = PageSize,
|
||||
Settings = Settings
|
||||
};
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new NewznabRssParser(Settings);
|
||||
}
|
||||
|
||||
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
|
||||
yield return GetDefinition("DrunkenSlug", GetSettings("https://api.drunkenslug.com"));
|
||||
yield return GetDefinition("Nzb-Tortuga", GetSettings("https://www.nzb-tortuga.com"));
|
||||
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
|
||||
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
|
||||
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", categories: new[] { 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090 }));
|
||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
||||
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
|
||||
yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me", categories: new[] { 2000, 2020, 2030, 2040, 2045, 2050, 2070 }));
|
||||
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
|
||||
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
|
||||
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
|
||||
yield return GetDefinition("Usenet Crawler", GetSettings("https://www.usenet-crawler.com"));
|
||||
}
|
||||
}
|
||||
|
||||
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, indexerStatusService, configService, logger)
|
||||
{
|
||||
_capabilitiesProvider = capabilitiesProvider;
|
||||
}
|
||||
|
||||
private IndexerDefinition GetDefinition(string name, NewznabSettings settings)
|
||||
{
|
||||
return new IndexerDefinition
|
||||
{
|
||||
EnableRss = false,
|
||||
EnableAutomaticSearch = false,
|
||||
EnableInteractiveSearch = false,
|
||||
Name = name,
|
||||
Implementation = GetType().Name,
|
||||
Settings = settings,
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Privacy = IndexerPrivacy.Private,
|
||||
SupportsRss = SupportsRss,
|
||||
SupportsSearch = SupportsSearch
|
||||
};
|
||||
}
|
||||
|
||||
private NewznabSettings GetSettings(string url, string apiPath = null, int[] categories = null)
|
||||
{
|
||||
var settings = new NewznabSettings { BaseUrl = url };
|
||||
|
||||
if (categories != null)
|
||||
{
|
||||
settings.Categories = categories;
|
||||
}
|
||||
|
||||
if (apiPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
settings.ApiPath = apiPath;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
base.Test(failures);
|
||||
if (failures.HasErrors())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
failures.AddIfNotNull(TestCapabilities());
|
||||
}
|
||||
|
||||
protected static List<int> CategoryIds(List<NewznabCategory> categories)
|
||||
{
|
||||
var l = categories.Select(c => c.Id).ToList();
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.Subcategories != null)
|
||||
{
|
||||
l.AddRange(CategoryIds(category.Subcategories));
|
||||
}
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
protected virtual ValidationFailure TestCapabilities()
|
||||
{
|
||||
try
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
var notSupported = Settings.Categories.Except(CategoryIds(capabilities.Categories));
|
||||
|
||||
if (notSupported.Any())
|
||||
{
|
||||
_logger.Warn($"{Definition.Name} does not support the following categories: {string.Join(", ", notSupported)}. You should probably remove them.");
|
||||
if (notSupported.Count() == Settings.Categories.Count())
|
||||
{
|
||||
return new ValidationFailure(string.Empty, $"This indexer does not support any of the selected categories! (You may need to turn on advanced settings to see them)");
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (capabilities.SupportedMovieSearchParameters != null &&
|
||||
new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) &&
|
||||
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ValidationFailure(string.Empty, "This indexer does not support searching for movies :(. Tell your indexer staff to enable this or force add the indexer by disabling search, adding the indexer and then enabling it again.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabCapabilities
|
||||
{
|
||||
public int DefaultPageSize { get; set; }
|
||||
public int MaxPageSize { get; set; }
|
||||
public string[] SupportedSearchParameters { get; set; }
|
||||
public string[] SupportedMovieSearchParameters { get; set; }
|
||||
public bool SupportsAggregateIdSearch { get; set; }
|
||||
public List<NewznabCategory> Categories { get; set; }
|
||||
|
||||
public NewznabCapabilities()
|
||||
{
|
||||
DefaultPageSize = 100;
|
||||
MaxPageSize = 100;
|
||||
SupportedSearchParameters = new[] { "q" };
|
||||
SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" };
|
||||
SupportsAggregateIdSearch = false;
|
||||
Categories = new List<NewznabCategory>();
|
||||
}
|
||||
}
|
||||
|
||||
public class NewznabCategory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<NewznabCategory> Subcategories { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public interface INewznabCapabilitiesProvider
|
||||
{
|
||||
NewznabCapabilities GetCapabilities(NewznabSettings settings);
|
||||
}
|
||||
|
||||
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
|
||||
{
|
||||
private readonly ICached<NewznabCapabilities> _capabilitiesCache;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NewznabCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_capabilitiesCache = cacheManager.GetCache<NewznabCapabilities>(GetType());
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings)
|
||||
{
|
||||
var key = indexerSettings.ToJson();
|
||||
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7));
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
private NewznabCapabilities FetchCapabilities(NewznabSettings indexerSettings)
|
||||
{
|
||||
var capabilities = new NewznabCapabilities();
|
||||
|
||||
var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/'));
|
||||
|
||||
if (indexerSettings.ApiKey.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
url += "&apikey=" + indexerSettings.ApiKey;
|
||||
}
|
||||
|
||||
var request = new HttpRequest(url, HttpAccept.Rss);
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = _httpClient.Get(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex, "Failed to get Newznab API capabilities from {0}", indexerSettings.BaseUrl);
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
capabilities = ParseCapabilities(response);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
ex.WithData(response, 128 * 1024);
|
||||
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
|
||||
_logger.Debug(ex, "Failed to parse newznab api capabilities for {0}", indexerSettings.BaseUrl);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.WithData(response, 128 * 1024);
|
||||
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
private NewznabCapabilities ParseCapabilities(HttpResponse response)
|
||||
{
|
||||
var capabilities = new NewznabCapabilities();
|
||||
|
||||
var xDoc = XDocument.Parse(response.Content);
|
||||
|
||||
if (xDoc == null)
|
||||
{
|
||||
throw new XmlException("Invalid XML").WithData(response);
|
||||
}
|
||||
|
||||
NewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
|
||||
|
||||
var xmlRoot = xDoc.Element("caps");
|
||||
|
||||
if (xmlRoot == null)
|
||||
{
|
||||
throw new XmlException("Unexpected XML").WithData(response);
|
||||
}
|
||||
|
||||
var xmlLimits = xmlRoot.Element("limits");
|
||||
if (xmlLimits != null)
|
||||
{
|
||||
capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value);
|
||||
capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value);
|
||||
}
|
||||
|
||||
var xmlSearching = xmlRoot.Element("searching");
|
||||
if (xmlSearching != null)
|
||||
{
|
||||
var xmlBasicSearch = xmlSearching.Element("search");
|
||||
if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes")
|
||||
{
|
||||
capabilities.SupportedSearchParameters = null;
|
||||
}
|
||||
else if (xmlBasicSearch.Attribute("supportedParams") != null)
|
||||
{
|
||||
capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(',');
|
||||
}
|
||||
|
||||
var xmlMovieSearch = xmlSearching.Element("movie-search");
|
||||
if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes")
|
||||
{
|
||||
capabilities.SupportedMovieSearchParameters = null;
|
||||
}
|
||||
else if (xmlMovieSearch.Attribute("supportedParams") != null)
|
||||
{
|
||||
capabilities.SupportedMovieSearchParameters = xmlMovieSearch.Attribute("supportedParams").Value.Split(',');
|
||||
capabilities.SupportsAggregateIdSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
var xmlCategories = xmlRoot.Element("categories");
|
||||
if (xmlCategories != null)
|
||||
{
|
||||
foreach (var xmlCategory in xmlCategories.Elements("category"))
|
||||
{
|
||||
var cat = new NewznabCategory
|
||||
{
|
||||
Id = int.Parse(xmlCategory.Attribute("id").Value),
|
||||
Name = xmlCategory.Attribute("name").Value,
|
||||
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty,
|
||||
Subcategories = new List<NewznabCategory>()
|
||||
};
|
||||
|
||||
foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
|
||||
{
|
||||
cat.Subcategories.Add(new NewznabCategory
|
||||
{
|
||||
Id = int.Parse(xmlSubcat.Attribute("id").Value),
|
||||
Name = xmlSubcat.Attribute("name").Value,
|
||||
Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
capabilities.Categories.Add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabException : IndexerException
|
||||
{
|
||||
public NewznabException(IndexerResponse response, string message, params object[] args)
|
||||
: base(response, message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public NewznabException(IndexerResponse response, string message)
|
||||
: base(response, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||
public int MaxPages { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public NewznabSettings Settings { get; set; }
|
||||
|
||||
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
|
||||
{
|
||||
_capabilitiesProvider = capabilitiesProvider;
|
||||
|
||||
MaxPages = 30;
|
||||
PageSize = 100;
|
||||
}
|
||||
|
||||
private bool SupportsSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedSearchParameters != null &&
|
||||
capabilities.SupportedSearchParameters.Contains("q");
|
||||
}
|
||||
}
|
||||
|
||||
private bool SupportsImdbSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedMovieSearchParameters != null &&
|
||||
capabilities.SupportedMovieSearchParameters.Contains("imdbid");
|
||||
}
|
||||
}
|
||||
|
||||
private bool SupportsTmdbSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedMovieSearchParameters != null &&
|
||||
capabilities.SupportedMovieSearchParameters.Contains("tmdbid");
|
||||
}
|
||||
}
|
||||
|
||||
private bool SupportsAggregatedIdSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportsAggregateIdSearch;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
// Some indexers might forget to enable movie search, but normal search still works fine. Thus we force a normal search.
|
||||
if (capabilities.SupportedMovieSearchParameters != null)
|
||||
{
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", ""));
|
||||
}
|
||||
else if (capabilities.SupportedSearchParameters != null)
|
||||
{
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", ""));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
AddMovieIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria);
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private void AddMovieIdPageableRequests(IndexerPageableRequestChain chain, int maxPages, IEnumerable<int> categories, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.TmdbId > 0;
|
||||
var includeImdbSearch = SupportsImdbSearch && searchCriteria.ImdbId.IsNotNullOrWhiteSpace();
|
||||
|
||||
if (SupportsAggregatedIdSearch && (includeTmdbSearch || includeImdbSearch))
|
||||
{
|
||||
var ids = "";
|
||||
|
||||
if (includeTmdbSearch)
|
||||
{
|
||||
ids += "&tmdbid=" + searchCriteria.TmdbId;
|
||||
}
|
||||
|
||||
if (includeImdbSearch)
|
||||
{
|
||||
ids += "&imdbid=" + searchCriteria.ImdbId.Substring(2);
|
||||
}
|
||||
|
||||
chain.Add(GetPagedRequests(maxPages, categories, "movie", ids));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (includeTmdbSearch)
|
||||
{
|
||||
chain.Add(GetPagedRequests(maxPages,
|
||||
categories,
|
||||
"movie",
|
||||
string.Format("&tmdbid={0}", searchCriteria.TmdbId)));
|
||||
}
|
||||
else if (includeImdbSearch)
|
||||
{
|
||||
chain.Add(GetPagedRequests(maxPages,
|
||||
categories,
|
||||
"movie",
|
||||
string.Format("&imdbid={0}", searchCriteria.ImdbId.Substring(2))));
|
||||
}
|
||||
}
|
||||
|
||||
if (SupportsSearch)
|
||||
{
|
||||
chain.AddTier();
|
||||
foreach (var queryTitle in searchCriteria.QueryTitles)
|
||||
{
|
||||
var searchQuery = queryTitle;
|
||||
|
||||
if (!Settings.RemoveYear)
|
||||
{
|
||||
searchQuery = string.Format("{0}", searchQuery);
|
||||
}
|
||||
|
||||
chain.Add(GetPagedRequests(MaxPages,
|
||||
Settings.Categories,
|
||||
"movie",
|
||||
string.Format("&q={0}", NewsnabifyTitle(searchQuery))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, IEnumerable<int> categories, string searchType, string parameters)
|
||||
{
|
||||
if (categories.Empty())
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var categoriesQuery = string.Join(",", categories.Distinct());
|
||||
|
||||
var baseUrl = string.Format("{0}{1}?t={2}&cat={3}&extended=1{4}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchType, categoriesQuery, Settings.AdditionalParameters);
|
||||
|
||||
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
baseUrl += "&apikey=" + Settings.ApiKey;
|
||||
}
|
||||
|
||||
if (PageSize == 0)
|
||||
{
|
||||
yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, parameters), HttpAccept.Rss);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var page = 0; page < maxPages; page++)
|
||||
{
|
||||
yield return new IndexerRequest(string.Format("{0}&offset={1}&limit={2}{3}", baseUrl, page * PageSize, PageSize, parameters), HttpAccept.Rss);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NewsnabifyTitle(string title)
|
||||
{
|
||||
return title.Replace("+", "%20");
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabRssParser : RssParser
|
||||
{
|
||||
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
|
||||
|
||||
private readonly NewznabSettings _settings;
|
||||
|
||||
public NewznabRssParser(NewznabSettings settings)
|
||||
{
|
||||
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
|
||||
UseEnclosureUrl = true;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
|
||||
{
|
||||
var error = xdoc.Descendants("error").FirstOrDefault();
|
||||
|
||||
if (error == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var code = Convert.ToInt32(error.Attribute("code").Value);
|
||||
var errorMessage = error.Attribute("description").Value;
|
||||
|
||||
if (code >= 100 && code <= 199)
|
||||
{
|
||||
throw new ApiKeyException(errorMessage);
|
||||
}
|
||||
|
||||
if (!indexerResponse.Request.Url.FullUri.Contains("apikey=") && (errorMessage == "Missing parameter" || errorMessage.Contains("apikey")))
|
||||
{
|
||||
throw new ApiKeyException("Indexer requires an API key");
|
||||
}
|
||||
|
||||
if (errorMessage == "Request limit reached")
|
||||
{
|
||||
throw new RequestLimitReachedException("API limit reached");
|
||||
}
|
||||
|
||||
throw new NewznabException(indexerResponse, errorMessage);
|
||||
}
|
||||
|
||||
protected override bool PreProcess(IndexerResponse indexerResponse)
|
||||
{
|
||||
var xdoc = LoadXmlDocument(indexerResponse);
|
||||
|
||||
CheckError(xdoc, indexerResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}, did you intend to add a Torznab indexer?", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Feed does not contain {0}, found {1}.", NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
|
||||
{
|
||||
releaseInfo = base.ProcessItem(item, releaseInfo);
|
||||
releaseInfo.ImdbId = GetImdbId(item);
|
||||
|
||||
return releaseInfo;
|
||||
}
|
||||
|
||||
protected override string GetInfoUrl(XElement item)
|
||||
{
|
||||
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
|
||||
}
|
||||
|
||||
protected override string GetCommentUrl(XElement item)
|
||||
{
|
||||
return ParseUrl(item.TryGetValue("comments"));
|
||||
}
|
||||
|
||||
protected override long GetSize(XElement item)
|
||||
{
|
||||
long size;
|
||||
|
||||
var sizeString = TryGetNewznabAttribute(item, "size");
|
||||
if (!sizeString.IsNullOrWhiteSpace() && long.TryParse(sizeString, out size))
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
size = GetEnclosureLength(item);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
protected override DateTime GetPublishDate(XElement item)
|
||||
{
|
||||
var dateString = TryGetNewznabAttribute(item, "usenetdate");
|
||||
if (!dateString.IsNullOrWhiteSpace())
|
||||
{
|
||||
return XElementExtensions.ParseDate(dateString);
|
||||
}
|
||||
|
||||
return base.GetPublishDate(item);
|
||||
}
|
||||
|
||||
protected override string GetDownloadUrl(XElement item)
|
||||
{
|
||||
var url = base.GetDownloadUrl(item);
|
||||
|
||||
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||
{
|
||||
url = ParseUrl((string)item.Element("enclosure").Attribute("url"));
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
protected virtual int GetImdbId(XElement item)
|
||||
{
|
||||
var imdbIdString = TryGetNewznabAttribute(item, "imdb");
|
||||
int imdbId;
|
||||
|
||||
if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out imdbId))
|
||||
{
|
||||
return imdbId;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual string GetImdbTitle(XElement item)
|
||||
{
|
||||
var imdbTitle = TryGetNewznabAttribute(item, "imdbtitle");
|
||||
if (!imdbTitle.IsNullOrWhiteSpace())
|
||||
{
|
||||
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(
|
||||
Parser.Parser.ReplaceGermanUmlauts(
|
||||
Parser.Parser.NormalizeTitle(imdbTitle).Replace(" ", ".")));
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
protected virtual int GetImdbYear(XElement item)
|
||||
{
|
||||
var imdbYearString = TryGetNewznabAttribute(item, "imdbyear");
|
||||
int imdbYear;
|
||||
|
||||
if (!imdbYearString.IsNullOrWhiteSpace() && int.TryParse(imdbYearString, out imdbYear))
|
||||
{
|
||||
return imdbYear;
|
||||
}
|
||||
|
||||
return 1900;
|
||||
}
|
||||
|
||||
protected string TryGetNewznabAttribute(XElement item, string key, string defaultValue = "")
|
||||
{
|
||||
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
if (attrElement != null)
|
||||
{
|
||||
var attrValue = attrElement.Attribute("value");
|
||||
if (attrValue != null)
|
||||
{
|
||||
return attrValue.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
|
||||
{
|
||||
private static readonly string[] ApiKeyWhiteList =
|
||||
{
|
||||
"nzbs.org",
|
||||
"nzb.su",
|
||||
"dognzb.cr",
|
||||
"nzbplanet.net",
|
||||
"nzbid.org",
|
||||
"nzbndx.com",
|
||||
"nzbindex.in"
|
||||
};
|
||||
|
||||
private static bool ShouldHaveApiKey(NewznabSettings settings)
|
||||
{
|
||||
if (settings.BaseUrl == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
|
||||
}
|
||||
|
||||
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
|
||||
|
||||
public NewznabSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c).Custom((c, context) =>
|
||||
{
|
||||
if (c.Categories.Empty())
|
||||
{
|
||||
context.AddFailure("'Categories' must be provided");
|
||||
}
|
||||
});
|
||||
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
||||
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
||||
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
|
||||
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
|
||||
}
|
||||
}
|
||||
|
||||
public class NewznabSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
|
||||
|
||||
public NewznabSettings()
|
||||
{
|
||||
ApiPath = "/api";
|
||||
Categories = new[] { 2000, 2010, 2020, 2030, 2035, 2040, 2045, 2050, 2060 };
|
||||
MultiLanguages = new List<int>();
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "URL")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
||||
public string ApiPath { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter), Label = "Multi Languages", HelpText = "What languages are normally in a multi release on this indexer?", Advanced = true)]
|
||||
public IEnumerable<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable all categories", Advanced = true)]
|
||||
public IEnumerable<int> Categories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||
public string AdditionalParameters { get; set; }
|
||||
|
||||
[FieldDefinition(6,
|
||||
Label = "Remove year from search string",
|
||||
HelpText = "Should Prowlarr remove the year after the title when searching this indexer?",
|
||||
Advanced = true,
|
||||
Type = FieldType.Checkbox)]
|
||||
public bool RemoveYear { get; set; }
|
||||
|
||||
// Field 8 is used by TorznabSettings MinimumSeeders
|
||||
// If you need to add another field here, update TorznabSettings as well and this comment
|
||||
public virtual NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user