From 7b45dcaed0bad08fd9762d3d46ed4560147adf4e Mon Sep 17 00:00:00 2001 From: Uilton Oliveira Date: Wed, 15 Apr 2020 01:01:13 -0300 Subject: [PATCH] BJ-Share: Refactoring and bugfixes. Resolves #8033 (#8142) --- src/Jackett.Common/Indexers/BJShare.cs | 768 +++++++++++-------------- 1 file changed, 343 insertions(+), 425 deletions(-) diff --git a/src/Jackett.Common/Indexers/BJShare.cs b/src/Jackett.Common/Indexers/BJShare.cs index f220d05df..0231551b3 100644 --- a/src/Jackett.Common/Indexers/BJShare.cs +++ b/src/Jackett.Common/Indexers/BJShare.cs @@ -23,54 +23,59 @@ namespace Jackett.Common.Indexers private string LoginUrl => SiteLink + "login.php"; private string BrowseUrl => SiteLink + "torrents.php"; private string TodayUrl => SiteLink + "torrents.php?action=today"; - private readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; - private readonly Dictionary _commonSearchTerms = new Dictionary + private static readonly Regex _EpisodeRegex = new Regex(@"(?:[SsEe]\d{2,4}){1,2}"); + + public override string[] LegacySiteLinks { get; protected set; } = { - { "agents of shield", "Agents of S.H.I.E.L.D."}, - { "tell me a story us", "Tell Me a Story"}, - { "greys anatomy", "grey's anatomy"} + "https://bj-share.me/" + }; + + private ConfigurationDataBasicLoginWithRSSAndDisplay ConfigData => (ConfigurationDataBasicLoginWithRSSAndDisplay)configData; + + + + private readonly List _absoluteNumbering = new List + { + "One Piece", + "Boruto", + "Black Clover", + "Fairy Tail", + "Super Dragon Ball Heroes" }; private readonly Dictionary _commonResultTerms = new Dictionary { - { "tell me a story", "Tell Me a Story US"}, - { "fairy tail: final season", "Fairy Tail: Final Series"}, - { "agents of s.h.i.e.l.d.", "Marvels Agents of SHIELD"}, - { "legends of tomorrow", "DCs Legends of Tomorrow"} + {"tell me a story", "Tell Me a Story US"}, + {"fairy tail: final season", "Fairy Tail: Final Series"}, + {"agents of s.h.i.e.l.d.", "Marvels Agents of SHIELD"}, + {"legends of tomorrow", "DCs Legends of Tomorrow"} }; - private readonly List _absoluteNumbering = new List + private readonly Dictionary _commonSearchTerms = new Dictionary { - "One Piece", "Boruto", "Black Clover", "Fairy Tail", "Super Dragon Ball Heroes" + {"agents of shield", "Agents of S.H.I.E.L.D."}, + {"tell me a story us", "Tell Me a Story"}, + {"greys anatomy", "grey's anatomy"} }; - public override string[] LegacySiteLinks { get; protected set; } = new string[] { - "https://bj-share.me/" - }; - - private ConfigurationDataBasicLoginWithRSSAndDisplay ConfigData - { - get => (ConfigurationDataBasicLoginWithRSSAndDisplay)configData; - set => configData = value; - } - - public BJShare(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) - : base("BJ-Share", - description: "A brazilian tracker.", - link: "https://bj-share.info/", - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), - configService: configService, - client: wc, - logger: l, - p: ps, - configData: new ConfigurationDataBasicLoginWithRSSAndDisplay()) + public BJShare(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) : + base("BJ-Share", + description: "A brazilian tracker.", + link: "https://bj-share.info/", + caps: new TorznabCapabilities + { + SupportsImdbMovieSearch = true + }, + configService: configService, + client: wc, + logger: l, + p: ps, + configData: new ConfigurationDataBasicLoginWithRSSAndDisplay()) { Encoding = Encoding.UTF8; Language = "pt-br"; Type = "private"; - TorznabCaps.SupportsImdbMovieSearch = true; - AddCategoryMapping(14, TorznabCatType.TVAnime, "Anime"); AddCategoryMapping(3, TorznabCatType.PC0day, "Aplicativos"); AddCategoryMapping(8, TorznabCatType.Other, "Apostilas/Tutoriais"); @@ -98,48 +103,64 @@ namespace Jackett.Common.Indexers public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); - var pairs = new Dictionary { - { "username", ConfigData.Username.Value }, - { "password", ConfigData.Password.Value }, - { "keeplogged", "1" } + {"username", ConfigData.Username.Value}, + {"password", ConfigData.Password.Value}, + {"keeplogged", "1"} }; - var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, LoginUrl, true); - await ConfigureIfOK(result.Cookies, result.Content != null && result.Content.Contains("logout.php"), () => - { - var errorMessage = result.Content; - throw new ExceptionWithConfigData(errorMessage, ConfigData); - }); + await ConfigureIfOK( + result.Cookies, result.Content?.Contains("logout.php") == true, () => + { + var errorMessage = result.Content; + throw new ExceptionWithConfigData(errorMessage, ConfigData); + }); return IndexerConfigurationStatus.RequiresTesting; } - private string InternationalTitle(string title) + private static string InternationalTitle(string title) { - // Get international title if available, or use the full title if not - var cleanTitle = Regex.Replace(title, @".* \[(.*?)\](.*)", "$1$2"); - cleanTitle = Regex.Replace(cleanTitle, @"(?:.*)\/(.*)", "$1"); - - return cleanTitle.Trim(); + var match = Regex.Match(title, @".* \[(.*?)\]$"); + return match.Success ? match.Groups[1].Value : title; } - private string StripSearchString(string term, bool isAnime) + + private static string StripSearchString(string term, bool isAnime) { // Search does not support searching with episode numbers so strip it if we have one // Ww AND filter the result later to archive the proper result - term = Regex.Replace(term, @"[S|E]\d\d", string.Empty).Trim(); - return isAnime ? term.TrimEnd(_digits) : term; + term = _EpisodeRegex.Replace(term, string.Empty); + term = isAnime ? Regex.Replace(term, @"\d*$", string.Empty) : term; + return term.TrimEnd(); + } + + private string ParseTitle(string title, string seasonEp, string year, string categoryStr) + { + var cleanTitle = _EpisodeRegex.Replace(title, 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); + foreach (var resultTerm in _commonResultTerms) + { + var newTitle = cleanTitle.ToLower().Replace(resultTerm.Key.ToLower(), resultTerm.Value); + if (!string.Equals(newTitle, cleanTitle, StringComparison.CurrentCultureIgnoreCase)) + cleanTitle = newTitle; + } + + // do not include year to animes + if (categoryStr == "14") + cleanTitle += " " + seasonEp; + else + cleanTitle += " " + year + " " + seasonEp; + return FixAbsoluteNumbering(cleanTitle); } private bool IsAbsoluteNumbering(string title) { foreach (var absoluteTitle in _absoluteNumbering) - { if (title.ToLower().Contains(absoluteTitle.ToLower())) - { return true; - } - } return false; } @@ -156,402 +177,299 @@ namespace Jackett.Common.Indexers // // In this indexer, it looks that it is added "automatically", so all current and new releases will be broken // until they or the source from where they get that info fix it... - if (IsAbsoluteNumbering(title)) { title = Regex.Replace(title, @"(Ep[\.]?[ ]?)|([S]\d\d[Ee])", ""); return title; } - else if (title.Contains("[Novela]")) + + if (title.Contains("[Novela]")) { title = Regex.Replace(title, @"(Cap[\.]?[ ]?)", "S01E"); title = Regex.Replace(title, @"(\[Novela\]\ )", ""); title = Regex.Replace(title, @"(\ \-\s*Completo)", " - S01"); return title; } - else - { - return title; - } + + return title; } - protected override async Task> PerformQuery(TorznabQuery query) + private string FixSearchTerm(TorznabQuery query) { - query = query.Clone(); // avoid modifing the original query + if (query.IsImdbQuery) + return query.ImdbID; + return _commonSearchTerms.Aggregate( + query.GetQueryString(), + (current, searchTerm) => current.ToLower().Replace(searchTerm.Key.ToLower(), searchTerm.Value)); + } + // if the search string is empty use the "last 24h torrents" view + protected override async Task> PerformQuery(TorznabQuery query) => + (string.IsNullOrWhiteSpace(query.SearchTerm) && !query.IsImdbQuery) + ? await ParseLast24HoursAsync() + : await ParseUserSearchAsync(query); + + private async Task> ParseUserSearchAsync(TorznabQuery query) + { var releases = new List(); - - // if the search string is empty use the "last 24h torrents" view - if (string.IsNullOrWhiteSpace(query.SearchTerm) && !query.IsImdbQuery) + var searchUrl = BrowseUrl; + var isSearchAnime = query.Categories.Any(s => s == TorznabCatType.TVAnime.ID); + var searchTerm = FixSearchTerm(query); + var queryCollection = new NameValueCollection { - var results = await RequestStringWithCookies(TodayUrl); - if (results.IsRedirect) - { - // re-login - await ApplyConfiguration(null); - results = await RequestStringWithCookies(TodayUrl); - } - try - { - const string rowsSelector = "table.torrent_table > tbody > tr:not(tr.colhead)"; - - var searchResultParser = new HtmlParser(); - var searchResultDocument = searchResultParser.ParseDocument(results.Content); - var rows = searchResultDocument.QuerySelectorAll(rowsSelector); - foreach (var row in rows) - { - try - { - var release = new ReleaseInfo - { - MinimumRatio = 1, - MinimumSeedTime = 0 - }; - - var qDetailsLink = row.QuerySelector("a.BJinfoBox"); - var qBJinfoBox = qDetailsLink.QuerySelector("span"); - var qCatLink = row.QuerySelector("a[href^=\"/torrents.php?filter_cat\"]"); - var qDlLink = row.QuerySelector("a[href^=\"torrents.php?action=download\"]"); - var qSeeders = row.QuerySelector("td:nth-child(4)"); - var qLeechers = row.QuerySelector("td:nth-child(5)"); - var qQuality = row.QuerySelector("font[color=\"red\"]"); - var qFreeLeech = row.QuerySelector("font[color=\"green\"]:contains(Free)"); - var qTitle = qDetailsLink.QuerySelector("font"); - // Get international title if available, or use the full title if not - release.Title = Regex.Replace(qTitle.TextContent, @".* \[(.*?)\](.*)", "$1$2"); - - var year = ""; - release.Description = ""; - var extra_info = ""; - foreach (var child in qBJinfoBox.ChildNodes) - { - var type = child.NodeType; - if (type != NodeType.Text) - continue; - - var line = child.TextContent; - if (line.StartsWith("Tamanho:")) - { - var size = line.Substring("Tamanho: ".Length); - ; - release.Size = ReleaseInfo.GetBytes(size); - } - else if (line.StartsWith("Lançado em: ")) - { - var publishDateStr = line.Substring("Lançado em: ".Length).Replace("às ", ""); - publishDateStr += " +0"; - var publishDate = DateTime.SpecifyKind(DateTime.ParseExact(publishDateStr, "dd/MM/yyyy HH:mm z", CultureInfo.InvariantCulture), DateTimeKind.Unspecified); - release.PublishDate = publishDate.ToLocalTime(); - } - else if (line.StartsWith("Ano:")) - { - year = line.Substring("Ano: ".Length); - - } - else - { - release.Description += line + "\n"; - if (line.Contains(":")) - { - if (!(line.StartsWith("Lançado") || line.StartsWith("Resolução") || line.StartsWith("Idioma") || line.StartsWith("Autor"))) - { - var info = line.Substring(line.IndexOf(": ") + 2); - if (info == "Dual Áudio") - { - info = "Dual"; - } - extra_info += info + " "; - } - } - } - } - extra_info.Trim(); - - var catStr = qCatLink.GetAttribute("href").Split('=')[1].Split('&')[0]; - release.Title = FixAbsoluteNumbering(release.Title); - - if (year != "") - { - release.Title += " " + year; - } - - if (qQuality != null) - { - var quality = qQuality.TextContent; - - switch (quality) - { - case "4K": - release.Title += " 2160p"; - break; - case "Full HD": - release.Title += " 1080p"; - break; - case "HD": - release.Title += " 720p"; - break; - default: - release.Title += " 480p"; - break; - } - } - - release.Title += " " + extra_info; - - release.Category = MapTrackerCatToNewznab(catStr); - release.Link = new Uri(SiteLink + qDlLink.GetAttribute("href")); - release.Comments = new Uri(SiteLink + qDetailsLink.GetAttribute("href")); - release.Guid = release.Link; - 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.Content, ex); - } + {"searchstr", StripSearchString(searchTerm, isSearchAnime)}, + {"order_by", "time"}, + {"order_way", "desc"}, + {"group_results", "1"}, + {"action", "basic"}, + {"searchsubmit", "1"} + }; + foreach (var cat in MapTorznabCapsToTrackers(query)) + queryCollection.Add("filter_cat[" + cat + "]", "1"); + searchUrl += "?" + queryCollection.GetQueryString(); + var results = await RequestStringWithCookies(searchUrl); + if (results.IsRedirect) + { + // re-login + await ApplyConfiguration(null); + results = await RequestStringWithCookies(searchUrl); } - else // use search + + try { - var searchUrl = BrowseUrl; - var isSearchAnime = query.Categories.Any(s => s == TorznabCatType.TVAnime.ID); - - if (!query.IsImdbQuery) - { - foreach (var searchTerm in _commonSearchTerms) + const string rowsSelector = "table.torrent_table > tbody > tr:not(tr.colhead)"; + var searchResultParser = new HtmlParser(); + var searchResultDocument = searchResultParser.ParseDocument(results.Content); + var rows = searchResultDocument.QuerySelectorAll(rowsSelector); + ICollection groupCategory = null; + string groupTitle = null; + string groupYearStr = null; + var categoryStr = ""; + foreach (var row in rows) + try { - query.SearchTerm = query.SearchTerm.ToLower().Replace(searchTerm.Key.ToLower(), searchTerm.Value); - } - } - - var searchString = query.GetQueryString(); - if (query.IsImdbQuery) - { - searchString = query.ImdbID; - } - - var queryCollection = new NameValueCollection - { - {"searchstr", StripSearchString(searchString, isSearchAnime)}, - {"order_by", "time"}, - {"order_way", "desc"}, - {"group_results", "1"}, - {"action", "basic"}, - {"searchsubmit", "1"} - }; - - foreach (var cat in MapTorznabCapsToTrackers(query)) - { - queryCollection.Add("filter_cat[" + cat + "]", "1"); - } - - searchUrl += "?" + queryCollection.GetQueryString(); - - var results = await RequestStringWithCookies(searchUrl); - if (results.IsRedirect) - { - // re-login - await ApplyConfiguration(null); - results = await RequestStringWithCookies(searchUrl); - } - try - { - const string rowsSelector = "table.torrent_table > tbody > tr:not(tr.colhead)"; - - var searchResultParser = new HtmlParser(); - var searchResultDocument = searchResultParser.ParseDocument(results.Content); - var rows = searchResultDocument.QuerySelectorAll(rowsSelector); - - ICollection groupCategory = null; - string groupTitle = null; - string groupYearStr = null; - var categoryStr = ""; - - 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; + var qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]"); + var title = StripSearchString(qDetailsLink.TextContent, false); + var seasonEp = _EpisodeRegex.Match(qDetailsLink.TextContent).Value; + ICollection category = null; + string yearStr = null; + if (row.ClassList.Contains("group") || row.ClassList.Contains("torrent")) // group/ungrouped headers { - var qDetailsLink = row.QuerySelector("a[href^=\"torrents.php?id=\"]"); - var title = qDetailsLink.TextContent; - ICollection category = null; - 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\"]"); + categoryStr = qCatLink.GetAttribute("href").Split('=')[1].Split('&')[0]; + category = MapTrackerCatToNewznab(categoryStr); + yearStr = qDetailsLink.NextSibling.TextContent.Trim().TrimStart('[').TrimEnd(']'); + if (row.ClassList.Contains("group")) // group headers { - var qCatLink = row.QuerySelector("a[href^=\"/torrents.php?filter_cat\"]"); - categoryStr = qCatLink.GetAttribute("href").Split('=')[1].Split('&')[0]; - category = MapTrackerCatToNewznab(categoryStr); - - yearStr = qDetailsLink.NextSibling.TextContent.Trim().TrimStart('[').TrimEnd(']'); - - title = FixAbsoluteNumbering(title); - - if (row.ClassList.Contains("group")) // group headers - { - groupCategory = category; - 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 - { - var description = Regex.Replace(qDetailsLink.TextContent.Trim(), @"\s+", " "); - description = Regex.Replace(description, @"((S\d{2})(E\d{2,4})?) (.*)", "$4"); - release.Description = description; - - var cleanTitle = Regex.Replace(groupTitle, @" - ((S(\d{2}))?E(\d{1,4}))", ""); - // Get international title if available, or use the full title if not - cleanTitle = InternationalTitle(cleanTitle); - - foreach (var resultTerm in _commonResultTerms) - { - var newTitle = cleanTitle.ToLower().Replace(resultTerm.Key.ToLower(), resultTerm.Value); - if (!string.Equals(newTitle, cleanTitle, StringComparison.CurrentCultureIgnoreCase)) - { - cleanTitle = newTitle; - } - } - - title = Regex.Replace(title.Trim(), @"\s+", " "); - var seasonEp = Regex.Replace(title, @"((S\d{2})?(E\d{2,4})?) .*", "$1"); - if (seasonEp[0] == '[') - seasonEp = ""; - - // do not include year to animes - if (categoryStr == "14") - { - release.Title = cleanTitle + " " + seasonEp; - } - else - { - release.Title = cleanTitle + " " + groupYearStr + " " + seasonEp; - } - release.Category = groupCategory; - } - else if (row.ClassList.Contains("torrent")) // standalone/un grouped torrents - { - var qDescription = row.QuerySelector("div.torrent_info"); - release.Description = qDescription.TextContent; - title = FixAbsoluteNumbering(title); - - var cleanTitle = Regex.Replace(title, @" - ((S\d{2})?(E\d{2,4})?)", ""); - // Get international title if available, or use the full title if not - cleanTitle = InternationalTitle(cleanTitle); - - foreach (var resultTerm in _commonResultTerms) - { - var newTitle = cleanTitle.ToLower().Replace(resultTerm.Key.ToLower(), resultTerm.Value); - if (!string.Equals(newTitle, cleanTitle, StringComparison.CurrentCultureIgnoreCase)) - { - cleanTitle = newTitle; - } - } - - var seasonEp = Regex.Replace(title, @"^(.*?) - ((S\d{2})?(E\d{2,4})?)", "$2"); - if (seasonEp[0] == '[') - seasonEp = ""; - - // do not include year to animes - if (categoryStr == "14") - { - release.Title = cleanTitle + " " + seasonEp; - } - // the seasonEp RegEx is getting all when done with movies, and then cleaning again when getting international name, - // so it was cutting of the year of movies and getting clonflict in Radarr - else if (categoryStr == "2" || categoryStr == "6") - { - release.Title = cleanTitle + " " + yearStr + " " + seasonEp; - } - else - { - release.Title = cleanTitle + " " + yearStr; - } - - release.Category = category; - } - - release.Description = release.Description.Replace(" / Free", ""); // Remove Free Tag - 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"); - // If it ain't nacional there will be the type of the audio / original audio - if (release.Description.IndexOf("Nacional") == -1) - { - release.Description = Regex.Replace(release.Description, @"(Dual|Legendado|Dublado) \/ (.*?) \/", "$1 /"); - } - - // 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 string[] { " / " }; - titleElements = cleanDescription.Split(stringSeparators, StringSplitOptions.None); - // release.Title += string.Join(" ", titleElements); - release.Title = release.Title.Trim(); - - release.Title += " " + titleElements[5] + " " + titleElements[3] + " " + titleElements[1] + " " + titleElements[2] + " " + titleElements[4] + " " + string.Join(" ", titleElements.Skip(6).Take(titleElements.Length - 6).ToArray()); - - // 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)) + groupCategory = category; + groupTitle = title; + groupYearStr = yearStr; continue; - - var size = qSize.TextContent; - release.Size = ReleaseInfo.GetBytes(size); - release.Link = new Uri(SiteLink + qDlLink.GetAttribute("href")); - release.Comments = 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) + + var release = new ReleaseInfo { - logger.Error($"{ID}: Error while parsing row '{row.OuterHtml}': {ex.Message}"); + 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, categoryStr); + release.Category = groupCategory; } + else if (row.ClassList.Contains("torrent")) // standalone/un grouped torrents + { + release.Description = row.QuerySelector("div.torrent_info").TextContent; + release.Title = ParseTitle(title, seasonEp, yearStr, categoryStr); + release.Category = category; + } + + release.Description = release.Description.Replace(" / Free", ""); // Remove Free Tag + 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"); + // If it ain't nacional there will be the type of the audio / original audio + if (!release.Description.Contains("Nacional")) + release.Description = Regex.Replace( + release.Description, @"(Dual|Legendado|Dublado) \/ (.*?) \/", "$1 /"); + + // 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)); + + // 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.Comments = 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) - { - OnParseError(results.Content, ex); - } + catch (Exception ex) + { + logger.Error($"{ID}: Error while parsing row '{row.OuterHtml}': {ex.Message}"); + } + } + catch (Exception ex) + { + OnParseError(results.Content, ex); + } + + return releases; + } + + private async Task> ParseLast24HoursAsync() + { + var releases = new List(); + var results = await RequestStringWithCookies(TodayUrl); + if (results.IsRedirect) + { + // re-login + await ApplyConfiguration(null); + results = await RequestStringWithCookies(TodayUrl); + } + + try + { + const string rowsSelector = "table.torrent_table > tbody > tr:not(tr.colhead)"; + var searchResultParser = new HtmlParser(); + var searchResultDocument = searchResultParser.ParseDocument(results.Content); + var rows = searchResultDocument.QuerySelectorAll(rowsSelector); + foreach (var row in rows) + try + { + var release = new ReleaseInfo + { + MinimumRatio = 1, + MinimumSeedTime = 0 + }; + var qDetailsLink = row.QuerySelector("a.BJinfoBox"); + var qBJinfoBox = qDetailsLink.QuerySelector("span"); + var qCatLink = row.QuerySelector("a[href^=\"/torrents.php?filter_cat\"]"); + var qDlLink = row.QuerySelector("a[href^=\"torrents.php?action=download\"]"); + var qSeeders = row.QuerySelector("td:nth-child(4)"); + var qLeechers = row.QuerySelector("td:nth-child(5)"); + var qQuality = row.QuerySelector("font[color=\"red\"]"); + var qFreeLeech = row.QuerySelector("font[color=\"green\"]:contains(Free)"); + var qTitle = qDetailsLink.QuerySelector("font"); + // Get international title if available, or use the full title if not + release.Title = Regex.Replace(qTitle.TextContent, @".* \[(.*?)\](.*)", "$1$2"); + var year = ""; + release.Description = ""; + var extraInfo = ""; + foreach (var child in qBJinfoBox.ChildNodes) + { + var type = child.NodeType; + if (type != NodeType.Text) + continue; + var line = child.TextContent; + if (line.StartsWith("Tamanho:")) + { + var size = line.Substring("Tamanho: ".Length); + release.Size = ReleaseInfo.GetBytes(size); + } + else if (line.StartsWith("Lançado em: ")) + { + var publishDateStr = line.Substring("Lançado em: ".Length).Replace("às ", ""); + publishDateStr += " +0"; + var publishDate = DateTime.SpecifyKind( + DateTime.ParseExact(publishDateStr, "dd/MM/yyyy HH:mm z", CultureInfo.InvariantCulture), + DateTimeKind.Unspecified); + release.PublishDate = publishDate.ToLocalTime(); + } + else if (line.StartsWith("Ano:")) + year = line.Substring("Ano: ".Length); + else + { + release.Description += line + "\n"; + if (line.Contains(":")) + if (!(line.StartsWith("Lançado") || line.StartsWith("Resolução") || + line.StartsWith("Idioma") || line.StartsWith("Autor"))) + { + var info = line.Substring(line.IndexOf(": ", StringComparison.Ordinal) + 2); + if (info == "Dual Áudio") + info = "Dual"; + extraInfo += info + " "; + } + } + } + + var catStr = qCatLink.GetAttribute("href").Split('=')[1].Split('&')[0]; + release.Title = FixAbsoluteNumbering(release.Title); + if (!string.IsNullOrEmpty(year)) + release.Title += " " + year; + if (qQuality != null) + { + var quality = qQuality.TextContent; + release.Title += quality switch + { + "4K" => " 2160p", + "Full HD" => " 1080p", + "HD" => " 720p", + _ => " 480p" + }; + } + + release.Title += " " + extraInfo.TrimEnd(); + release.Category = MapTrackerCatToNewznab(catStr); + release.Link = new Uri(SiteLink + qDlLink.GetAttribute("href")); + release.Comments = new Uri(SiteLink + qDetailsLink.GetAttribute("href")); + release.Guid = release.Link; + 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.Content, ex); } return releases;