diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js index 98934e71c..2a7c7a6ee 100644 --- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js +++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js @@ -89,7 +89,6 @@ function EditIndexerModalContent(props) { type={inputTypes.CHECK} name="enable" helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')} - isDisabled={!supportsRss.value} {...enable} onChange={onInputChange} /> diff --git a/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs b/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs new file mode 100644 index 000000000..dd1ed9d45 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/BinSearch.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using AngleSharp.Html.Parser; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class BinSearch : UsenetIndexerBase + { + public override string Name => "BinSearch"; + public override string[] IndexerUrls => new string[] { "https://binsearch.info/" }; + public override string Description => "The binary Usenet search engine"; + public override string Language => "en-us"; + public override Encoding Encoding => Encoding.UTF8; + public override DownloadProtocol Protocol => DownloadProtocol.Usenet; + public override IndexerPrivacy Privacy => IndexerPrivacy.Public; + public override bool SupportsRss => false; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public BinSearch(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new BinSearchRequestGenerator() { Capabilities = Capabilities, Settings = Settings }; + } + + public override IParseIndexerResponse GetParser() + { + return new BinSearchParser(Capabilities.Categories, Settings); + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }; + + return caps; + } + } + + public class BinSearchRequestGenerator : IIndexerRequestGenerator + { + public IndexerCapabilities Capabilities { get; set; } + public BinSearchSettings Settings { get; set; } + + public BinSearchRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, SearchCriteriaBase searchCriteria) + { + var qc = new NameValueCollection + { + { "adv_col", "on" }, + { "postdate", "date" }, + { "adv_sort", "date" }, + { "q", term }, + { "m", searchCriteria.Offset.ToString() }, + { "max", searchCriteria.Limit?.ToString() ?? "100" } + }; + + var searchUrl = string.Format("{0}/?{1}", Settings.BaseUrl.TrimEnd('/'), qc.GetQueryString()); + + var request = new IndexerRequest(searchUrl, HttpAccept.Json); + + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria)); + + return pageableRequests; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class BinSearchParser : IParseIndexerResponse + { + private readonly IndexerCapabilitiesCategories _categories; + private readonly BinSearchSettings _settings; + + public BinSearchParser(IndexerCapabilitiesCategories categories, BinSearchSettings settings) + { + _categories = categories; + _settings = settings; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var releaseInfos = new List(); + + var parser = new HtmlParser(); + var doc = parser.ParseDocument(indexerResponse.Content); + var rows = doc.QuerySelectorAll("table.xMenuT > tbody > tr").Skip(1); + foreach (var row in rows) + { + var titleElement = row.QuerySelector("td > span.s"); + + if (titleElement == null) + { + continue; + } + + var parsedTitle = ParseTitleRegex.Match(titleElement.TextContent); + + if (!parsedTitle.Success || parsedTitle.Groups["title"].Value.IsNullOrWhiteSpace()) + { + continue; + } + + var guid = row.QuerySelector("input[type=checkbox]").GetAttribute("name"); + var publishDate = DateTimeUtil.FromUnknown(row.QuerySelector("td:nth-child(6)").TextContent); + var sizeElement = row.QuerySelector("td > span.d"); + var size = ParseSizeRegex.Match(sizeElement.TextContent); + var infoUrl = string.Format("{0}{1}", _settings.BaseUrl.TrimEnd('/'), row.QuerySelector("a").GetAttribute("href")); + var downloadUrl = string.Format("{0}/?action=nzb&{1}=1", _settings.BaseUrl.TrimEnd('/'), guid); + + var release = new ReleaseInfo + { + Guid = guid, + Title = parsedTitle.Groups["title"].Value, + Size = ReleaseInfo.GetBytes(string.Format("{0} {1}", size.Groups["size"].Value, size.Groups["unit"].Value)), + PublishDate = publishDate, + Categories = new List { NewznabStandardCategory.Other }, + InfoUrl = infoUrl, + DownloadUrl = downloadUrl + }; + + releaseInfos.Add(release); + } + + return releaseInfos.ToArray(); + } + + private static readonly Regex ParseTitleRegex = new Regex(@"\""(?.*)(?:\.(rar|nfo|mkv|par2|001|nzb|url|zip|r[0-9]{2}))\"""); + private static readonly Regex ParseSizeRegex = new Regex(@"size: (?<size>[0-9]+(\.[0-9]+)?).(?<unit>(GB|MB|KB|B))"); + + public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; } + } + + public class BinSearchSettings : IIndexerSettings + { + [FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")] + public string BaseUrl { get; set; } + + [FieldDefinition(2)] + public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings(); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(); + } + } +}