From 59d19f9ae87ed985de875769852536e872699862 Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Tue, 7 Apr 2026 00:37:39 -0300 Subject: [PATCH 1/7] Add BjShare C# definition --- .../Indexers/Definitions/BjShare.cs | 481 ++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 src/NzbDrone.Core/Indexers/Definitions/BjShare.cs diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs new file mode 100644 index 000000000..6a30db5e4 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using FluentValidation; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Indexers.Settings; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions; + +public class BjShare : TorrentIndexerBase +{ + public override string Name => "Bj-Share"; + public override string[] IndexerUrls => new[] { "https://bj-share.info/" }; + public override string Description => "Private PT-BR torrent tracker"; + public override string Language => "pt-BR"; + public override Encoding Encoding => Encoding.UTF8; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public BjShare( + IIndexerHttpClient httpClient, + IEventAggregator eventAggregator, + IIndexerStatusService indexerStatusService, + IConfigService configService, + Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new BjShareRequestGenerator(Settings, Capabilities); + } + + public override IParseIndexerResponse GetParser() + { + return new BjShareParser(Settings, Capabilities.Categories); + } + + protected override async Task DoLogin() + { + if (string.IsNullOrWhiteSpace(Settings.Cookie)) + { + throw new IndexerAuthException("BJ-Share cookie is empty"); + } + + var cookies = ParseCookieHeader(Settings.Cookie); + + if (cookies.Count == 0) + { + throw new IndexerAuthException("BJ-Share cookie is invalid"); + } + + UpdateCookies(cookies, DateTime.Now.AddDays(30)); + await Task.CompletedTask; + } + + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) + { + return httpResponse.RedirectUrl.Contains("login.php") || + !httpResponse.Content.Contains("/logout.php?auth="); + } + + private static IDictionary ParseCookieHeader(string rawCookie) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var part in rawCookie.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + var idx = part.IndexOf('='); + if (idx <= 0) + { + continue; + } + + var name = part[..idx].Trim(); + var value = part[(idx + 1)..].Trim(); + + if (!string.IsNullOrWhiteSpace(name)) + { + dict[name] = value; + } + } + + return dict; + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep }, + MovieSearchParams = new List { MovieSearchParam.Q }, + SupportsRawSearch = true + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Filmes"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.PC, "Aplicativos"); + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Jogos"); + caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.BooksComics, "Mangás"); + caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Other, "Outros"); + caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TVSport, "Esportes"); + caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.BooksMags, "Revistas"); + caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.Books, "E-Books"); + caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.AudioAudiobook, "Audiobook"); + caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.BooksComics, "HQ"); + caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.TV, "Stand Up Comedy"); + caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TVAnime, "Anime"); + caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.Other, "Cursos"); + caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.XXX, "Filmes Adultos"); + caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.XXXOther, "Jogos Adultos"); + caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.XXXOther, "Mangás Adultos"); + caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.XXXOther, "Animes Adultos"); + caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.XXXOther, "HQ Adultas"); + + return caps; + } +} + +public class BjShareRequestGenerator : IIndexerRequestGenerator +{ + private readonly BjShareSettings _settings; + private readonly IndexerCapabilities _capabilities; + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + + public BjShareRequestGenerator(BjShareSettings settings, IndexerCapabilities capabilities) + { + _settings = settings; + _capabilities = capabilities; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + return BuildSearch(searchCriteria, new[] { 2, 14 }); + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return BuildSearch(searchCriteria, new[] { 1 }); + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + return BuildSearch(searchCriteria, null); + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + return BuildSearch(searchCriteria, null); + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return BuildSearch(searchCriteria, null); + } + + private IndexerPageableRequestChain BuildSearch(SearchCriteriaBase searchCriteria, IEnumerable forcedCategories) + { + var chain = new IndexerPageableRequestChain(); + var parameters = new NameValueCollection(); + + var query = searchCriteria.SearchTerm?.Trim() ?? string.Empty; + + if (searchCriteria is TvSearchCriteria) + { + query = Regex.Replace(query, @"(S\d+E\d+|S\d+)", "", RegexOptions.IgnoreCase).Trim(); + } + + parameters.Set("searchstr", query); + parameters.Set("action", "basic"); + parameters.Set("searchsubmit", "1"); + + if (_settings.FreeleechOnly) + { + parameters.Set("freetorrent", "1"); + } + + foreach (var cat in forcedCategories ?? Array.Empty()) + { + parameters.Set($"filter_cat[{cat}]", "1"); + } + + var request = new HttpRequestBuilder($"{_settings.BaseUrl.TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}") + .Accept(HttpAccept.Html) + .SetCookies(GetCookies() ?? new Dictionary()) + .Build(); + + chain.Add(new[] { new IndexerRequest(request) }); + return chain; + } +} + +public class BjShareParser : IParseIndexerResponse +{ + private readonly BjShareSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + public Action, DateTime?> CookiesUpdater { get; set; } + + public BjShareParser(BjShareSettings settings, IndexerCapabilitiesCategories categories) + { + _settings = settings; + _categories = categories; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + var parser = new HtmlParser(); + + using var document = parser.ParseDocument(indexerResponse.Content); + + var rows = document.QuerySelectorAll("table#torrent_table tr, table.torrent_table tr"); + GroupContext currentGroup = null; + + foreach (var row in rows) + { + if (row.ClassList.Contains("colhead")) + { + continue; + } + + if (row.ClassList.Contains("group")) + { + currentGroup = ParseGroup(row, indexerResponse.Request.Url.FullUri); + continue; + } + + if (row.ClassList.Contains("group_torrent")) + { + if (currentGroup == null) + { + continue; + } + + var release = ParseGroupedTorrent(row, currentGroup, indexerResponse.Request.Url.FullUri); + if (release != null) + { + releases.Add(release); + } + + continue; + } + + if (row.ClassList.Contains("torrent")) + { + var release = ParseStandaloneTorrent(row, indexerResponse.Request.Url.FullUri); + if (release != null) + { + releases.Add(release); + } + } + } + + return releases; + } + + private GroupContext ParseGroup(IElement row, string baseUrl) + { + var titleCell = row.QuerySelector("div.group_info"); + var categoryHref = row.QuerySelector("td.cats_col a")?.GetAttribute("href") ?? string.Empty; + + var rawTitle = titleCell?.TextContent?.Trim() ?? string.Empty; + rawTitle = Regex.Replace(rawTitle, @"\s+", " "); + + var groupDetailsHref = row.QuerySelector("div.group_info a[href^=\"torrents.php?id=\"]")?.GetAttribute("href"); + var seriesHref = row.QuerySelector("div.group_info a[href^=\"series.php?id=\"]")?.GetAttribute("href"); + + var title = ExtractEnglishOrFallbackTitle(rawTitle); + var year = ExtractYear(rawTitle); + var categoryId = ExtractCategoryId(categoryHref); + + return new GroupContext + { + Title = title, + Year = year, + CategoryId = categoryId, + GroupDetailsUrl = ToAbsolute(baseUrl, groupDetailsHref), + SeriesUrl = ToAbsolute(baseUrl, seriesHref) + }; + } + + private ReleaseInfo ParseGroupedTorrent(IElement row, GroupContext group, string baseUrl) + { + var downloadHref = row.QuerySelector("a[title=\"Baixar\"]")?.GetAttribute("href"); + if (string.IsNullOrWhiteSpace(downloadHref)) + { + return null; + } + + var detailsHref = row.QuerySelector("a[href*=\"torrentid=\"]")?.GetAttribute("href"); + var infoText = row.QuerySelector("td[colspan=\"3\"] a[href*=\"torrentid=\"]")?.TextContent?.Trim() ?? string.Empty; + + var release = new TorrentInfo + { + Guid = ToAbsolute(baseUrl, downloadHref), + DownloadUrl = ToAbsolute(baseUrl, downloadHref), + InfoUrl = ToAbsolute(baseUrl, detailsHref), + Title = BuildReleaseTitle(group.Title, group.Year, infoText), + Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.TextContent), + Seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent), + Peers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent), + Grabs = ParseInt(row.QuerySelector("td:nth-last-child(3)")?.TextContent), + PublishDate = ParseBjDate(row.QuerySelector("td.nobr .time")?.GetAttribute("title")), + DownloadVolumeFactor = row.QuerySelector("strong[title*=\"Free\"]") != null ? 0 : 1, + UploadVolumeFactor = 1, + MinimumRatio = 1.0, + MinimumSeedTime = 604800 + }; + + var cats = _categories.MapTrackerCatToNewznab(group.CategoryId.ToString()); + if (cats.Any()) + { + release.Categories = cats; + } + + return release; + } + + private ReleaseInfo ParseStandaloneTorrent(IElement row, string baseUrl) + { + // Implementar se o BJ-Share realmente retornar esse formato para outras buscas. + return null; + } + + private static string ExtractEnglishOrFallbackTitle(string raw) + { + var match = Regex.Match(raw, @"^(.*?)\[(.*?)\]"); + if (match.Success) + { + var inner = match.Groups[2].Value.Trim(); + if (!string.IsNullOrWhiteSpace(inner) && !Regex.IsMatch(inner, @"^\d{4}$")) + { + return inner; + } + } + + return Regex.Replace(raw, @"\[\d{4}\].*$", "").Trim(); + } + + private static int? ExtractYear(string raw) + { + var match = Regex.Match(raw, @"\[(\d{4})\]"); + if (!match.Success) + { + return null; + } + + return int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + } + + private static int ExtractCategoryId(string href) + { + var match = Regex.Match(href ?? string.Empty, @"filter_cat%5b(\d+)%5d|filter_cat\[(\d+)\]"); + if (!match.Success) + { + return 0; + } + + var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + return int.TryParse(value, out var id) ? id : 0; + } + + private static string BuildReleaseTitle(string title, int? year, string infoText) + { + var cleanInfo = Regex.Replace(infoText ?? string.Empty, @"[\[\]]", ""); + cleanInfo = cleanInfo.Replace("Full HD", "1080p"); + cleanInfo = cleanInfo.Replace("4K", "2160p"); + cleanInfo = cleanInfo.Replace("SD", "480p"); + cleanInfo = cleanInfo.Replace(" / Free", ""); + cleanInfo = Regex.Replace(cleanInfo, @"\s+", " ").Trim(); + + return $"{title} {(year.HasValue ? year.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)} {cleanInfo}".Trim(); + } + + private static DateTime PublishDateFallback() => DateTime.UtcNow; + + private static DateTime ParseBjDate(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return PublishDateFallback(); + } + + var formats = new[] + { + "MMM dd yyyy, HH:mm", + "MMM dd yyyy, HH:mm", + "MMM dd yyyy, HH:mm" + }; + + if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)) + { + return parsed; + } + + return PublishDateFallback(); + } + + private static long ParseSize(string value) + { + return ParseUtil.GetBytes(value); + } + + private static int ParseInt(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return 0; + } + + return ParseUtil.CoerceInt(value.Replace(",", "").Trim()); + } + + private static string ToAbsolute(string baseUrl, string href) + { + if (string.IsNullOrWhiteSpace(href)) + { + return null; + } + + return new HttpUri(baseUrl).CombinePath(href).FullUri; + } + + private sealed class GroupContext + { + public string Title { get; set; } + public int? Year { get; set; } + public int CategoryId { get; set; } + public string GroupDetailsUrl { get; set; } + public string SeriesUrl { get; set; } + } +} + +public class BjShareSettingsValidator : NoAuthSettingsValidator +{ + public BjShareSettingsValidator() + { + RuleFor(x => x.Cookie).NotEmpty(); + RuleFor(x => x.BaseUrl).NotEmpty(); + } +} + +public class BjShareSettings : NoAuthTorrentBaseSettings +{ + private static readonly BjShareSettingsValidator Validator = new(); + + [FieldDefinition(1, Label = "Cookie", Type = FieldType.Textbox, HelpText = "Cookie completo da sessão autenticada")] + public string Cookie { get; set; } + + [FieldDefinition(2, Label = "Freeleech only", Type = FieldType.Checkbox)] + public bool FreeleechOnly { get; set; } + + public BjShareSettings() + { + BaseUrl = "https://bj-share.info/"; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } +} From 6739d14c8599ce2cf1a872d98a68cb8260c6493e Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Tue, 7 Apr 2026 00:49:08 -0300 Subject: [PATCH 2/7] Fix season parsing --- .../Indexers/Definitions/BjShare.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index 6a30db5e4..e25b64be9 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -179,10 +179,34 @@ private IndexerPageableRequestChain BuildSearch(SearchCriteriaBase searchCriteri var parameters = new NameValueCollection(); var query = searchCriteria.SearchTerm?.Trim() ?? string.Empty; + int? seasonFromQuery = null; - if (searchCriteria is TvSearchCriteria) + var seasonMatch = Regex.Match(query, @"\{season:\s*(\d+)\}", RegexOptions.IgnoreCase); + if (seasonMatch.Success) + { + seasonFromQuery = int.Parse(seasonMatch.Groups[1].Value, CultureInfo.InvariantCulture); + query = Regex.Replace(query, @"\{season:\s*\d+\}", "", RegexOptions.IgnoreCase).Trim(); + } + + var episodeMatch = Regex.Match(query, @"\{episode:\s*(\d+)\}", RegexOptions.IgnoreCase); + if (episodeMatch.Success) + { + query = Regex.Replace(query, @"\{episode:\s*\d+\}", "", RegexOptions.IgnoreCase).Trim(); + } + + if (searchCriteria is TvSearchCriteria tv) { query = Regex.Replace(query, @"(S\d+E\d+|S\d+)", "", RegexOptions.IgnoreCase).Trim(); + + var season = tv.Season ?? seasonFromQuery; + if (season.HasValue) + { + query = $"{query} S{season.Value:00}"; + } + } + else if (seasonFromQuery.HasValue) + { + query = $"{query} S{seasonFromQuery.Value:00}"; } parameters.Set("searchstr", query); From bde4ac29cb83fd043da5f2cc11eb4d8bd289c653 Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Tue, 7 Apr 2026 10:26:03 -0300 Subject: [PATCH 3/7] Fix code review comments and implement BjShare standalone torrent search --- .../Indexers/Definitions/BjShare.cs | 142 +++++++----------- 1 file changed, 53 insertions(+), 89 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index e25b64be9..4a34015a6 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -2,13 +2,9 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using AngleSharp.Dom; using AngleSharp.Html.Parser; -using FluentValidation; using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Annotations; @@ -29,7 +25,6 @@ public class BjShare : TorrentIndexerBase public override string[] IndexerUrls => new[] { "https://bj-share.info/" }; public override string Description => "Private PT-BR torrent tracker"; public override string Language => "pt-BR"; - public override Encoding Encoding => Encoding.UTF8; public override IndexerPrivacy Privacy => IndexerPrivacy.Private; public override IndexerCapabilities Capabilities => SetCapabilities(); @@ -45,63 +40,30 @@ public BjShare( public override IIndexerRequestGenerator GetRequestGenerator() { - return new BjShareRequestGenerator(Settings, Capabilities); + return new BjShareRequestGenerator(Settings); } public override IParseIndexerResponse GetParser() { - return new BjShareParser(Settings, Capabilities.Categories); - } - - protected override async Task DoLogin() - { - if (string.IsNullOrWhiteSpace(Settings.Cookie)) - { - throw new IndexerAuthException("BJ-Share cookie is empty"); - } - - var cookies = ParseCookieHeader(Settings.Cookie); - - if (cookies.Count == 0) - { - throw new IndexerAuthException("BJ-Share cookie is invalid"); - } - - UpdateCookies(cookies, DateTime.Now.AddDays(30)); - await Task.CompletedTask; + return new BjShareParser(Capabilities.Categories); } protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) { - return httpResponse.RedirectUrl.Contains("login.php") || - !httpResponse.Content.Contains("/logout.php?auth="); - } - - private static IDictionary ParseCookieHeader(string rawCookie) - { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var part in rawCookie.Split(';', StringSplitOptions.RemoveEmptyEntries)) + if (!httpResponse.Content.Contains("logout.php?auth=")) { - var idx = part.IndexOf('='); - if (idx <= 0) - { - continue; - } - - var name = part[..idx].Trim(); - var value = part[(idx + 1)..].Trim(); - - if (!string.IsNullOrWhiteSpace(name)) - { - dict[name] = value; - } + throw new IndexerAuthException("BjShare authentication with cookies failed."); } - return dict; + return false; } - private IndexerCapabilities SetCapabilities() + protected override IDictionary GetCookies() + { + return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); + } + + private static IndexerCapabilities SetCapabilities() { var caps = new IndexerCapabilities { @@ -137,15 +99,13 @@ private IndexerCapabilities SetCapabilities() public class BjShareRequestGenerator : IIndexerRequestGenerator { private readonly BjShareSettings _settings; - private readonly IndexerCapabilities _capabilities; public Func> GetCookies { get; set; } public Action, DateTime?> CookiesUpdater { get; set; } - public BjShareRequestGenerator(BjShareSettings settings, IndexerCapabilities capabilities) + public BjShareRequestGenerator(BjShareSettings settings) { _settings = settings; - _capabilities = capabilities; } public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) @@ -235,13 +195,12 @@ private IndexerPageableRequestChain BuildSearch(SearchCriteriaBase searchCriteri public class BjShareParser : IParseIndexerResponse { - private readonly BjShareSettings _settings; private readonly IndexerCapabilitiesCategories _categories; + public Action, DateTime?> CookiesUpdater { get; set; } - public BjShareParser(BjShareSettings settings, IndexerCapabilitiesCategories categories) + public BjShareParser(IndexerCapabilitiesCategories categories) { - _settings = settings; _categories = categories; } @@ -339,6 +298,7 @@ private ReleaseInfo ParseGroupedTorrent(IElement row, GroupContext group, string DownloadUrl = ToAbsolute(baseUrl, downloadHref), InfoUrl = ToAbsolute(baseUrl, detailsHref), Title = BuildReleaseTitle(group.Title, group.Year, infoText), + Categories = _categories.MapTrackerCatToNewznab(group.CategoryId.ToString()), Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.TextContent), Seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent), Peers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent), @@ -346,23 +306,49 @@ private ReleaseInfo ParseGroupedTorrent(IElement row, GroupContext group, string PublishDate = ParseBjDate(row.QuerySelector("td.nobr .time")?.GetAttribute("title")), DownloadVolumeFactor = row.QuerySelector("strong[title*=\"Free\"]") != null ? 0 : 1, UploadVolumeFactor = 1, - MinimumRatio = 1.0, + MinimumRatio = 1, MinimumSeedTime = 604800 }; - var cats = _categories.MapTrackerCatToNewznab(group.CategoryId.ToString()); - if (cats.Any()) - { - release.Categories = cats; - } - return release; } private ReleaseInfo ParseStandaloneTorrent(IElement row, string baseUrl) { - // Implementar se o BJ-Share realmente retornar esse formato para outras buscas. - return null; + var downloadHref = row.QuerySelector("span.download_torrent a")?.GetAttribute("href"); + if (string.IsNullOrWhiteSpace(downloadHref)) + { + return null; + } + + var detailsHref = row.QuerySelector("a[href*=\"torrentid=\"]")?.GetAttribute("href"); + var categoryHref = row.QuerySelector("td.cats_col a")?.GetAttribute("href") ?? string.Empty; + + var rawTitle = row.QuerySelector("div.group_info a[href^=\"series.php\"]")?.TextContent?.Trim() ?? string.Empty; + var groupInfoText = Regex.Replace(row.QuerySelector("div.group_info")?.TextContent?.Trim() ?? string.Empty, @"\s+", " "); + var infoText = row.QuerySelector("div.torrent_info")?.TextContent?.Trim() ?? string.Empty; + + var title = ExtractEnglishOrFallbackTitle(rawTitle); + var year = ExtractYear(groupInfoText); + var categoryId = ExtractCategoryId(categoryHref); + + return new TorrentInfo + { + Guid = ToAbsolute(baseUrl, downloadHref), + DownloadUrl = ToAbsolute(baseUrl, downloadHref), + InfoUrl = ToAbsolute(baseUrl, detailsHref), + Title = BuildReleaseTitle(title, year, infoText), + Categories = _categories.MapTrackerCatToNewznab(categoryId.ToString()), + Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.TextContent), + Seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent), + Peers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent), + Grabs = ParseInt(row.QuerySelector("td:nth-last-child(3)")?.TextContent), + PublishDate = ParseBjDate(row.QuerySelector("td.nobr .time")?.GetAttribute("title")), + DownloadVolumeFactor = row.QuerySelector("strong[title*=\"Free\"]") != null ? 0 : 1, + UploadVolumeFactor = 1, + MinimumRatio = 1, + MinimumSeedTime = 604800 + }; } private static string ExtractEnglishOrFallbackTitle(string raw) @@ -424,13 +410,6 @@ private static DateTime ParseBjDate(string value) return PublishDateFallback(); } - var formats = new[] - { - "MMM dd yyyy, HH:mm", - "MMM dd yyyy, HH:mm", - "MMM dd yyyy, HH:mm" - }; - if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)) { return parsed; @@ -474,30 +453,15 @@ private sealed class GroupContext } } -public class BjShareSettingsValidator : NoAuthSettingsValidator -{ - public BjShareSettingsValidator() - { - RuleFor(x => x.Cookie).NotEmpty(); - RuleFor(x => x.BaseUrl).NotEmpty(); - } -} +public class BjShareSettingsValidator : CookieBaseSettingsValidator; -public class BjShareSettings : NoAuthTorrentBaseSettings +public class BjShareSettings : CookieTorrentBaseSettings { private static readonly BjShareSettingsValidator Validator = new(); - [FieldDefinition(1, Label = "Cookie", Type = FieldType.Textbox, HelpText = "Cookie completo da sessão autenticada")] - public string Cookie { get; set; } - - [FieldDefinition(2, Label = "Freeleech only", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox)] public bool FreeleechOnly { get; set; } - public BjShareSettings() - { - BaseUrl = "https://bj-share.info/"; - } - public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); From 719d281ad708f8b412632b71183a82e5516d9231 Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Thu, 9 Apr 2026 22:52:55 -0300 Subject: [PATCH 4/7] Fix BjShare torrent parsing --- .../BjShareTests/BjShareFixture.cs | 218 ++++++++++++++++++ .../Indexers/Definitions/BjShare.cs | 95 +++++++- 2 files changed, 302 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs diff --git a/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs new file mode 100644 index 000000000..0fa76c676 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs @@ -0,0 +1,218 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Test.IndexerTests.BjShareTests +{ + [TestFixture] + public class BjShareFixture + { + private static IndexerResponse CreateResponse(string content) + { + var httpRequest = new HttpRequest("https://bj-share.info/torrents.php?searchstr=test&action=basic&searchsubmit=1"); + var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(content)); + + return new IndexerResponse(new IndexerRequest(httpRequest), httpResponse); + } + + [Test] + public void should_parse_individual_torrent_row_from_search_results() + { + const string html = @" + + + + + + + + + + + + +
Seriados +
+ + +    + + Cidade Invisivel [Invisible City] - [2021] +
[MKV / x264 / HDTV / HD / Legendado / Free]
+
+
+
5 anos atras92.05 GiB12162
"; + + var parser = new BjShareParser(new IndexerCapabilitiesCategories()); + + var release = parser.ParseResponse(CreateResponse(html)).Single() as TorrentInfo; + + release.Title.Should().Be("Invisible City 2021 MKV / x264 / HDTV / 720p / Legendado"); + release.DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=222222&source=browse"); + release.InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=111111&torrentid=222222"); + release.PublishDate.Should().Be(DateTime.Parse("May 02 2021, 20:22", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + release.Size.Should().Be(98837934899); + release.Grabs.Should().Be(121); + release.Seeders.Should().Be(6); + release.Peers.Should().Be(8); + } + + [Test] + public void should_parse_full_grouped_tv_search_results_table() + { + const string html = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NomeUploaderLancado haTamanho
+
+ +
+
Seriados + +  1 semana atras10.45 GiB (Max)394910
+ +  ->  [MKV / H.264 / WEB-DL / Full HD / Dolby Atmos / Dual Audio / StreamBox / WANDER / Free] +

