mirror of https://github.com/Jackett/Jackett
427 lines
21 KiB
C#
427 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using AngleSharp.Dom;
|
|
using AngleSharp.Html.Parser;
|
|
using Jackett.Common.Models;
|
|
using Jackett.Common.Models.IndexerConfig;
|
|
using Jackett.Common.Services.Interfaces;
|
|
using Jackett.Common.Utils;
|
|
using Newtonsoft.Json.Linq;
|
|
using NLog;
|
|
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
|
|
using WebClient = Jackett.Common.Utils.Clients.WebClient;
|
|
|
|
namespace Jackett.Common.Indexers
|
|
{
|
|
/// <summary>
|
|
/// Provider for Abnormal Private French Tracker
|
|
/// gazelle based but the ajax.php API seems to be broken (always returning failure)
|
|
/// </summary>
|
|
[ExcludeFromCodeCoverage]
|
|
public class Abnormal : BaseCachingWebIndexer
|
|
{
|
|
private string TorrentDetailsUrl => SiteLink + "Torrent/Details?ReleaseId={id}";
|
|
private string TorrentDownloadUrl => SiteLink + "Torrent/Download?ReleaseId={id}";
|
|
private string LoginUrl => SiteLink + "Home/Login";
|
|
private string SearchUrl => SiteLink + "Torrent";
|
|
private string WebRequestDelay => ((SingleSelectConfigurationItem)configData.GetDynamic("webRequestDelay")).Value;
|
|
private int MaxPages => Convert.ToInt32(((SingleSelectConfigurationItem)configData.GetDynamic("maxPages")).Value);
|
|
private string MultiReplacement => ((StringConfigurationItem)configData.GetDynamic("multiReplacement")).Value;
|
|
private bool SubReplacement => ((BoolConfigurationItem)configData.GetDynamic("subReplacement")).Value;
|
|
private bool EnhancedAnimeSearch => ((BoolConfigurationItem)configData.GetDynamic("enhancedAnimeSearch")).Value;
|
|
|
|
public override string[] LegacySiteLinks { get; protected set; } = {
|
|
"https://abnormal.ws"
|
|
};
|
|
private ConfigurationDataBasicLogin ConfigData => (ConfigurationDataBasicLogin)configData;
|
|
|
|
public Abnormal(IIndexerConfigurationService configService, WebClient w, Logger l, IProtectionService ps,
|
|
ICacheService cs)
|
|
: base(id: "abnormal",
|
|
name: "Abnormal",
|
|
description: "General French Private Tracker",
|
|
link: "https://abn.lol/",
|
|
caps: new TorznabCapabilities
|
|
{
|
|
TvSearchParams = new List<TvSearchParam>
|
|
{
|
|
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
|
},
|
|
MovieSearchParams = new List<MovieSearchParam>
|
|
{
|
|
MovieSearchParam.Q
|
|
},
|
|
BookSearchParams = new List<BookSearchParam>
|
|
{
|
|
BookSearchParam.Q
|
|
}
|
|
},
|
|
configService: configService,
|
|
client: w,
|
|
logger: l,
|
|
p: ps,
|
|
cacheService: cs,
|
|
downloadBase: "https://abn.lol/Torrent/Download?ReleaseId=",
|
|
configData: new ConfigurationDataBasicLogin()
|
|
)
|
|
{
|
|
Encoding = Encoding.UTF8;
|
|
Language = "fr-FR";
|
|
Type = "private";
|
|
|
|
AddCategoryMapping(1, TorznabCatType.TV, "Series");
|
|
AddCategoryMapping(2, TorznabCatType.Movies, "Movies");
|
|
AddCategoryMapping(3, TorznabCatType.TVDocumentary, "Documentaries");
|
|
AddCategoryMapping(4, TorznabCatType.TVAnime, "Anime");
|
|
AddCategoryMapping(5, TorznabCatType.PCGames, "Games");
|
|
AddCategoryMapping(6, TorznabCatType.PC, "Applications");
|
|
AddCategoryMapping(7, TorznabCatType.BooksEBook, "Ebooks");
|
|
AddCategoryMapping(9, TorznabCatType.TV, "Emissions");
|
|
|
|
// Dynamic Configuration
|
|
ConfigData.AddDynamic("advancedConfigurationWarning", new DisplayInfoConfigurationItem(string.Empty, "<center><b>Advanced Configuration</b></center>,<br /><br /> <center><b><u>WARNING !</u></b> <i>Be sure to read instructions before editing options bellow, you can <b>drastically reduce performance</b> of queries or have <b>non-accurate results</b>.</i></center><br/><br/><ul><li><b>Delay between Requests</b>: (<i>not recommended</i>) you can increase delay to requests made to the tracker, but a minimum of 2.1s is enforced as there is an anti-spam protection.</li><br /><li><b>Max Pages</b>: (<i>not recommended</i>) you can increase max pages to follow when making a request. But be aware that others apps can consider this indexer not working if jackett take too many times to return results. </li><br /><li><b>Enhanced Anime</b>: if you have \"Anime\", this will improve queries made to this tracker related to this type when making searches.</li><br /><li><b>Multi Replacement</b>: you can dynamically replace the word \"MULTI\" with another of your choice like \"MULTI.FRENCH\" for better analysis of 3rd party softwares.</li><br /><li><b>Sub Replacement</b>: you can dynamically replace the word \"VOSTFR\" or \"SUBFRENCH\" with the word \"ENGLISH\" for better analysis of 3rd party softwares.</li></ul>"));
|
|
|
|
var ConfigWebRequestDelay = new SingleSelectConfigurationItem("Which delay do you want to apply between each requests made to tracker ?", new Dictionary<string, string>
|
|
{
|
|
{"0", "0s (disabled)"},
|
|
{"0.1", "0.1s"},
|
|
{"0.3", "0.3s"},
|
|
{"0.5", "0.5s (default)" },
|
|
{"0.7", "0.7s" },
|
|
{"1.0", "1.0s"},
|
|
{"1.25", "1.25s"},
|
|
{"1.50", "1.50s"}
|
|
})
|
|
{ Value = "0.5" };
|
|
ConfigData.AddDynamic("webRequestDelay", ConfigWebRequestDelay);
|
|
|
|
var ConfigMaxPages = new SingleSelectConfigurationItem("How many pages do you want to follow ?", new Dictionary<string, string>
|
|
{
|
|
{"1", "1 (50 results - default / best perf.)"},
|
|
{"2", "2 (100 results)"},
|
|
{"3", "3 (150 results)"},
|
|
{"4", "4 (200 results - hard limit max)" },
|
|
})
|
|
{ Value = "1" };
|
|
ConfigData.AddDynamic("maxPages", ConfigMaxPages);
|
|
|
|
var ConfigEnhancedAnimeSearch = new BoolConfigurationItem("Do you want to use enhanced ANIME search ?") { Value = false };
|
|
ConfigData.AddDynamic("enhancedAnimeSearch", ConfigEnhancedAnimeSearch);
|
|
|
|
var ConfigMultiReplacement = new StringConfigurationItem("Do you want to replace \"MULTI\" keyword in release title by another word ?") { Value = "MULTI.FRENCH" };
|
|
ConfigData.AddDynamic("multiReplacement", ConfigMultiReplacement);
|
|
|
|
var ConfigSubReplacement = new BoolConfigurationItem("Do you want to replace \"VOSTFR\" and \"SUBFRENCH\" with \"ENGLISH\" word ?") { Value = false };
|
|
ConfigData.AddDynamic("subReplacement", ConfigSubReplacement);
|
|
|
|
webclient.requestDelay = Convert.ToDouble(WebRequestDelay);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure our Provider
|
|
/// </summary>
|
|
/// <param name="configJson">Our params in Json</param>
|
|
/// <returns>Configuration state</returns>
|
|
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
|
{
|
|
// Provider not yet configured
|
|
IsConfigured = false;
|
|
|
|
// Retrieve config values set by Jackett's user
|
|
LoadValuesFromJson(configJson);
|
|
|
|
// Check & Validate Config
|
|
logger.Debug("\nAbnormal - Validating Settings ...");
|
|
|
|
// Check Username Setting
|
|
if (string.IsNullOrEmpty(ConfigData.Username.Value))
|
|
{
|
|
throw new ExceptionWithConfigData("You must provide a username for this tracker to login !", ConfigData);
|
|
}
|
|
else
|
|
{
|
|
logger.Debug("\nAbnormal - Validated Setting -- Username (auth) => " + ConfigData.Username.Value.ToString());
|
|
}
|
|
|
|
// Check Password Setting
|
|
if (string.IsNullOrEmpty(ConfigData.Password.Value))
|
|
{
|
|
throw new ExceptionWithConfigData("You must provide a password with your username for this tracker to login !", ConfigData);
|
|
}
|
|
else
|
|
{
|
|
logger.Debug("\nAbnormal - Validated Setting -- Password (auth) => " + ConfigData.Password.Value.ToString());
|
|
}
|
|
|
|
// Building login form data
|
|
var pairs = new Dictionary<string, string> {
|
|
{ "Username", ConfigData.Username.Value },
|
|
{ "Password", ConfigData.Password.Value },
|
|
{ "RememberMe", "true" },
|
|
};
|
|
|
|
// Get CSRF Token
|
|
logger.Debug("\nAbnormal - Getting CSRF token for " + LoginUrl);
|
|
var response = await RequestWithCookiesAsync(LoginUrl);
|
|
|
|
var loginResultParser = new HtmlParser();
|
|
var loginResultDocument = loginResultParser.ParseDocument(response.ContentString);
|
|
var csrfToken = loginResultDocument.QuerySelector("input[name=\"__RequestVerificationToken\"]").GetAttribute("value");
|
|
pairs.Add("__RequestVerificationToken", csrfToken);
|
|
|
|
// Perform loggin
|
|
logger.Debug("\nAbnormal - Perform loggin.. with " + LoginUrl);
|
|
response = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, LoginUrl, true);
|
|
|
|
// Test if we are logged in
|
|
await ConfigureIfOK(response.Cookies, response.Cookies.Contains(".AspNetCore.Identity.Application="), () =>
|
|
{
|
|
// Parse error page
|
|
var parser = new HtmlParser();
|
|
var dom = parser.ParseDocument(response.ContentString);
|
|
var message = dom.QuerySelector(".validation-summary-errors").TextContent.Split('.').Reverse().Skip(1).First();
|
|
|
|
// Oops, unable to login
|
|
logger.Debug("Abnormal - Login failed: \"" + message, "error");
|
|
throw new ExceptionWithConfigData("\nAbnormal - Login failed: " + message, configData);
|
|
});
|
|
|
|
logger.Debug("\nAbnormal - Login Success");
|
|
|
|
return IndexerConfigurationStatus.RequiresTesting;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute our search query
|
|
/// </summary>
|
|
/// <param name="query">Query</param>
|
|
/// <returns>Releases</returns>
|
|
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
|
{
|
|
var releases = new List<ReleaseInfo>();
|
|
var searchTerm = query.SanitizedSearchTerm + " " + query.GetEpisodeSearchString();
|
|
|
|
if (EnhancedAnimeSearch && query.HasSpecifiedCategories && (query.Categories.Contains(TorznabCatType.TVAnime.ID) || query.Categories.Contains(100032) || query.Categories.Contains(100101) || query.Categories.Contains(100110)))
|
|
{
|
|
var regex = new Regex(" ([0-9]+)");
|
|
searchTerm = regex.Replace(searchTerm, " E$1");
|
|
}
|
|
|
|
searchTerm = searchTerm.Trim();
|
|
searchTerm = searchTerm.ToLower();
|
|
searchTerm = searchTerm.Replace(" ", ".");
|
|
|
|
// Multiple page support
|
|
var nextPage = 1;
|
|
var followingPages = true;
|
|
do
|
|
{
|
|
|
|
// Build our query
|
|
var request = BuildQuery(searchTerm, query, SearchUrl, nextPage);
|
|
|
|
// Getting results
|
|
logger.Info("\nAbnormal - Querying API page " + nextPage);
|
|
var dom = new HtmlParser().ParseDocument(await QueryExecAsync(request));
|
|
var results = dom.QuerySelectorAll(".table-rows > tbody > tr:not(.mvc-grid-empty-row)");
|
|
|
|
// Torrents Result Count
|
|
var torrentsCount = results.Length;
|
|
|
|
try
|
|
{
|
|
// If contains torrents
|
|
if (torrentsCount > 0)
|
|
{
|
|
logger.Info("\nAbnormal - Found " + torrentsCount + " torrents on current page.");
|
|
|
|
// Adding each torrent row to releases
|
|
releases.AddRange(results.Select(torrent =>
|
|
{
|
|
// Selectors
|
|
var id = torrent.QuerySelector("td.grid-release-column > a").GetAttribute("href"); // ID
|
|
var name = torrent.QuerySelector("td.grid-release-column > a").TextContent; // Release Name
|
|
var categoryId = torrent.QuerySelector("td.grid-cat-column > a").GetAttribute("href"); // Category
|
|
var completed = torrent.QuerySelector("td:nth-of-type(3)").TextContent; // Completed
|
|
var seeders = torrent.QuerySelector("td.text-green").TextContent; // Seeders
|
|
var leechers = torrent.QuerySelector("td.text-red").TextContent; // Leechers
|
|
var size = torrent.QuerySelector("td:nth-of-type(6)").TextContent; // Size
|
|
|
|
var release = new ReleaseInfo
|
|
{
|
|
// Mapping data
|
|
Category = MapTrackerCatToNewznab(Regex.Match(categoryId, @"\d+").Value),
|
|
Title = name,
|
|
Seeders = int.Parse(Regex.Match(seeders, @"\d+").Value),
|
|
Peers = int.Parse(Regex.Match(seeders, @"\d+").Value) + int.Parse(Regex.Match(leechers, @"\d+").Value),
|
|
Grabs = int.Parse(Regex.Match(completed, @"\d+").Value) + int.Parse(Regex.Match(leechers, @"\d+").Value),
|
|
MinimumRatio = 1,
|
|
MinimumSeedTime = 172800,
|
|
Size = ReleaseInfo.GetBytes(size.Replace("Go", "gb").Replace("Mo", "mb").Replace("Ko", "kb")),
|
|
UploadVolumeFactor = 1,
|
|
DownloadVolumeFactor = 1,
|
|
PublishDate = DateTime.Now,
|
|
Guid = new Uri(TorrentDetailsUrl.Replace("{id}", Regex.Match(id, @"\d+").Value)),
|
|
Details = new Uri(TorrentDetailsUrl.Replace("{id}", Regex.Match(id, @"\d+").Value)),
|
|
Link = new Uri(TorrentDownloadUrl.Replace("{id}", Regex.Match(id, @"\d+").Value))
|
|
};
|
|
|
|
// Multi Replacement
|
|
if (!string.IsNullOrEmpty(MultiReplacement))
|
|
{
|
|
var regex = new Regex("(?i)([\\.\\- ])MULTI([\\.\\- ])");
|
|
release.Title = regex.Replace(release.Title, "$1" + MultiReplacement + "$2");
|
|
}
|
|
|
|
// Sub Replacement
|
|
if (SubReplacement)
|
|
release.Title = release.Title.Replace("VOSTFR", "ENGLISH").Replace("SUBFRENCH", "ENGLISH");
|
|
|
|
// Freeleech
|
|
if (torrent.QuerySelector("img[alt=\"Freeleech\"]") != null)
|
|
{
|
|
release.DownloadVolumeFactor = 0;
|
|
}
|
|
|
|
return release;
|
|
}));
|
|
if (torrentsCount == 50)
|
|
{
|
|
// Is there more pages to follow ?
|
|
var morePages = dom.QuerySelectorAll("div.mvc-grid-pager > button").Last().GetAttribute("tabindex");
|
|
if (morePages == "-1")
|
|
followingPages = false;
|
|
}
|
|
nextPage++;
|
|
}
|
|
else
|
|
{
|
|
logger.Info("\nAbnormal - No results found on page " + nextPage + ", stopping follow of next page.");
|
|
// No results or no more results available
|
|
followingPages = false;
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnParseError("Unable to parse result \n" + ex.StackTrace, ex);
|
|
}
|
|
|
|
// Stop ?
|
|
if (torrentsCount < int.Parse(dom.QuerySelector(".mvc-grid-pager-rows").GetAttribute("value")))
|
|
{
|
|
logger.Info("\nAbnormal - Stopping follow of next page " + nextPage + " due max available results reached.");
|
|
break;
|
|
}
|
|
else if (nextPage > MaxPages)
|
|
{
|
|
logger.Info("\nAbnormal - Stopping follow of next page " + nextPage + " due to page limit reached.");
|
|
break;
|
|
}
|
|
else if (query.IsTest)
|
|
{
|
|
logger.Info("\nAbnormal - Stopping follow of next page " + nextPage + " due to index test query.");
|
|
break;
|
|
}
|
|
|
|
} while (followingPages);
|
|
|
|
// Return found releases
|
|
return releases;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build query to process
|
|
/// </summary>
|
|
/// <param name="term">Term to search</param>
|
|
/// <param name="query">Torznab Query for categories mapping</param>
|
|
/// <param name="url">Search url for provider</param>
|
|
/// <param name="page">Page number to request</param>
|
|
/// <returns>URL to query for parsing and processing results</returns>
|
|
private string BuildQuery(string term, TorznabQuery query, string url, int page = 0)
|
|
{
|
|
var parameters = new NameValueCollection();
|
|
var categoriesList = MapTorznabCapsToTrackers(query);
|
|
string categories = null;
|
|
|
|
// Pages handling
|
|
if (page > 1 && !query.IsTest)
|
|
{
|
|
parameters.Add("page", page.ToString());
|
|
}
|
|
|
|
// Loop on Categories needed
|
|
foreach (var category in categoriesList)
|
|
{
|
|
// If last, build !
|
|
if (categoriesList.Last() == category)
|
|
{
|
|
// Adding previous categories to URL with latest category
|
|
parameters.Add(Uri.EscapeDataString("SelectedCats="), WebUtility.UrlEncode(category) + categories);
|
|
}
|
|
else
|
|
{
|
|
// Build categories parameter
|
|
categories += "&" + Uri.EscapeDataString("SelectedCats=") + "=" + WebUtility.UrlEncode(category);
|
|
}
|
|
}
|
|
|
|
// If search term provided
|
|
if (!string.IsNullOrWhiteSpace(term))
|
|
{
|
|
// Add search term
|
|
parameters.Add("Search", WebUtility.UrlEncode(term));
|
|
url += "?" + string.Join("&", parameters.AllKeys.Select(a => a + "=" + parameters[a]));
|
|
}
|
|
else
|
|
{
|
|
// Showing all torrents (just for output function)
|
|
term = "all";
|
|
}
|
|
|
|
logger.Info("\nAbnormal - Builded query for \"" + term + "\"... " + url);
|
|
|
|
// Return our search url
|
|
return url;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch Method for Querying
|
|
/// </summary>
|
|
/// <param name="request">URL created by Query Builder</param>
|
|
/// <returns>Results from query</returns>
|
|
private async Task<string> QueryExecAsync(string request)
|
|
{
|
|
|
|
// Querying tracker directly
|
|
var results = await QueryTrackerAsync(request);
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get Torrents Page from Tracker by Query Provided
|
|
/// </summary>
|
|
/// <param name="request">URL created by Query Builder</param>
|
|
/// <returns>Results from query</returns>
|
|
private async Task<string> QueryTrackerAsync(string request)
|
|
{
|
|
// Cache mode not enabled or cached file didn't exist for our query
|
|
logger.Info("\nAbnormal - Querying tracker for results....");
|
|
|
|
// Request our first page
|
|
var results = await RequestWithCookiesAndRetryAsync(request);
|
|
|
|
// Return results from tracker
|
|
return results.ContentString;
|
|
}
|
|
}
|
|
}
|