This commit is contained in:
leandrobattochio 2026-05-02 20:00:34 +01:00 committed by GitHub
commit 1d344faf2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 767 additions and 0 deletions

View file

@ -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 = @"
<table class=""torrent_table cats grouping"" id=""torrent_table"">
<tr class=""torrent"">
<td></td>
<td class=""center cats_col""><a href='/torrents.php?filter_cat%5b2%5d=2'><img alt='Seriados' title='Seriados' /></a></td>
<td class=""big_info"">
<div class=""group_info clear"">
<span style=""padding-left: 0px"">
<span class=""add_bookmark float_right""><a href=""#"" class=""tooltip bookmarklink_torrent_111111"" title=""Adicionar aos Favoritos""><i class=""fad fa-bookmark""></i></a></span>
<span class=""download_torrent float_right""><a href=""torrents.php?action=download&amp;id=222222&amp;source=browse"" class=""tooltip"" title=""Baixar""><i class=""fad fa-download""></i></a>&nbsp;&nbsp;</span>
</span>
<a href=""series.php?id=3333"">Cidade Invisivel [Invisible City]</a> - <a href=""torrents.php?id=111111&amp;torrentid=222222"" class=""tooltip"" title=""View torrent group"" dir=""ltr""></a> [2021]
<div class=""torrent_info"" data-imdbid="""" data-audiotype=""Legendado"" data-videocodec=""x264"" data-audiocodec=""AC3"" data-language=""Portugues"" data-format=""MKV"" data-resolution=""HD"" data-name=""Cidade Invisivel [Invisible City] - [2021]"" data-localizedname="""" data-year=""2021"">[MKV / x264 / HDTV / HD / Legendado / <strong class=""torrent_label bjtooltip free"" title=""Free"">Free</strong>]</div>
<div class=""tags""></div>
</div>
</td>
<td><a href=""/user.php?username=""></a></td>
<td class=""number_column nobr""><span class=""time bjtooltip"" title=""May 02 2021, 20:22"">5 anos atras</span></td>
<td class=""number_column nobr"">92.05 GiB</td>
<td class=""number_column"">121</td>
<td class=""number_column"">6</td>
<td class=""number_column"">2</td>
</tr>
</table>";
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(98837938176);
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 = @"
<table class=""torrent_table cats grouping"" id=""torrent_table"">
<tr class=""colhead"">
<td class=""small""></td>
<td class=""small cats_col""></td>
<td onclick=""window.location='torrents.php?way=desc&order=name&searchstr=Journey+Beyond+S03E07&amp;tags_type=0&amp;action=basic&amp;searchsubmit=1'"" style=""cursor: pointer"">Nome</td>
<td>Uploader</td>
<td onclick=""window.location='torrents.php?way=asc&order=time&searchstr=Journey+Beyond+S03E07&amp;tags_type=0&amp;action=basic&amp;searchsubmit=1'"" style=""cursor: pointer"">Lancado ha</td>
<td onclick=""window.location='torrents.php?way=desc&order=size&searchstr=Journey+Beyond+S03E07&amp;tags_type=0&amp;action=basic&amp;searchsubmit=1'"" style=""cursor: pointer"">Tamanho</td>
<td class=""center""><i class=""fas fa-redo""></i></td>
<td class=""center""><i class=""fas fa-arrow-up""></i></td>
<td class=""center""><i class=""fas fa-arrow-down""></i></td>
</tr>
<tr class=""group"">
<td class=""center"">
<div id=""showimg_765432"" class=""hide_torrents"">
<a href=""#"" class=""tooltip show_torrents_link"" onclick=""toggle_group(765432, this, event)"" title=""Collapse this group.""></a>
</div>
</td>
<td class=""center cats_col""><a href='/torrents.php?filter_cat%5b2%5d=2'><img width='50' height='50' src='static/common/newcaticons3/seriadob4.svg' alt='Seriados' title='Seriados' class='brackets tooltip' /></a></td>
<td class=""big_info"">
<div class=""group_info clear"">
<a href=""series.php?id=4444"">Viagem Alem do Tempo [Journey Beyond]</a> - <a href=""torrents.php?id=765432"" class=""tooltip"" title=""View torrent group"" dir=""ltr"">S03E07</a> [2027]
<span class=""add_bookmark float_right""><a href=""#"" class=""tooltip bookmarklink_torrent_765432"" title=""Adicionar aos Favoritos""><i class=""fad fa-bookmark""></i></a></span>
<br/>
<div class=""tags""></div>
</div>
</td>
<td>&nbsp;</td>
<td class=""number_column nobr upload_time""><span class=""time bjtooltip"" title=""Mar 26 2027, 22:06"">1 semana atras</span></td>
<td class=""number_column nobr"">10.45 GiB (Max)</td>
<td class=""number_column"">394</td>
<td class=""number_column"">91</td>
<td class=""number_column"">0</td>
</tr>
<tr class=""group_torrent groupid_765432 edition_0"">
<td colspan=""3"">
<span><a href=""torrents.php?action=download&amp;id=888001&amp;source=browse"" class=""tooltip"" title=""Baixar""><i class=""fad fa-download""></i></a></span>
&nbsp;-&gt;&nbsp; <a href=""torrents.php?id=765432&amp;torrentid=888001"">[MKV / H.264 / WEB-DL / Full HD / Dolby Atmos / Dual Audio / <strong class=""torrentinfo_release"">StreamBox</strong> / <strong style=""color:red"">WANDER</strong> / <strong class=""torrent_label bjtooltip free"" title=""Free"">Free</strong>]</a>
</td>
<td><a href=""/user.php?username=""></a><br /><br /><span style=""color:red;font-weight:bold;float:none""><a href=""torrents.php?action=team&TeamID=55"" style=""color:red;font-weight:bold;float:none""><span style=""color:red;font-weight:bold;float:none"">WANDER</span></a></span></td>
<td class=""nobr""><span class=""time bjtooltip"" title=""Mar 26 2027, 22:02"">1 semana atras</span></td>
<td class=""number_column nobr"">4.58 GiB</td>
<td class=""number_column"">286</td>
<td class=""number_column"">74</td>
<td class=""number_column"">5</td>
</tr>
<tr class=""group_torrent groupid_765432 edition_0"">
<td colspan=""3"">
<span><a href=""torrents.php?action=download&amp;id=888002&amp;source=browse"" class=""tooltip"" title=""Baixar""><i class=""fad fa-download""></i></a></span>
&nbsp;-&gt;&nbsp; <a href=""torrents.php?id=765432&amp;torrentid=888002"">[MKV / H.265 / WEB-DL / 4K / Dolby Atmos / 10-bit / Dolby Vision / HDR10+ / Dual Audio / <strong class=""torrentinfo_release"">StreamBox</strong> / <strong class=""torrent_label bjtooltip free"" title=""Free"">Free</strong>]</a>
</td>
<td><a href=""/user.php?username=""></a></td>
<td class=""nobr""><span class=""time bjtooltip"" title=""Mar 26 2027, 22:06"">1 semana atras</span></td>
<td class=""number_column nobr"">10.45 GiB</td>
<td class=""number_column"">108</td>
<td class=""number_column"">17</td>
<td class=""number_column"">0</td>
</tr>
</table>";
var parser = new BjShareParser(new IndexerCapabilitiesCategories());
var releases = parser.ParseResponse(CreateResponse(html)).Cast<TorrentInfo>().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(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(11220601856);
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 = @"
<table class=""torrent_table cats grouping"" id=""torrent_table"">
<tr class=""colhead"">
<td class=""small""></td>
<td class=""small cats_col""></td>
<td>Nome</td>
<td>Uploader</td>
<td>Lancado ha</td>
<td>Tamanho</td>
<td class=""center""><i class=""fas fa-redo""></i></td>
<td class=""center""><i class=""fas fa-arrow-up""></i></td>
<td class=""center""><i class=""fas fa-arrow-down""></i></td>
</tr>
<tr class=""group"">
<td class=""center"">
<div id=""showimg_654321"" class=""hide_torrents"">
<a href=""#"" class=""tooltip show_torrents_link"" onclick=""toggle_group(654321, this, event)"" title=""Collapse this group.""></a>
</div>
</td>
<td class=""center cats_col""><a href='/torrents.php?filter_cat%5b1%5d=1'><img width='50' height='50' src='static/common/newcaticons3/filmes4.svg' alt='Filmes' title='Filmes' class='brackets tooltip' /></a></td>
<td class=""big_info"">
<div class=""group_info clear"">
<a href=""torrents.php?id=654321"" class=""tooltip"" title=""View torrent group"" dir=""ltr"">A Lua de Papel [Paper Moonlight]</a> [1989]
<span class=""add_bookmark float_right""><a href=""#"" class=""tooltip bookmarklink_torrent_654321"" title=""Adicionar aos Favoritos""><i class=""fad fa-bookmark""></i></a></span>
<br/>
<div class=""tags""></div>
</div>
</td>
<td>&nbsp;</td>
<td class=""number_column nobr upload_time""><span class=""time bjtooltip"" title=""Dec 07 2019, 15:46"">6 anos atras</span></td>
<td class=""number_column nobr"">12.90 GiB (Max)</td>
<td class=""number_column"">40</td>
<td class=""number_column"">0</td>
<td class=""number_column"">1</td>
</tr>
<tr class=""group_torrent groupid_654321 edition_0"">
<td colspan=""3"">
<span><a href=""torrents.php?action=download&amp;id=240001&amp;source=browse"" class=""tooltip"" title=""Baixar""><i class=""fad fa-download""></i></a></span>
&nbsp;-&gt;&nbsp; <a href=""torrents.php?id=654321&amp;torrentid=240001"">[MKV / H.264 / Blu-ray / Full HD / Legendado / <strong class=""torrent_label bjtooltip free"" title=""Free"">Free</strong>]</a>
</td>
<td><a href=""/user.php?username=""></a></td>
<td class=""nobr""><span class=""time bjtooltip"" title=""Dec 07 2019, 15:46"">6 anos atras</span></td>
<td class=""number_column nobr"">12.90 GiB</td>
<td class=""number_column"">40</td>
<td class=""number_column"">0</td>
<td class=""number_column"">1</td>
</tr>
</table>";
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(13851269120);
release.Grabs.Should().Be(40);
release.Seeders.Should().Be(0);
release.Peers.Should().Be(1);
}
}
}

