using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using AngleSharp.Dom; using AngleSharp.Html.Parser; using Jackett.Common.Extensions; using Jackett.Common.Models; using Jackett.Common.Models.IndexerConfig.Bespoke; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using Newtonsoft.Json.Linq; using NLog; namespace Jackett.Common.Indexers { [ExcludeFromCodeCoverage] public class AnimeTorrents : IndexerBase { public override string Id => "animetorrents"; public override string Name => "AnimeTorrents"; public override string Description => "Definitive source for anime and manga"; public override string SiteLink { get; protected set; } = "https://animetorrents.me/"; public override string Language => "en-US"; public override string Type => "private"; public override bool SupportsPagination => true; public override TorznabCapabilities TorznabCaps => SetCapabilities(); private string LoginUrl => SiteLink + "login.php"; private string SearchUrl => SiteLink + "ajax/torrents_data.php"; private string SearchUrlReferer => SiteLink + "torrents.php?cat=0&searchin=filename&search="; private new ConfigurationDataAnimeTorrents configData => (ConfigurationDataAnimeTorrents)base.configData; public AnimeTorrents(IIndexerConfigurationService configService, WebClient c, Logger l, IProtectionService ps, ICacheService cs) : base(configService: configService, client: c, logger: l, p: ps, cacheService: cs, configData: new ConfigurationDataAnimeTorrents()) { } private TorznabCapabilities SetCapabilities() { var caps = new TorznabCapabilities { TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep }, MovieSearchParams = new List { MovieSearchParam.Q } }; caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesSD, "Anime Movie"); caps.Categories.AddCategoryMapping(6, TorznabCatType.MoviesHD, "Anime Movie HD"); caps.Categories.AddCategoryMapping(2, TorznabCatType.TVAnime, "Anime Series"); caps.Categories.AddCategoryMapping(7, TorznabCatType.TVAnime, "Anime Series HD"); caps.Categories.AddCategoryMapping(5, TorznabCatType.XXXDVD, "Hentai (censored)"); caps.Categories.AddCategoryMapping(9, TorznabCatType.XXXDVD, "Hentai (censored) HD"); caps.Categories.AddCategoryMapping(4, TorznabCatType.XXXDVD, "Hentai (un-censored)"); caps.Categories.AddCategoryMapping(8, TorznabCatType.XXXDVD, "Hentai (un-censored) HD"); caps.Categories.AddCategoryMapping(13, TorznabCatType.BooksForeign, "Light Novel"); caps.Categories.AddCategoryMapping(3, TorznabCatType.BooksComics, "Manga"); caps.Categories.AddCategoryMapping(10, TorznabCatType.BooksComics, "Manga 18+"); caps.Categories.AddCategoryMapping(11, TorznabCatType.TVAnime, "OVA"); caps.Categories.AddCategoryMapping(12, TorznabCatType.TVAnime, "OVA HD"); caps.Categories.AddCategoryMapping(14, TorznabCatType.BooksComics, "Doujin Anime"); caps.Categories.AddCategoryMapping(15, TorznabCatType.XXXDVD, "Doujin Anime 18+"); caps.Categories.AddCategoryMapping(16, TorznabCatType.AudioForeign, "Doujin Music"); caps.Categories.AddCategoryMapping(17, TorznabCatType.BooksComics, "Doujinshi"); caps.Categories.AddCategoryMapping(18, TorznabCatType.BooksComics, "Doujinshi 18+"); caps.Categories.AddCategoryMapping(19, TorznabCatType.Audio, "OST"); return caps; } public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); var pairs = new Dictionary { { "username", configData.Username.Value }, { "password", configData.Password.Value }, { "form", "login" }, { "rememberme[]", "1" } }; var loginPage = await RequestWithCookiesAndRetryAsync(LoginUrl, "", RequestType.GET, LoginUrl); var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true); await ConfigureIfOK(result.Cookies, result.ContentString != null && result.ContentString.Contains("logout.php"), () => { var parser = new HtmlParser(); using var dom = parser.ParseDocument(result.ContentString); var errorMessage = dom.QuerySelector(".ui-state-error").Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); return IndexerConfigurationStatus.RequiresTesting; } protected override async Task> PerformQuery(TorznabQuery query) { var releases = new List(); var searchString = query.GetQueryString().Trim(); // replace non-word characters with % (wildcard) searchString = Regex.Replace(searchString, @"[\W]+", "%"); var searchUrl = SearchUrl; var page = query.Limit > 0 && query.Offset > 0 ? (int)(query.Offset / query.Limit) + 1 : 1; var queryCollection = new NameValueCollection { { "total", "146" }, // Assuming the total number of pages { "cat", MapTorznabCapsToTrackers(query).FirstIfSingleOrDefault("0") }, { "page", page.ToString() }, { "searchin", "filename" }, { "search", searchString } }; if (configData.DownloadableOnly.Value) { queryCollection.Set("dlable", "1"); } searchUrl += "?" + queryCollection.GetQueryString(); var extraHeaders = new Dictionary { { "X-Requested-With", "XMLHttpRequest" } }; var response = await RequestWithCookiesAndRetryAsync(searchUrl, referer: SearchUrlReferer, headers: extraHeaders); var results = response.ContentString; try { var parser = new HtmlParser(); using var dom = parser.ParseDocument(results); var rows = dom.QuerySelectorAll("table tr"); foreach (var (row, index) in rows.Skip(1).Select((v, i) => (v, i))) { var downloadVolumeFactor = row.QuerySelector("img[alt=\"Gold Torrent\"]") != null ? 0 : row.QuerySelector("img[alt=\"Silver Torrent\"]") != null ? 0.5 : 1; // skip non-freeleech results when freeleech only is set if (configData.FreeleechOnly.Value && downloadVolumeFactor != 0) { continue; } var qTitleLink = row.QuerySelector("td:nth-of-type(2) a:nth-of-type(1)"); var title = qTitleLink?.TextContent.Trim(); // If we search and get no results, we still get a table just with no info. if (title.IsNullOrWhiteSpace()) { break; } var infoUrl = qTitleLink?.GetAttribute("href"); // newbie users don't see DL links // use details link as placeholder // skipping the release prevents newbie users from adding the tracker (empty result) var downloadUrl = row.QuerySelector("td:nth-of-type(3) a")?.GetAttribute("href") ?? infoUrl; var connections = row.QuerySelector("td:nth-of-type(8)").TextContent.Trim().Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var seeders = ParseUtil.CoerceInt(connections[0]); var categoryLink = row.QuerySelector("td:nth-of-type(1) a")?.GetAttribute("href") ?? string.Empty; var categoryId = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat"); var publishedDate = DateTime.ParseExact(row.QuerySelector("td:nth-of-type(5)").TextContent, "dd MMM yy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); if (publishedDate.Date == DateTime.Today) { publishedDate = publishedDate.Date + DateTime.Now.TimeOfDay - TimeSpan.FromMinutes(index); } var release = new ReleaseInfo { Guid = new Uri(infoUrl), Details = new Uri(infoUrl), Link = new Uri(downloadUrl), Title = title, Category = MapTrackerCatToNewznab(categoryId), PublishDate = publishedDate, Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(6)").TextContent.Trim()), Seeders = seeders, Peers = ParseUtil.CoerceInt(connections[1]) + seeders, Grabs = ParseUtil.CoerceInt(connections[2]), DownloadVolumeFactor = downloadVolumeFactor, UploadVolumeFactor = 1, Genres = row.QuerySelectorAll("td:nth-of-type(2) a.tortags").Select(t => t.TextContent.Trim()).ToList() }; var uLFactorImg = row.QuerySelector("img[alt*=\"x Multiplier Torrent\"]"); if (uLFactorImg != null) { release.UploadVolumeFactor = ParseUtil.CoerceDouble(uLFactorImg.GetAttribute("alt").Split('x')[0]); } releases.Add(release); } } catch (Exception ex) { OnParseError(results, ex); } return releases; } } }