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 +
+ Viagem Alem do Tempo [Journey Beyond] - S03E07 [2027] + +
+
+
+
 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; }