diff --git a/src/Jackett.Common/Definitions/brasiltracker.yml b/src/Jackett.Common/Definitions/brasiltracker.yml
deleted file mode 100644
index 9587d1804..000000000
--- a/src/Jackett.Common/Definitions/brasiltracker.yml
+++ /dev/null
@@ -1,140 +0,0 @@
----
-id: brasiltracker
-name: BrasilTracker
-description: "BrasilTracker is a BRAZILIAN Private Torrent Tracker for MOVIES / TV / GENERAL"
-language: pt-BR
-encoding: UTF-8
-type: private
-links:
- - https://brasiltracker.org/
-
-caps:
- categories:
- Other: Other
-
- modes:
- search: [q]
- tv-search: [q, season, ep]
- movie-search: [q, imdbid]
-
-settings:
- - name: username
- type: text
- label: Username
- - name: password
- type: password
- label: Password
- - name: info_8000
- type: info
- label: About BrasilTracker Categories
- default: BrasilTracker does not return categories in its search results.To add to your Apps' Torznab indexer, replace all categories with 8000(Other).
- - name: freeleech
- type: checkbox
- label: Search freeleech only
- default: false
- - name: sort
- type: select
- label: Sort requested from site
- default: time
- options:
- time: created
- seeders: seeders
- size: size
- - name: type
- type: select
- label: Order requested from site
- default: desc
- options:
- desc: desc
- asc: asc
- - name: info_results
- type: info
- label: "Search results"
- default: "This indexer does not support Torrent Groups
Un-tick the Torrent grouping (Habilitar Grupo de Torrents) checkbox in your Configurações."
-
-login:
- path: login.php
- method: form
- form: form#loginform
- inputs:
- username: "{{ .Config.username }}"
- password: "{{ .Config.password }}"
- keeplogged: 1
- error:
- - selector: form#loginform:contains("incorretos")
- test:
- path: index.php
- selector: a[href^="logout.php?auth="]
-
-search:
- paths:
- # https://brasiltracker.org/torrents.php?order_by=time&order_way=desc&freetorrent=1&filter_cat[6]=1&filter_cat[3]=1&action=basic&searchsubmit=1
- # https://brasiltracker.org/torrents.php?searchstr=mandalorain&order_by=size&order_way=desc&action=basic&searchsubmit=1
- # https://brasiltracker.org/torrents.php?searchstr=tt8179024&order_by=time&order_way=desc&action=basic&searchsubmit=1
- - path: torrents.php
- inputs:
- searchstr: "{{ if .Query.IMDBID }}{{ .Query.IMDBID }}{{ else }}{{ .Keywords }}{{ end }}"
- order_by: "{{ .Config.sort }}"
- order_way: "{{ .Config.type }}"
- action: basic
- freetorrent: "{{ if .Config.freeleech }}1{{ else }}{{ end }}"
- searchsubmit: 1
-
- rows:
- selector: table#torrent_table > tbody > tr.torrent
-
- fields:
- category:
- text: Other
- details:
- selector: a[href^="torrents.php?id="]
- attribute: href
- download:
- selector: a[href^="torrents.php?action=download&id="]
- attribute: href
- description:
- selector: div.tags
- poster:
- selector: img[alt="Cover"]
- attribute: src
- imdbid:
- selector: a[href*="imdb.com/title/tt"]
- attribute: href
- files:
- selector: td:nth-child(3)
- date:
- selector: span.time
- attribute: title
- filters:
- - name: append
- args: " -03:00" # BRT
- - name: dateparse
- args: "Jan 2 2006, 15:04 -07:00"
- size:
- selector: td:nth-child(5)
- grabs:
- selector: td:nth-child(6)
- seeders:
- selector: td:nth-child(7)
- leechers:
- selector: td:nth-child(8)
- downloadvolumefactor:
- case:
- strong.tl_free: 0
- "*": 1
- uploadvolumefactor:
- text: 1
- title_details:
- selector: div.torrent_info
- remove: strong
- title:
- selector: a[href^="torrents.php?id="]
- filters:
- - name: append
- args: " {{ .Result.title_details }}"
- minimumratio:
- text: 1.0
- minimumseedtime:
- # 2 days (as seconds = 2 x 24 x 60 x 60)
- text: 172800
-# Project Gazelle
diff --git a/src/Jackett.Common/Indexers/BrasilTracker.cs b/src/Jackett.Common/Indexers/BrasilTracker.cs
new file mode 100644
index 000000000..51e0ad1cf
--- /dev/null
+++ b/src/Jackett.Common/Indexers/BrasilTracker.cs
@@ -0,0 +1,292 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using AngleSharp.Html.Parser;
+using Jackett.Common.Models;
+using Jackett.Common.Models.IndexerConfig;
+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 BrasilTracker : BaseWebIndexer
+ {
+ private string LoginUrl => SiteLink + "login.php";
+ private string BrowseUrl => SiteLink + "torrents.php";
+ private static readonly Regex _EpisodeRegex = new Regex(@"(?:[SsEe]\d{2,4}){1,2}");
+
+ private new ConfigurationDataBasicLogin configData => (ConfigurationDataBasicLogin)base.configData;
+
+ public BrasilTracker(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps,
+ ICacheService cs)
+ : base(id: "brasiltracker",
+ name: "BrasilTracker",
+ description: "BrasilTracker is a BRAZILIAN Private Torrent Tracker for MOVIES / TV / GENERAL",
+ link: "https://brasiltracker.org/",
+ caps: new TorznabCapabilities
+ {
+ TvSearchParams = new List
+ {
+ TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
+ },
+ MovieSearchParams = new List
+ {
+ MovieSearchParam.Q, MovieSearchParam.ImdbId
+ },
+ MusicSearchParams = new List
+ {
+ MusicSearchParam.Q
+ },
+ BookSearchParams = new List
+ {
+ BookSearchParam.Q
+ }
+ },
+ configService: configService,
+ client: wc,
+ logger: l,
+ p: ps,
+ cacheService: cs,
+ configData: new ConfigurationDataBasicLogin())
+ {
+ Encoding = Encoding.UTF8;
+ Language = "pt-BR";
+ Type = "private";
+ AddCategoryMapping(1, TorznabCatType.Other, "Other");
+ }
+
+ public override async Task ApplyConfiguration(JToken configJson)
+ {
+ LoadValuesFromJson(configJson);
+ var pairs = new Dictionary
+ {
+ { "username", configData.Username.Value },
+ { "password", configData.Password.Value },
+ { "keeplogged", "1" },
+ { "login", "Log in" }
+ };
+
+ var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, LoginUrl, true);
+ await ConfigureIfOK(result.Cookies, result.ContentString?.Contains("logout.php") == true, () =>
+ {
+ var parser = new HtmlParser();
+ var dom = parser.ParseDocument(result.ContentString);
+ var errorMessage = dom.QuerySelector("form#loginform").TextContent.Trim();
+ throw new ExceptionWithConfigData(errorMessage, configData);
+ });
+ return IndexerConfigurationStatus.RequiresTesting;
+ }
+
+ private static string InternationalTitle(string title)
+ {
+ var match = Regex.Match(title, @".* \[(.*\/?)\]");
+ return match.Success ? match.Groups[1].Value.Split('/')[0] : title;
+ }
+
+ private static string StripSearchString(string term)
+ {
+ // Search does not support searching with episode numbers so strip it if we have one
+ // AND filter the result later to archive the proper result
+ term = _EpisodeRegex.Replace(term, string.Empty);
+ return term.TrimEnd();
+ }
+
+ private string ParseTitle(string title, string seasonEp, string year)
+ {
+ // Removes the SxxExx if it comes on the title
+ var cleanTitle = _EpisodeRegex.Replace(title, string.Empty);
+ // Removes the year if it comes on the title
+ // The space is added because on daily releases the date will be XX/XX/YYYY
+ if (!string.IsNullOrEmpty(year))
+ cleanTitle = cleanTitle.Replace(" " + year, string.Empty);
+ cleanTitle = Regex.Replace(cleanTitle, @"^\s*|[\s-]*$", string.Empty);
+
+ // Get international title if available, or use the full title if not
+ cleanTitle = InternationalTitle(cleanTitle);
+ cleanTitle += " " + year + " " + seasonEp;
+ cleanTitle = cleanTitle.Trim();
+ return cleanTitle;
+ }
+ private string FixSearchTerm(TorznabQuery query)
+ {
+ if (query.IsImdbQuery)
+ return query.ImdbID;
+ return query.GetQueryString();
+ }
+
+ protected override async Task> PerformQuery(TorznabQuery query)
+ {
+ var releases = new List();
+ var searchUrl = BrowseUrl;
+ var searchTerm = FixSearchTerm(query);
+ var queryCollection = new NameValueCollection
+ {
+ {"searchstr", StripSearchString(searchTerm)},
+ {"order_by", "time"},
+ {"order_way", "desc"},
+ {"group_results", "1"},
+ {"action", "basic"},
+ {"searchsubmit", "1"}
+ };
+
+ searchUrl += "?" + queryCollection.GetQueryString();
+ var results = await RequestWithCookiesAsync(searchUrl);
+ //try
+ //{
+ const string rowsSelector = "table.torrent_table > tbody > tr:not(tr.colhead)";
+ var searchResultParser = new HtmlParser();
+ var searchResultDocument = searchResultParser.ParseDocument(results.ContentString);
+ var rows = searchResultDocument.QuerySelectorAll(rowsSelector);
+ string groupTitle = null;
+ string groupYearStr = null;
+ foreach (var row in rows)
+ //try
+ {
+ // ignore sub groups info row, it's just an row with an info about the next section, something like "Dual Áudio" or "Legendado"
+ if (row.QuerySelector(".edition_info") != null)
+ continue;
+
+ // some torrents has more than one link, and the one with .tooltip is the wrong one in that case,
+ // so let's try to pick up first without the .tooltip class,
+ // if nothing is found, then we try again without that filter
+ var qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]:not(.tooltip)");
+ if (qDetailsLink == null)
+ {
+ qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]");
+ // if still can't find the right link, skip it
+ if (qDetailsLink == null)
+ {
+ logger.Error($"{Id}: Error while parsing row '{row.OuterHtml}': Can't find the right details link");
+ continue;
+ }
+ }
+ var title = StripSearchString(qDetailsLink.TextContent);
+
+ var seasonEl = row.QuerySelector("a[href^=\"torrents.php?torrentid=\"]");
+ string seasonEp = null;
+ if (seasonEl != null)
+ {
+ var seasonMatch = _EpisodeRegex.Match(seasonEl.TextContent);
+ seasonEp = seasonMatch.Success ? seasonMatch.Value : null;
+ }
+ seasonEp ??= _EpisodeRegex.Match(qDetailsLink.TextContent).Value;
+
+ ICollection category = new List { TorznabCatType.Other.ID };
+ string yearStr = null;
+ if (row.ClassList.Contains("group") || row.ClassList.Contains("torrent")) // group/ungrouped headers
+ {
+ var qCatLink = row.QuerySelector("a[href^=\"/torrents.php?filter_cat\"]");
+
+ var torrentInfoEl = row.QuerySelector("div.torrent_info");
+ if (torrentInfoEl != null)
+ {
+ // valid for torrent grouped but that has only 1 episode yet
+ yearStr = torrentInfoEl.GetAttribute("data-year");
+ }
+ yearStr ??= qDetailsLink.NextSibling.TextContent.Trim().TrimStart('[').TrimEnd(']');
+
+ if (row.ClassList.Contains("group")) // group headers
+ {
+ groupTitle = title;
+ groupYearStr = yearStr;
+ continue;
+ }
+ }
+
+ var release = new ReleaseInfo
+ {
+ MinimumRatio = 1,
+ MinimumSeedTime = 0
+ };
+ var qDlLink = row.QuerySelector("a[href^=\"torrents.php?action=download\"]");
+ var qSize = row.QuerySelector("td:nth-last-child(4)");
+ var qGrabs = row.QuerySelector("td:nth-last-child(3)");
+ var qSeeders = row.QuerySelector("td:nth-last-child(2)");
+ var qLeechers = row.QuerySelector("td:nth-last-child(1)");
+ var qFreeLeech = row.QuerySelector("strong[title=\"Free\"]");
+ if (row.ClassList.Contains("group_torrent")) // torrents belonging to a group
+ {
+ release.Description = Regex.Match(qDetailsLink.TextContent, @"\[.*?\]").Value;
+ release.Title = ParseTitle(groupTitle, seasonEp, groupYearStr);
+ }
+ else if (row.ClassList.Contains("torrent")) // standalone/un grouped torrents
+ {
+ release.Description = row.QuerySelector("div.torrent_info").TextContent;
+ release.Title = ParseTitle(title, seasonEp, yearStr);
+ }
+ release.Category = category;
+ release.Description = release.Description.Replace(" / Free", ""); // Remove Free Tag
+ release.Description = release.Description.Replace("/ WEB ", "/ WEB-DL "); // Fix web/web-dl
+ release.Description = release.Description.Replace("Full HD", "1080p");
+ // Handles HDR conflict
+ release.Description = release.Description.Replace("/ HD /", "/ 720p /");
+ release.Description = release.Description.Replace("/ HD]", "/ 720p]");
+ release.Description = release.Description.Replace("4K", "2160p");
+ release.Description = release.Description.Replace("SD", "480p");
+ release.Description = release.Description.Replace("Dual Áudio", "Dual");
+
+ // Adjust the description in order to can be read by Radarr and Sonarr
+ var cleanDescription = release.Description.Trim().TrimStart('[').TrimEnd(']');
+ string[] titleElements;
+
+ //Formats the title so it can be parsed later
+ var stringSeparators = new[]
+ {
+ " / "
+ };
+ titleElements = cleanDescription.Split(stringSeparators, StringSplitOptions.None);
+ // release.Title += string.Join(" ", titleElements);
+ release.Title = release.Title.Trim();
+ if (titleElements.Length < 6)
+ // Usually non movies / series could have less than 6 elements, eg: Books.
+ release.Title += " " + string.Join(" ", titleElements);
+ else
+ release.Title += " " + titleElements[5] + " " + titleElements[3] + " " + titleElements[1] + " " +
+ titleElements[2] + " " + titleElements[4] + " " + string.Join(
+ " ", titleElements.Skip(6));
+
+ if (Regex.IsMatch(release.Description, "(Dual|[Nn]acional|[Dd]ublado)"))
+ release.Title += " Brazilian";
+
+ // This tracker does not provide an publish date to search terms (only on last 24h page)
+ release.PublishDate = DateTime.Today;
+
+ // check for previously stripped search terms
+ if (!query.IsImdbQuery && !query.MatchQueryStringAND(release.Title, null, searchTerm))
+ continue;
+ var size = qSize.TextContent;
+ release.Size = ReleaseInfo.GetBytes(size);
+ release.Link = new Uri(SiteLink + qDlLink.GetAttribute("href"));
+ release.Details = new Uri(SiteLink + qDetailsLink.GetAttribute("href"));
+ release.Guid = release.Link;
+ release.Grabs = ParseUtil.CoerceLong(qGrabs.TextContent);
+ release.Seeders = ParseUtil.CoerceInt(qSeeders.TextContent);
+ release.Peers = ParseUtil.CoerceInt(qLeechers.TextContent) + release.Seeders;
+ release.DownloadVolumeFactor = qFreeLeech != null ? 0 : 1;
+ release.UploadVolumeFactor = 1;
+ releases.Add(release);
+ }
+ //catch (Exception ex)
+ //{
+ // logger.Error($"{Id}: Error while parsing row '{row.OuterHtml}': {ex.Message}");
+ //}
+ //}
+ //catch (Exception ex)
+ //{
+ // OnParseError(results.ContentString, ex);
+ //}
+
+ return releases;
+ }
+
+ }
+}