WANDER
1 semana atras4.58 GiB286745
+ +  ->  [MKV / H.265 / WEB-DL / 4K / Dolby Atmos / 10-bit / Dolby Vision / HDR10+ / Dual Audio / StreamBox / Free] + 1 semana atras10.45 GiB108170
"; + + var parser = new BjShareParser(new IndexerCapabilitiesCategories()); + + var releases = parser.ParseResponse(CreateResponse(html)).Cast().ToList(); + + releases.Should().HaveCount(2); + releases[0].Title.Should().Be("Journey Beyond 2027 S03E07 MKV / H.264 / WEB-DL / 1080p / Dolby Atmos / Dual Audio / StreamBox / WANDER"); + releases[0].DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=888001&source=browse"); + releases[0].InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=765432&torrentid=888001"); + releases[0].PublishDate.Should().Be(DateTime.Parse("Mar 26 2027, 22:02", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + releases[0].Size.Should().Be(4917737553); + releases[0].Seeders.Should().Be(74); + releases[0].Peers.Should().Be(79); + + releases[1].Title.Should().Be("Journey Beyond 2027 S03E07 MKV / H.265 / WEB-DL / 2160p / Dolby Atmos / 10-bit / Dolby Vision / HDR10+ / Dual Audio / StreamBox"); + releases[1].DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=888002&source=browse"); + releases[1].Size.Should().Be(11220602060); + releases[1].Seeders.Should().Be(17); + releases[1].Peers.Should().Be(17); + } + + [Test] + public void should_parse_full_grouped_movie_search_results_table_with_year_outside_anchor() + { + const string html = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NomeUploaderLancado haTamanho
+
+ +
+
Filmes + +  6 anos atras12.90 GiB (Max)4001
+ +  ->  [MKV / H.264 / Blu-ray / Full HD / Legendado / Free] + 6 anos atras12.90 GiB4001
"; + + var parser = new BjShareParser(new IndexerCapabilitiesCategories()); + + var release = parser.ParseResponse(CreateResponse(html)).Single() as TorrentInfo; + + release.Title.Should().Be("Paper Moonlight 1989 MKV / H.264 / Blu-ray / 1080p / Legendado"); + release.DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=240001&source=browse"); + release.InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=654321&torrentid=240001"); + release.PublishDate.Should().Be(DateTime.Parse("Dec 07 2019, 15:46", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + release.Size.Should().Be(13851269529); + release.Grabs.Should().Be(40); + release.Seeders.Should().Be(0); + release.Peers.Should().Be(1); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index 4a34015a6..dfad99e8d 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -77,20 +77,24 @@ private static IndexerCapabilities SetCapabilities() caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.PC, "Aplicativos"); caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.PCGames, "Jogos"); caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.BooksComics, "Mangás"); + caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TV, "Vídeos de TV"); caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Other, "Outros"); caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TVSport, "Esportes"); caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.BooksMags, "Revistas"); caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.Books, "E-Books"); caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.AudioAudiobook, "Audiobook"); - caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.BooksComics, "HQ"); + caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.BooksComics, "Histórias em Quadrinhos"); caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.TV, "Stand Up Comedy"); caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TVAnime, "Anime"); + caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.XXXImageSet, "Fotos Adultas"); + caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.TVOther, "Desenho Animado"); + caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.TVDocumentary, "Documentários"); caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.Other, "Cursos"); caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.XXX, "Filmes Adultos"); caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.XXXOther, "Jogos Adultos"); caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.XXXOther, "Mangás Adultos"); caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.XXXOther, "Animes Adultos"); - caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.XXXOther, "HQ Adultas"); + caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.XXXOther, "HQ Adultos"); return caps; } @@ -267,7 +271,15 @@ private GroupContext ParseGroup(IElement row, string baseUrl) var groupDetailsHref = row.QuerySelector("div.group_info a[href^=\"torrents.php?id=\"]")?.GetAttribute("href"); var seriesHref = row.QuerySelector("div.group_info a[href^=\"series.php?id=\"]")?.GetAttribute("href"); - var title = ExtractEnglishOrFallbackTitle(rawTitle); + var groupDetailsText = row.QuerySelector("div.group_info a[href^=\"torrents.php?id=\"][title=\"View torrent group\"]")?.TextContent?.Trim(); + var titleSource = row.QuerySelector("div.group_info a[href^=\"series.php?id=\"]")?.TextContent?.Trim(); + if (string.IsNullOrWhiteSpace(titleSource)) + { + titleSource = RemoveGroupDetailsText(rawTitle, groupDetailsText); + } + + var seasonInformation = ExtractSeasonEpisode(groupDetailsText); + var title = ExtractEnglishOrFallbackTitle(string.IsNullOrWhiteSpace(titleSource) ? rawTitle : titleSource); var year = ExtractYear(rawTitle); var categoryId = ExtractCategoryId(categoryHref); @@ -275,6 +287,7 @@ private GroupContext ParseGroup(IElement row, string baseUrl) { Title = title, Year = year, + SeasonEpisode = seasonInformation, CategoryId = categoryId, GroupDetailsUrl = ToAbsolute(baseUrl, groupDetailsHref), SeriesUrl = ToAbsolute(baseUrl, seriesHref) @@ -291,17 +304,18 @@ private ReleaseInfo ParseGroupedTorrent(IElement row, GroupContext group, string var detailsHref = row.QuerySelector("a[href*=\"torrentid=\"]")?.GetAttribute("href"); var infoText = row.QuerySelector("td[colspan=\"3\"] a[href*=\"torrentid=\"]")?.TextContent?.Trim() ?? string.Empty; + var peerStats = ParsePeerStats(row); var release = new TorrentInfo { Guid = ToAbsolute(baseUrl, downloadHref), DownloadUrl = ToAbsolute(baseUrl, downloadHref), InfoUrl = ToAbsolute(baseUrl, detailsHref), - Title = BuildReleaseTitle(group.Title, group.Year, infoText), + Title = BuildReleaseTitle(group.Title, group.Year, group.SeasonEpisode, infoText), Categories = _categories.MapTrackerCatToNewznab(group.CategoryId.ToString()), Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.TextContent), - Seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent), - Peers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent), + Seeders = peerStats.Seeders, + Peers = peerStats.Peers, Grabs = ParseInt(row.QuerySelector("td:nth-last-child(3)")?.TextContent), PublishDate = ParseBjDate(row.QuerySelector("td.nobr .time")?.GetAttribute("title")), DownloadVolumeFactor = row.QuerySelector("strong[title*=\"Free\"]") != null ? 0 : 1, @@ -330,18 +344,21 @@ private ReleaseInfo ParseStandaloneTorrent(IElement row, string baseUrl) var title = ExtractEnglishOrFallbackTitle(rawTitle); var year = ExtractYear(groupInfoText); + var seasonLink = row.QuerySelector("div.group_info a.tooltip[href^=\"torrents.php\"]"); + var seasonEpisode = seasonLink?.TextContent?.Trim() ?? string.Empty; var categoryId = ExtractCategoryId(categoryHref); + var peerStats = ParsePeerStats(row); return new TorrentInfo { Guid = ToAbsolute(baseUrl, downloadHref), DownloadUrl = ToAbsolute(baseUrl, downloadHref), InfoUrl = ToAbsolute(baseUrl, detailsHref), - Title = BuildReleaseTitle(title, year, infoText), + Title = BuildReleaseTitle(title, year, seasonEpisode, infoText), Categories = _categories.MapTrackerCatToNewznab(categoryId.ToString()), Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.TextContent), - Seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent), - Peers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent), + Seeders = peerStats.Seeders, + Peers = peerStats.Peers, Grabs = ParseInt(row.QuerySelector("td:nth-last-child(3)")?.TextContent), PublishDate = ParseBjDate(row.QuerySelector("td.nobr .time")?.GetAttribute("title")), DownloadVolumeFactor = row.QuerySelector("strong[title*=\"Free\"]") != null ? 0 : 1, @@ -389,16 +406,71 @@ private static int ExtractCategoryId(string href) return int.TryParse(value, out var id) ? id : 0; } - private static string BuildReleaseTitle(string title, int? year, string infoText) + private static string RemoveGroupDetailsText(string rawTitle, string groupDetailsText) + { + var title = rawTitle ?? string.Empty; + if (!string.IsNullOrWhiteSpace(groupDetailsText)) + { + title = Regex.Replace(title, $@"\s*-\s*{Regex.Escape(groupDetailsText)}(?=\s*\[|$)", string.Empty); + } + + title = Regex.Replace(title, @"\s*-\s*(?=\[(?:19|20)\d{2}\])", " "); + + return title.Trim(); + } + + private static string ExtractSeasonEpisode(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var match = Regex.Match(value, @"\b[ST](\d{1,2})(?:\s*E(\d{1,3}))?\b", RegexOptions.IgnoreCase); + if (!match.Success) + { + return string.Empty; + } + + var season = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + if (!match.Groups[2].Success) + { + return $"S{season:00}"; + } + + var episode = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + return $"S{season:00}E{episode:00}"; + } + + private static string BuildReleaseTitle(string title, int? year, string seasonEpisode, string infoText) { var cleanInfo = Regex.Replace(infoText ?? string.Empty, @"[\[\]]", ""); cleanInfo = cleanInfo.Replace("Full HD", "1080p"); cleanInfo = cleanInfo.Replace("4K", "2160p"); cleanInfo = cleanInfo.Replace("SD", "480p"); + cleanInfo = Regex.Replace(cleanInfo, @"(^|/\s*)HD(?=\s*/|$)", "$1720p", RegexOptions.IgnoreCase); cleanInfo = cleanInfo.Replace(" / Free", ""); cleanInfo = Regex.Replace(cleanInfo, @"\s+", " ").Trim(); - return $"{title} {(year.HasValue ? year.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)} {cleanInfo}".Trim(); + if (!string.IsNullOrWhiteSpace(seasonEpisode) && Regex.IsMatch(cleanInfo, $@"\b{Regex.Escape(seasonEpisode)}\b", RegexOptions.IgnoreCase)) + { + seasonEpisode = string.Empty; + } + + if (!string.IsNullOrWhiteSpace(seasonEpisode) && Regex.IsMatch(title ?? string.Empty, $@"\b{Regex.Escape(seasonEpisode)}\b", RegexOptions.IgnoreCase)) + { + seasonEpisode = string.Empty; + } + + return $"{title} {(year.HasValue ? year.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)} {seasonEpisode} {cleanInfo}".Trim(); + } + + private static (int Seeders, int Peers) ParsePeerStats(IElement row) + { + var seeders = ParseInt(row.QuerySelector("td:nth-last-child(2)")?.TextContent); + var leechers = ParseInt(row.QuerySelector("td:nth-last-child(1)")?.TextContent); + + return (seeders, seeders + leechers); } private static DateTime PublishDateFallback() => DateTime.UtcNow; @@ -447,6 +519,7 @@ private sealed class GroupContext { public string Title { get; set; } public int? Year { get; set; } + public string SeasonEpisode { get; set; } public int CategoryId { get; set; } public string GroupDetailsUrl { get; set; } public string SeriesUrl { get; set; } From 2000d0c7682d79764bcec98bfdd0e677ada8fb0b Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Thu, 9 Apr 2026 23:16:37 -0300 Subject: [PATCH 5/7] Fix BjShare title formatting --- src/NzbDrone.Core/Indexers/Definitions/BjShare.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index dfad99e8d..d31aaf3b0 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -448,7 +448,7 @@ private static string BuildReleaseTitle(string title, int? year, string seasonEp cleanInfo = cleanInfo.Replace("Full HD", "1080p"); cleanInfo = cleanInfo.Replace("4K", "2160p"); cleanInfo = cleanInfo.Replace("SD", "480p"); - cleanInfo = Regex.Replace(cleanInfo, @"(^|/\s*)HD(?=\s*/|$)", "$1720p", RegexOptions.IgnoreCase); + cleanInfo = Regex.Replace(cleanInfo, @"(^|/\s*)HD(?=\s*/|$)", "${1}720p", RegexOptions.IgnoreCase); cleanInfo = cleanInfo.Replace(" / Free", ""); cleanInfo = Regex.Replace(cleanInfo, @"\s+", " ").Trim(); @@ -462,7 +462,13 @@ private static string BuildReleaseTitle(string title, int? year, string seasonEp seasonEpisode = string.Empty; } - return $"{title} {(year.HasValue ? year.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)} {seasonEpisode} {cleanInfo}".Trim(); + return string.Join(" ", new[] + { + title, + year?.ToString(CultureInfo.InvariantCulture), + seasonEpisode, + cleanInfo + }.Where(x => !string.IsNullOrWhiteSpace(x))); } private static (int Seeders, int Peers) ParsePeerStats(IElement row) From baee6a6d33700e33c1574fadf77ca96c894fbd12 Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Thu, 9 Apr 2026 23:25:23 -0300 Subject: [PATCH 6/7] Add missing BjShare Linq import --- src/NzbDrone.Core/Indexers/Definitions/BjShare.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index d31aaf3b0..6f29a7159 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; using AngleSharp.Dom; using AngleSharp.Html.Parser; From 80d82e798d34e6e675fb1698a9e42b74d8d834ae Mon Sep 17 00:00:00 2001 From: Leandro Battochio Date: Thu, 9 Apr 2026 23:44:53 -0300 Subject: [PATCH 7/7] Fix BjShare URL and size assertions --- .../IndexerTests/BjShareTests/BjShareFixture.cs | 8 ++++---- src/NzbDrone.Core/Indexers/Definitions/BjShare.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs index 0fa76c676..09e8e3f9a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BjShareTests/BjShareFixture.cs @@ -59,7 +59,7 @@ public void should_parse_individual_torrent_row_from_search_results() release.DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=222222&source=browse"); release.InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=111111&torrentid=222222"); release.PublishDate.Should().Be(DateTime.Parse("May 02 2021, 20:22", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); - release.Size.Should().Be(98837934899); + release.Size.Should().Be(98837938176); release.Grabs.Should().Be(121); release.Seeders.Should().Be(6); release.Peers.Should().Be(8); @@ -138,13 +138,13 @@ public void should_parse_full_grouped_tv_search_results_table() releases[0].DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=888001&source=browse"); releases[0].InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=765432&torrentid=888001"); releases[0].PublishDate.Should().Be(DateTime.Parse("Mar 26 2027, 22:02", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); - releases[0].Size.Should().Be(4917737553); + releases[0].Size.Should().Be(4917737472); releases[0].Seeders.Should().Be(74); releases[0].Peers.Should().Be(79); releases[1].Title.Should().Be("Journey Beyond 2027 S03E07 MKV / H.265 / WEB-DL / 2160p / Dolby Atmos / 10-bit / Dolby Vision / HDR10+ / Dual Audio / StreamBox"); releases[1].DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=888002&source=browse"); - releases[1].Size.Should().Be(11220602060); + releases[1].Size.Should().Be(11220601856); releases[1].Seeders.Should().Be(17); releases[1].Peers.Should().Be(17); } @@ -209,7 +209,7 @@ public void should_parse_full_grouped_movie_search_results_table_with_year_outsi release.DownloadUrl.Should().Be("https://bj-share.info/torrents.php?action=download&id=240001&source=browse"); release.InfoUrl.Should().Be("https://bj-share.info/torrents.php?id=654321&torrentid=240001"); release.PublishDate.Should().Be(DateTime.Parse("Dec 07 2019, 15:46", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); - release.Size.Should().Be(13851269529); + release.Size.Should().Be(13851269120); release.Grabs.Should().Be(40); release.Seeders.Should().Be(0); release.Peers.Should().Be(1); diff --git a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs index 6f29a7159..6b3a5cb4d 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/BjShare.cs @@ -519,7 +519,7 @@ private static string ToAbsolute(string baseUrl, string href) return null; } - return new HttpUri(baseUrl).CombinePath(href).FullUri; + return (new HttpUri(baseUrl) + new HttpUri(href)).FullUri; } private sealed class GroupContext