View file

@ -0,0 +1,549 @@
using System;
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;
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<BjShareSettings>
{
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 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);
}
public override IParseIndexerResponse GetParser()
{
return new BjShareParser(Capabilities.Categories);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php?auth="))
{
throw new IndexerAuthException("BjShare authentication with cookies failed.");
}
return false;
}
protected override IDictionary<string, string> GetCookies()
{
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
}
private static IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam> { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep },
MovieSearchParams = new List<MovieSearchParam> { 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(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, "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 Adultos");
return caps;
}
}
public class BjShareRequestGenerator : IIndexerRequestGenerator
{
private readonly BjShareSettings _settings;
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public BjShareRequestGenerator(BjShareSettings settings)
{
_settings = settings;
}
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<int> forcedCategories)
{
var chain = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
var query = searchCriteria.SearchTerm?.Trim() ?? string.Empty;
int? seasonFromQuery = null;
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);
parameters.Set("action", "basic");
parameters.Set("searchsubmit", "1");
if (_settings.FreeleechOnly)
{
parameters.Set("freetorrent", "1");
}
foreach (var cat in forcedCategories ?? Array.Empty<int>())
{
parameters.Set($"filter_cat[{cat}]", "1");
}
var request = new HttpRequestBuilder($"{_settings.BaseUrl.TrimEnd('/')}/torrents.php?{parameters.GetQueryString()}")
.Accept(HttpAccept.Html)
.SetCookies(GetCookies() ?? new Dictionary<string, string>())
.Build();
chain.Add(new[] { new IndexerRequest(request) });
return chain;
}
}
public class BjShareParser : IParseIndexerResponse
{
private readonly IndexerCapabilitiesCategories _categories;
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public BjShareParser(IndexerCapabilitiesCategories categories)
{
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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 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);
return new GroupContext
{
Title = title,
Year = year,
SeasonEpisode = seasonInformation,
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 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, group.SeasonEpisode, infoText),
Categories = _categories.MapTrackerCatToNewznab(group.CategoryId.ToString()),
Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.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,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 604800
};
return release;
}
private ReleaseInfo ParseStandaloneTorrent(IElement row, string baseUrl)
{
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 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, seasonEpisode, infoText),
Categories = _categories.MapTrackerCatToNewznab(categoryId.ToString()),
Size = ParseSize(row.QuerySelector("td:nth-last-child(4)")?.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,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 604800
};
}
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 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*/|$)", "${1}720p", RegexOptions.IgnoreCase);
cleanInfo = cleanInfo.Replace(" / Free", "");
cleanInfo = Regex.Replace(cleanInfo, @"\s+", " ").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 string.Join(" ", new[]
{
title,
year?.ToString(CultureInfo.InvariantCulture),
seasonEpisode,
cleanInfo
}.Where(x => !string.IsNullOrWhiteSpace(x)));
}
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;
private static DateTime ParseBjDate(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return PublishDateFallback();
}
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) + new HttpUri(href)).FullUri;
}
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; }
}
}
public class BjShareSettingsValidator : CookieBaseSettingsValidator<BjShareSettings>;
public class BjShareSettings : CookieTorrentBaseSettings
{
private static readonly BjShareSettingsValidator Validator = new();
[FieldDefinition(3, Label = "IndexerSettingsFreeleechOnly", Type = FieldType.Checkbox)]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}