Jackett/src/Jackett.Common/Indexers/AnimeTorrents.cs

232 lines
10 KiB
C#

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>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
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<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
{
LoadValuesFromJson(configJson);
var pairs = new Dictionary<string, string> {
{ "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<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var releases = new List<ReleaseInfo>();
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<string, string>
{
{ "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;
}
}
}