diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index deb75de1e..ef687a922 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -160,5 +160,16 @@ namespace NzbDrone.Common.Extensions { return new HashSet(source, comparer); } + + public static T FirstIfSingleOrDefault(this IEnumerable source, T replace = default) + { + if (source is ICollection collection) + { + return collection.Count == 1 ? collection.First() : replace; + } + + var test = source.Take(2).ToList(); + return test.Count == 1 ? test[0] : replace; + } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/BitHDTV.cs b/src/NzbDrone.Core/Indexers/Definitions/BitHDTV.cs new file mode 100644 index 000000000..c457e9021 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/BitHDTV.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web; +using AngleSharp.Html.Parser; +using FluentValidation; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class BitHDTV : TorrentIndexerBase + { + public override string Name => "BitHDTV"; + public override string[] IndexerUrls => new string[] { "https://www.bit-hdtv.com/" }; + public override string Description => "BIT-HDTV - Home of High Definition"; + public override string Language => "en-US"; + public override Encoding Encoding => Encoding.GetEncoding("iso-8859-1"); + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public BitHDTV(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new BitHDTVRequestGenerator() { Settings = Settings, Capabilities = Capabilities }; + } + + public override IParseIndexerResponse GetParser() + { + return new BitHDTVParser(Settings, Capabilities.Categories); + } + + protected override IDictionary GetCookies() + { + return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); + } + + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) + { + return false; + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId + }, + MovieSearchParams = new List + { + MovieSearchParam.Q, MovieSearchParam.ImdbId + } + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "Anime"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.MoviesBluRay, "Movies/Blu-ray"); + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVDocumentary, "Documentaries"); + caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.AudioLossless, "HQ Audio"); + caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Movies, "Movies"); + caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.AudioVideo, "Music Videos"); + caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.Other, "Other"); + caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVSport, "Sports"); + caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.TV, "TV/Seasonpack"); + caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.XXX, "XXX"); + + return caps; + } + } + + public class BitHDTVRequestGenerator : IIndexerRequestGenerator + { + public BitHDTVSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + + public BitHDTVRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, int[] categories, string imdbId = null) + { + var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/')); + + var qc = new NameValueCollection + { + { "cat", Capabilities.Categories.MapTorznabCapsToTrackers(categories, true).FirstIfSingleOrDefault("0") } + }; + + var search = new UriBuilder(searchUrl); + + if (imdbId.IsNotNullOrWhiteSpace()) + { + qc.Add("search", imdbId); + qc.Add("options", "4"); //Search URL field for IMDB link + search.Query = qc.GetQueryString(); + yield return new IndexerRequest(search.ToString(), HttpAccept.Html); + + qc["Options"] = "1"; //Search Title and Description + search.Query = qc.GetQueryString(); + yield return new IndexerRequest(search.ToString(), HttpAccept.Html); + } + else + { + //Site handles empty string on search param. No need to check for IsNullOrEmpty() + qc.Add("search", term); + qc.Add("options", "0"); //Search Title Only + search.Query = qc.GetQueryString(); + yield return new IndexerRequest(search.ToString(), HttpAccept.Html); + } + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class BitHDTVParser : IParseIndexerResponse + { + private readonly BitHDTVSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + + public BitHDTVParser(BitHDTVSettings settings, IndexerCapabilitiesCategories categories) + { + _settings = settings; + _categories = categories; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + foreach (var child in dom.QuerySelectorAll("#needseed")) + { + child.Remove(); + } + + var table = dom.QuerySelector("table[align=center] + br + table > tbody"); + + // No results, so skip this search + if (table == null) + { + return torrentInfos; + } + + foreach (var row in table.Children.Skip(1)) + { + var release = new TorrentInfo(); + var qLink = row.Children[2].QuerySelector("a"); + release.MinimumRatio = 1; + release.MinimumSeedTime = 172800; // 48 hours + release.Title = qLink.GetAttribute("title"); + var detailsLink = new Uri(qLink.GetAttribute("href")); + + //Skip irrelevant and duplicate entries + if (torrentInfos.Any(r => r.Guid == detailsLink.AbsoluteUri)) + { + continue; + } + + release.Files = ParseUtil.CoerceInt(row.Children[3].TextContent); + release.Grabs = ParseUtil.CoerceInt(row.Children[7].TextContent); + release.Guid = detailsLink.AbsoluteUri; + release.InfoUrl = release.Guid; + release.DownloadUrl = new Uri(_settings.BaseUrl + row.QuerySelector("a[href^=\"download.php\"]").GetAttribute("href")).AbsoluteUri; + var catUrl = new Uri(_settings.BaseUrl + row.Children[1].FirstElementChild.GetAttribute("href")); + var catQuery = HttpUtility.ParseQueryString(catUrl.Query); + var catNum = catQuery["cat"]; + release.Categories = _categories.MapTrackerCatToNewznab(catNum); + + var dateString = row.Children[5].TextContent.Trim(); + var pubDate = DateTime.ParseExact(dateString, "yyyy-MM-ddHH:mm:ss", CultureInfo.InvariantCulture); + release.PublishDate = DateTime.SpecifyKind(pubDate, DateTimeKind.Local); + var sizeStr = row.Children[6].TextContent; + release.Size = ParseUtil.GetBytes(sizeStr); + release.Seeders = ParseUtil.CoerceInt(row.Children[8].TextContent.Trim()); + release.Peers = ParseUtil.CoerceInt(row.Children[9].TextContent.Trim()) + release.Seeders; + switch (row.GetAttribute("bgcolor")) + { + case "#DDDDDD": + release.DownloadVolumeFactor = 1; + release.UploadVolumeFactor = 2; + break; + case "#FFFF99": + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 1; + break; + case "#CCFF99": + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 2; + break; + default: + release.DownloadVolumeFactor = 1; + release.UploadVolumeFactor = 1; + break; + } + + torrentInfos.Add(release); + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class BitHDTVSettingsValidator : AbstractValidator + { + public BitHDTVSettingsValidator() + { + RuleFor(c => c.Cookie).NotEmpty(); + } + } + + public class BitHDTVSettings : IIndexerSettings + { + private static readonly BitHDTVSettingsValidator Validator = new BitHDTVSettingsValidator(); + + public BitHDTVSettings() + { + Cookie = ""; + } + + [FieldDefinition(1, Label = "Base Url", HelpText = "Select which baseurl Prowlarr will use for requests to the site", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls")] + public string BaseUrl { get; set; } + + [FieldDefinition(2, Label = "Cookie", HelpText = "Login cookie from website")] + public string Cookie { get; set; } + + [FieldDefinition(3)] + public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings(); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}