From ce2f322478cfd7b3e13f19d5f7a39f87e0c7fa08 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 22 Jan 2023 14:40:44 +0200 Subject: [PATCH] New: Add Anidex --- .../Indexers/Definitions/Anidex.cs | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Anidex.cs diff --git a/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs b/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs new file mode 100644 index 000000000..3c93129c6 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Anidex.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +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.Indexers.Exceptions; +using NzbDrone.Core.Indexers.Settings; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Anidex : TorrentIndexerBase + { + public override string Name => "Anidex"; + public override string[] IndexerUrls => new[] { "https://anidex.info/" }; + public override string Description => "Anidex is a Public torrent tracker and indexer, primarily for English fansub groups of anime"; + public override string Language => "en-US"; + public override Encoding Encoding => Encoding.UTF8; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Public; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Anidex(IIndexerHttpClient httpClient, + IEventAggregator eventAggregator, + IIndexerStatusService indexerStatusService, + IConfigService configService, + Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new AnidexRequestGenerator(Settings, Capabilities); + } + + public override IParseIndexerResponse GetParser() + { + return new AnidexParser(Settings, Capabilities.Categories); + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.TVAnime, "Anime - Sub"); + caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVAnime, "Anime - Raw"); + caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TVAnime, "Anime - Dub"); + caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.TVAnime, "LA - Sub"); + caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.TVAnime, "LA - Raw"); + caps.Categories.AddCategoryMapping("6", NewznabStandardCategory.BooksEBook, "Light Novel"); + caps.Categories.AddCategoryMapping("7", NewznabStandardCategory.BooksComics, "Manga - TLed"); + caps.Categories.AddCategoryMapping("8", NewznabStandardCategory.BooksComics, "Manga - Raw"); + caps.Categories.AddCategoryMapping("9", NewznabStandardCategory.AudioMP3, "♫ - Lossy"); + caps.Categories.AddCategoryMapping("10", NewznabStandardCategory.AudioLossless, "♫ - Lossless"); + caps.Categories.AddCategoryMapping("11", NewznabStandardCategory.AudioVideo, "♫ - Video"); + caps.Categories.AddCategoryMapping("12", NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping("13", NewznabStandardCategory.PC0day, "Applications"); + caps.Categories.AddCategoryMapping("14", NewznabStandardCategory.XXXImageSet, "Pictures"); + caps.Categories.AddCategoryMapping("15", NewznabStandardCategory.XXX, "Adult Video"); + caps.Categories.AddCategoryMapping("16", NewznabStandardCategory.Other, "Other"); + + return caps; + } + } + + public class AnidexRequestGenerator : IIndexerRequestGenerator + { + private readonly AnidexSettings _settings; + private readonly IndexerCapabilities _capabilities; + + public AnidexRequestGenerator(AnidexSettings settings, IndexerCapabilities capabilities) + { + _settings = settings; + _capabilities = capabilities; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories)); + + return pageableRequests; + } + + private IEnumerable GetPagedRequests(string term, int[] categories) + { + var parameters = new NameValueCollection + { + { "page", "search" }, + { "s", "upload_timestamp" }, + { "o", "desc" }, + { "group_id", "0" }, // All groups + { "q", term ?? string.Empty } + }; + + if (_settings.AuthorisedOnly) + { + parameters.Add("a", "1"); + } + + var searchUrl = $"{_settings.BaseUrl}?{parameters.GetQueryString()}"; + + var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories); + + if (queryCats.Any()) + { + searchUrl += "&id=" + string.Join(",", queryCats); + } + + if (_settings.LanguagesOnly.Any()) + { + searchUrl += "&lang_id=" + string.Join(",", _settings.LanguagesOnly); + } + + var request = new IndexerRequest(searchUrl, HttpAccept.Html); + + yield return request; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class AnidexParser : IParseIndexerResponse + { + private readonly AnidexSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + + public AnidexParser(AnidexSettings settings, IndexerCapabilitiesCategories categories) + { + _settings = settings; + _categories = categories; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, $"Anidex search returned unexpected result. Expected 200 OK but got {indexerResponse.HttpResponse.StatusCode}."); + } + + var releaseInfos = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + + var rows = dom.QuerySelectorAll("div#content table > tbody > tr"); + foreach (var row in rows) + { + var downloadUrl = _settings.BaseUrl + row.QuerySelector("a[href^=\"/dl/\"]")?.GetAttribute("href"); + var infoUrl = _settings.BaseUrl + row.QuerySelector("td:nth-child(3) a")?.GetAttribute("href"); + + var title = row.QuerySelector("td:nth-child(3) span")?.GetAttribute("title")?.Trim(); + var language = row.QuerySelector("td:nth-child(1) img")?.GetAttribute("title")?.Trim(); + + if (language.IsNotNullOrWhiteSpace()) + { + title += $" [{language}]"; + } + + var categoryLink = row.QuerySelector("td:nth-child(1) a").GetAttribute("href"); + var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "id"); + var categories = _categories.MapTrackerCatToNewznab(cat); + + var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)")?.TextContent); + var peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(10)")?.TextContent.Trim()); + + var added = row.QuerySelector("td:nth-child(8)").GetAttribute("title").Trim(); + + var release = new TorrentInfo + { + Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = downloadUrl, + MagnetUrl = row.QuerySelector("a[href^=\"magnet:?\"]")?.GetAttribute("href"), + Title = title, + Categories = categories, + Seeders = seeders, + Peers = peers, + Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(7)")?.TextContent.Trim()), + Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(11)")?.TextContent), + PublishDate = DateTime.ParseExact(added, "yyyy-MM-dd HH:mm:ss UTC", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1 + }; + + releaseInfos.Add(release); + } + + return releaseInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class AnidexSettings : NoAuthTorrentBaseSettings + { + public AnidexSettings() + { + AuthorisedOnly = false; + LanguagesOnly = Array.Empty(); + } + + [FieldDefinition(2, Label = "Authorised Only", Type = FieldType.Checkbox, HelpText = "Search authorised torrents only")] + public bool AuthorisedOnly { get; set; } + + [FieldDefinition(3, Label = "Languages Only", Type = FieldType.Select, SelectOptions = typeof(AnidexLanguage), HelpText = "Search selected languages only. None ticked = ALL.")] + public IEnumerable LanguagesOnly { get; set; } + } + + public enum AnidexLanguage + { + [FieldOption(Hint = "English")] + GB = 1, + + [FieldOption(Hint = "Japanese")] + JP = 2, + + [FieldOption(Hint = "Polish")] + PL = 3, + + [FieldOption(Hint = "Serbo-Croatian")] + RS = 4, + + [FieldOption(Hint = "Dutch")] + NL = 5, + + [FieldOption(Hint = "Italian")] + IT = 6, + + [FieldOption(Hint = "Russian")] + RU = 7, + + [FieldOption(Hint = "German")] + DE = 8, + + [FieldOption(Hint = "Hungarian")] + HU = 9, + + [FieldOption(Hint = "French")] + FR = 10, + + [FieldOption(Hint = "Finnish")] + FI = 11, + + [FieldOption(Hint = "Vietnamese")] + VN = 12, + + [FieldOption(Hint = "Greek")] + GR = 13, + + [FieldOption(Hint = "Bulgarian")] + BG = 14, + + [FieldOption(Hint = "Spanish (Spain)")] + ES = 15, + + [FieldOption(Hint = "Portuguese (Brazil)")] + BR = 16, + + [FieldOption(Hint = "Portuguese (Portugal)")] + PT = 17, + + [FieldOption(Hint = "Swedish")] + SE = 18, + + [FieldOption(Hint = "Arabic")] + SA = 19, + + [FieldOption(Hint = "Danish")] + DK = 20, + + [FieldOption(Hint = "Chinese (Simplified)")] + CN = 21, + + [FieldOption(Hint = "Bengali")] + BD = 22, + + [FieldOption(Hint = "Romanian")] + RO = 23, + + [FieldOption(Hint = "Czech")] + CZ = 24, + + [FieldOption(Hint = "Mongolian")] + MN = 25, + + [FieldOption(Hint = "Turkish")] + TR = 26, + + [FieldOption(Hint = "Indonesian")] + ID = 27, + + [FieldOption(Hint = "Korean")] + KR = 28, + + [FieldOption(Hint = "Spanish (LATAM)")] + MX = 29, + + [FieldOption(Hint = "Persian")] + IR = 30, + + [FieldOption(Hint = "Malaysian")] + MY = 31, + } +}