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; + } + + } +}