diff --git a/src/Jackett.Common/Definitions/abnormal.yml b/src/Jackett.Common/Definitions/abnormal.yml new file mode 100644 index 000000000..1cda0e731 --- /dev/null +++ b/src/Jackett.Common/Definitions/abnormal.yml @@ -0,0 +1,170 @@ +--- +id: abnormal +name: Abnormal +description: "General French Private Tracker" +language: fr-FR +type: private +encoding: UTF-8 +requestDelay: 2.1 +links: + - https://abn.lol/ +legacylinks: + - https://abnormal.ws + +caps: + categorymappings: + - {id: 1, cat: TV, desc: "Series"} + - {id: 2, cat: Movies, desc: "Movies"} + - {id: 3, cat: TV/Documentary, desc: "Documentaries"} + - {id: 4, cat: TV/Anime, desc: "Anime"} + - {id: 5, cat: PC/Games, desc: "Games"} + - {id: 6, cat: PC, desc: "Applications"} + - {id: 7, cat: Books/EBook, desc: "Ebooks"} + - {id: 9, cat: TV, desc: "Emissions"} + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + book-search: [q] + +settings: + - name: username + type: text + label: Username + - name: password + type: password + label: Password + - name: multilang + type: checkbox + label: Replace MULTI by another language in release name + default: false + - name: multilanguage + type: select + label: Replace MULTI by this language + default: FRENCH + options: + FRENCH: FRENCH + MULTI.FRENCH: MULTI.FRENCH + ENGLISH: ENGLISH + MULTI.ENGLISH: MULTI.ENGLISH + VOSTFR: VOSTFR + MULTI.VOSTFR: MULTI.VOSTFR + - name: vostfr + type: checkbox + label: Replace VOSTFR with ENGLISH + default: false + - name: freeleech + type: checkbox + label: Search freeleech only + default: false + - name: sort + type: select + label: Sort requested from site + default: Created + options: + Created: created + Seeders: seeders + Size: size + ReleaseName: title + - name: type + type: select + label: Order requested from site + default: desc + options: + desc: desc + asc: asc + +login: + method: form + path: Home/Login + form: "#account" + inputs: + Username: "{{ .Config.username }}" + Password: "{{ .Config.password }}" + RememberMe: true + selectorinputs: + __RequestVerificationToken: + selector: input[name="__RequestVerificationToken"] + attribute: value + test: + path: / + +search: + paths: + - path: Torrent + inputs: + $raw: "{{ range .Categories }}SelectedCats={{.}}&{{end}}" + Search: "{{ .Keywords }}" + UserId: "" + YearOperator: ≥ + Year: "" + RatingOperator: ≥ + Rating: "" + Pending: "" + Pack: "" + Scene: "" + Freeleech: "{{ if .Config.freeleech }}true{{ else }}{{ end }}" + SortOn: "{{ .Config.sort }}" + SortOrder: "{{ .Config.type }}" + + rows: + selector: table.table-rows > tbody > tr + + fields: + category: + selector: a[href^="/Torrent?SelectedCats="] + attribute: href + filters: + - name: querystring + args: SelectedCats + title_phase1: + selector: td.grid-release-column > a + title_multilang: + text: "{{ .Result.title_phase1 }}" + filters: + - name: re_replace + args: ["(?i)(\\smulti\\s)", " {{ .Config.multilanguage }} "] + title_phase2: + text: "{{ if .Config.multilang }}{{ .Result.title_multilang }}{{ else }}{{ .Result.title_phase1 }}{{ end }}" + title_vostfr: + text: "{{ .Result.title_phase2 }}" + filters: + - name: re_replace + args: ["(?i)(\\svostfr\\s)", " ENGLISH "] + - name: re_replace + args: ["(?i)(\\ssubfrench\\s)", " ENGLISH "] + title: + text: "{{ if .Config.vostfr }}{{ .Result.title_vostfr }}{{ else }}{{ .Result.title_phase2 }}{{ end }}" + details: + selector: a[href^="/Torrent/Details?ReleaseId="] + attribute: href + download: + selector: a[href^="/Torrent/Download?ReleaseId="] + attribute: href + date: + text: now + size: + selector: td:nth-child(6) + filters: + - name: re_replace + args: [ ",", "." ] + - name: replace + args: [ "o", "B" ] + seeders: + selector: td:nth-child(7) + leechers: + selector: td:nth-child(8) + downloadvolumefactor: + case: + img[title="Freeleech"]: 0 + "*": 1 + uploadvolumefactor: + case: + "*": 1 + minimumratio: + text: 1.0 + minimumseedtime: + # 2 days (as seconds = 2 x 24 x 60 x 60) + text: 172800 +# Gazelle diff --git a/src/Jackett.Common/Indexers/Abnormal.cs b/src/Jackett.Common/Indexers/Abnormal.cs deleted file mode 100644 index 8621a0a07..000000000 --- a/src/Jackett.Common/Indexers/Abnormal.cs +++ /dev/null @@ -1,426 +0,0 @@ -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 -{ - /// - /// Provider for Abnormal Private French Tracker - /// gazelle based but the ajax.php API seems to be broken (always returning failure) - /// - [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.Q, TvSearchParam.Season, TvSearchParam.Ep - }, - MovieSearchParams = new List - { - MovieSearchParam.Q - }, - BookSearchParams = new List - { - 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, "
Advanced Configuration
,

WARNING ! Be sure to read instructions before editing options bellow, you can drastically reduce performance of queries or have non-accurate results.


  • Delay between Requests: (not recommended) 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.

  • Max Pages: (not recommended) 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.

  • Enhanced Anime: if you have \"Anime\", this will improve queries made to this tracker related to this type when making searches.

  • Multi Replacement: you can dynamically replace the word \"MULTI\" with another of your choice like \"MULTI.FRENCH\" for better analysis of 3rd party softwares.

  • Sub Replacement: you can dynamically replace the word \"VOSTFR\" or \"SUBFRENCH\" with the word \"ENGLISH\" for better analysis of 3rd party softwares.
")); - - var ConfigWebRequestDelay = new SingleSelectConfigurationItem("Which delay do you want to apply between each requests made to tracker ?", new Dictionary - { - {"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 - { - {"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); - } - - /// - /// Configure our Provider - /// - /// Our params in Json - /// Configuration state - public override async Task 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 { - { "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; - } - - /// - /// Execute our search query - /// - /// Query - /// Releases - protected override async Task> PerformQuery(TorznabQuery query) - { - var releases = new List(); - 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; - } - - /// - /// Build query to process - /// - /// Term to search - /// Torznab Query for categories mapping - /// Search url for provider - /// Page number to request - /// URL to query for parsing and processing results - 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; - } - - /// - /// Switch Method for Querying - /// - /// URL created by Query Builder - /// Results from query - private async Task QueryExecAsync(string request) - { - - // Querying tracker directly - var results = await QueryTrackerAsync(request); - - return results; - } - - /// - /// Get Torrents Page from Tracker by Query Provided - /// - /// URL created by Query Builder - /// Results from query - private async Task 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; - } - } -}