From 14860ff3969d306a88e76cbf83a871b220b55ff5 Mon Sep 17 00:00:00 2001 From: vgveloso Date: Tue, 17 Dec 2024 21:27:05 -0500 Subject: [PATCH] Expand Brazilian Portuguese Torrent Support with New Indexers (#15713) Indexers: ApacheTorrent, RedeTorrent, TorrentDosFilmes, FilmesHdTorrent, LAPUMiA, BluDV and TorrentsMegaFilmes. Mostly usable with Radarr, not so much with Sonarr. --- README.md | 6 + .../Abstract/PublicBrazilianIndexerBase.cs | 396 ++++++++++++++++++ .../Indexers/Definitions/ApacheTorrent.cs | 166 ++++++++ .../Indexers/Definitions/BluDV.cs | 107 +++++ .../Indexers/Definitions/FilmesHdTorrent.cs | 140 +++++++ .../Indexers/Definitions/LAPUMiA.cs | 128 ++++++ .../Indexers/Definitions/RedeTorrent.cs | 167 ++++++++ .../Indexers/Definitions/TorrentDosFilmes.cs | 134 ++++++ .../Definitions/TorrentsMegaFilmes.cs | 87 ++++ 9 files changed, 1331 insertions(+) create mode 100644 src/Jackett.Common/Indexers/Definitions/Abstract/PublicBrazilianIndexerBase.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/ApacheTorrent.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/BluDV.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/FilmesHdTorrent.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/LAPUMiA.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/RedeTorrent.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/TorrentDosFilmes.cs create mode 100644 src/Jackett.Common/Indexers/Definitions/TorrentsMegaFilmes.cs diff --git a/README.md b/README.md index fe010144f..a41f67360 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,14 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * Anime Tosho * AniRena * AniSource + * ApacheTorrent * AudioBook Bay (ABB) * Badass Torrents * Bangumi Moe * BigFANGroup * BitRu * BitSearch + * BluDV * BlueRoms * BT.etree * BTdirectory (BT目录) @@ -62,6 +64,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * EXT Torrents * ExtraTorrent.st * EZTV + * FilmesHdTorrent * Frozen Layer * GamesTorrents * GkTorrent @@ -76,6 +79,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * kickasstorrents.to * kickasstorrents.ws * Knaben + * LAPUMiA * LePorno.info * Libronube * LimeTorrents @@ -106,6 +110,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * Postman * ProPorn * Rapidzona + * RedeTorrent * RinTorNeT * RuTor * RuTracker.RU @@ -130,6 +135,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * Torrent9 * Torrent9-tel * TorrentFunk + * TorrentDosFilmes * TorrentDownload * TorrentKitty * TorrentProject2 diff --git a/src/Jackett.Common/Indexers/Definitions/Abstract/PublicBrazilianIndexerBase.cs b/src/Jackett.Common/Indexers/Definitions/Abstract/PublicBrazilianIndexerBase.cs new file mode 100644 index 000000000..5e5b53ad1 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/Abstract/PublicBrazilianIndexerBase.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using AngleSharp.Dom; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Newtonsoft.Json.Linq; +using NLog; +using static System.Linq.Enumerable; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers.Definitions.Abstract +{ + public abstract class PublicBrazilianIndexerBase : IndexerBase + { + public PublicBrazilianIndexerBase(IIndexerConfigurationService configService, WebClient wc, Logger l, + IProtectionService ps, ICacheService cs) : base( + configService: configService, client: wc, logger: l, p: ps, cacheService: cs, + configData: new ConfigurationData()) + { + webclient.requestDelay = .5; + } + + public override string Description => + $"{Name} is a Public Torrent Tracker for Movies and TV Shows dubbed in Brazilian Portuguese"; + + public override string Language => "pt-BR"; + public override string Type => "public"; + public override TorznabCapabilities TorznabCaps => SetCapabilities(); + + private TorznabCapabilities SetCapabilities() + { + var caps = new TorznabCapabilities + { + MovieSearchParams = new List { MovieSearchParam.Q }, + TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep } + }; + caps.Categories.AddCategoryMapping("filmes", TorznabCatType.Movies); + caps.Categories.AddCategoryMapping("series", TorznabCatType.TV); + return caps; + } + + public override IIndexerRequestGenerator GetRequestGenerator() => new SimpleRequestGenerator(SiteLink); + + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + await ConfigureIfOK(string.Empty, true, () => throw new Exception("Could not find releases from this URL")); + return IndexerConfigurationStatus.Completed; + } + public class FileInfo + { + public string[] Genres { get; set; } + public string[] Audio { get; set; } + public string Subtitle { get; set; } + public string Format { get; set; } + public string Quality { get; set; } + public string Size { get; set; } + public string ReleaseYear { get; set; } + public string Duration { get; set; } + public string AudioQuality { get; set; } + public string VideoQuality { get; set; } + public string TitleTranslated { get; set; } + public string TitleOriginal { get; set; } + public string IMDb { get; set; } + + public static FileInfo FromDictionary(Dictionary dict) + { + return new FileInfo + { + Genres = dict.TryGetValue("Gênero", out var genres) ? genres?.Split(',').Select(g => g.Trim()).ToArray() : null, + Audio = dict.TryGetValue("Áudio", out var audio) ? audio?.Split(',').Select(a => a.Trim()).ToArray() : ( + dict.TryGetValue("Idioma", out var lang) ? new[] { lang } : null), + Subtitle = dict.TryGetValue("Legenda", out var subtitle) ? subtitle : null, + Format = dict.TryGetValue("Formato", out var format) ? format : null, + Quality = dict.TryGetValue("Qualidade", out var quality) ? quality : null, + Size = dict.TryGetValue("Tamanho", out var size) ? size : null, + ReleaseYear = dict.TryGetValue("Ano de Lançamento", out var releaseYear) ? releaseYear : (dict.TryGetValue("Lançamento", out var year) ? year : null), + Duration = dict.TryGetValue("Duração", out var duration) ? duration : null, + AudioQuality = dict.TryGetValue("Qualidade de Áudio", out var audioQuality) ? audioQuality : null, + VideoQuality = dict.TryGetValue("Qualidade de Vídeo", out var videoQuality) ? videoQuality : null, + TitleTranslated = dict.TryGetValue("Título Traduzido", out var titleTr) ? titleTr : null, + TitleOriginal = dict.TryGetValue("Título Original", out var titleOr) ? titleOr : (dict.TryGetValue("Título", out var title) ? title : null), + IMDb = dict.TryGetValue("IMDb", out var imdb) ? imdb : null + }; + } + } + } + + public class SimpleRequestGenerator : IIndexerRequestGenerator + { + private readonly string _siteLink; + private string SearchQueryParamsKey { get; } + + public SimpleRequestGenerator(string siteLink, string searchQueryParamsKey = "?s=") + { + _siteLink = siteLink; + SearchQueryParamsKey = searchQueryParamsKey; + } + + public IndexerPageableRequestChain GetSearchRequests(TorznabQuery query) + { + var pageableRequests = new IndexerPageableRequestChain(); + var searchUrl = $"{_siteLink}{SearchQueryParamsKey}"; + if (!string.IsNullOrWhiteSpace(query.SearchTerm)) + { + searchUrl += WebUtility.UrlEncode(query.SearchTerm); + if (query.Season is { } value) + { + searchUrl += WebUtility.UrlEncode($" {value}"); + } + } + else + { + searchUrl = _siteLink; + } + + pageableRequests.Add(new[] { new IndexerRequest(searchUrl) }); + + return pageableRequests; + } + } + + public static class RowParsingExtensions + { + public static Uri ExtractMagnet(this IElement downloadButton) + { + var magnetLink = downloadButton.GetAttribute("href"); + var magnet = string.IsNullOrEmpty(magnetLink) ? null : new Uri(magnetLink); + return magnet; + } + + public static List ExtractGenres(this IElement row) + { + var genres = new List(); + row.ExtractFromRow( + "span:contains(\"Gênero:\")", genreText => + { + ExtractPattern( + genreText, @"Gênero:\s*(.+)", genre => ExtractMultiValuesFromField(values: out genres, field: genre)); + }); + return genres; + } + + public static List ExtractCategory(this IElement row, string title = null) + { + var releaseCategory = new List(); + var category = TorznabCatType.Movies; + row.ExtractFromRow( + "div.title > a", categoryText => + { + category = ExtractCategory(categoryText); + }); + if (!category.Equals(TorznabCatType.TV) && !string.IsNullOrWhiteSpace(title)) + { + category = ExtractCategory(title); + } + releaseCategory.Add(category.ID); + return releaseCategory; + } + + private static TorznabCategory ExtractCategory(string text) + { + var hasSeasonInfo = text.IndexOf("temporada", StringComparison.OrdinalIgnoreCase) >= 0 || + text.IndexOf("season", StringComparison.OrdinalIgnoreCase) >= 0 || + Regex.IsMatch(text, @"\bS\d{1,2}(?:E\d{1,2})?\b", RegexOptions.IgnoreCase); + var category = hasSeasonInfo ? TorznabCatType.TV : TorznabCatType.Movies; + return category; + } + + public static DateTime ExtractReleaseDate(this IElement row) + { + var result = DateTime.Today; + row.ExtractFromRow( + "span:contains(\"Lançamento:\")", releaseDateText => + { + ExtractPattern( + releaseDateText, @"Lançamento:\s*(.+)", releaseDate => + { + DateTime.TryParseExact( + releaseDate, "yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out result); + }); + }); + return result; + } + + public static List ExtractSubtitles(this IElement row) + { + var subtitles = new List(); + row.ExtractFromRow( + "span:contains(\"Legenda:\")", subtitleText => + { + ExtractPattern( + subtitleText, @"Legenda:\s*(.+)", subtitle => ExtractMultiValuesFromField(values: out subtitles, field: subtitle)); + }); + return subtitles; + } + + public static long ExtractSize(this IElement row) + { + long result = 0; + row.ExtractFromRow( + "span:contains(\"Tamanho:\")", sizeText => + { + ExtractPattern( + sizeText, @"Tamanho:\s*(.+)", size => + { + result = GetBytes(size); + }); + }); + return result; + } + + public static long GetBytes(string text) + { + if (Regex.Matches(text, @"\b[GTKP]?B\b", RegexOptions.IgnoreCase).Count > 1) + { + var match = Regex.Match(text, @"[GTKP]?B([.,| \d]+[GTKP]?B)", RegexOptions.RightToLeft); + if (match.Success) + { + text = match.Groups[1].Value; + } + } + + return ParseUtil.GetBytes(text); + } + + public static List ExtractLanguages(this IElement row) + { + var languages = new List(); + row.ExtractFromRow( + "span:contains(\"Áudio:\")", audioText => + { + ExtractPattern( + audioText, @"Áudio:\s*(.+)", language => ExtractMultiValuesFromField(values: out languages, field: language)); + }); + if (languages.Count == 0) + { + row.ExtractFromRow( + "span:contains(\"Idioma:\")", languageText => + { + ExtractPattern( + languageText, @"Idioma:\s*(.+)", language => ExtractMultiValuesFromField(values: out languages, field: language)); + }); + } + return languages; + } + private static void ExtractMultiValuesFromField(out List values, in string field) + { + if (field.Contains("|")) + { + values = field.Split('|').Select(token => token.Trim()).ToList(); + } + else if (field.Contains(",")) + { + values = field.Split(',').Select(token => token.Trim()).ToList(); + } + else + { + values = new List { field }; + } + } + + public static void ExtractFromRow(this IElement row, string selector, Action extraction) + { + var element = row.QuerySelector(selector); + if (element != null) + { + extraction(element.TextContent); + } + } + + public static void ExtractPattern(string text, string pattern, Action extraction) + { + var match = Regex.Match(text, pattern); + if (match.Success) + { + extraction(match.Groups[1].Value.Trim()); + } + } + } + public abstract class PublicBrazilianParser : IParseIndexerResponse + { + public abstract IList ParseResponse(IndexerResponse indexerResponse); + + + + public string ExtractTitleOrDefault(IElement downloadButton, string defaultTitle) + { + var magnetTitle = ""; + RowParsingExtensions.ExtractPattern(downloadButton?.GetAttribute("href"), + @"&dn=(.+?)&|&dn=(.+?)$", + mt => magnetTitle = HttpUtility.UrlDecode(mt)); + if (!string.IsNullOrWhiteSpace(magnetTitle)) + return FormatTitle(CleanTitle(magnetTitle), ExtractResolution(magnetTitle)); + var description = GetTitleElementOrNull(downloadButton); + var resolution = description?.TextContent switch + { + string text when !string.IsNullOrWhiteSpace(text) => ExtractResolution(text), + _ => ExtractResolution(defaultTitle) + }; + var title = (defaultTitle, description?.TextContent) switch + { + (string defTitle, _) when !string.IsNullOrWhiteSpace(defTitle) => CleanTitle(defTitle), + (_, string text) when !string.IsNullOrWhiteSpace(text) => CleanTitle(text), + _ => defaultTitle + }; + return FormatTitle(title, resolution); + } + + private string ExtractResolution(string text) + { + var resolution = ""; + RowParsingExtensions.ExtractPattern(text, @"\b(\d{3,4}p)\b", res => resolution = res); + return resolution; + } + + private string FormatTitle(string title, string resolution = null) + { + return string.IsNullOrWhiteSpace(resolution) + ? $"{title}" + : $"{title} {resolution}"; + } + + public long ExtractSizeByResolution(string title) + { + var resolution = "Other"; + RowParsingExtensions.ExtractPattern( + title, @"\b(\d{3,4}p)\b", res => + { + resolution = res; + }); + + var size = resolution switch + { + "720p" => "1GB", + "1080p" => "2.5GB", + "2160p" => "5GB", + _ => "512MB" + }; + + return RowParsingExtensions.GetBytes(size); + } + + protected static string CleanTitle(string title) + { + if (string.IsNullOrWhiteSpace(title)) + return null; + + // Remove size info in parentheses + title = Regex.Replace(title, @"\(\d+(?:\.\d+)?\s*(?:GB|MB)\)", "", RegexOptions.IgnoreCase); + + // Remove quality info + title = Regex.Replace(title, @"\b(?:720p|1080p|2160p|4K)\b", "", RegexOptions.IgnoreCase); + + // Remove source info + title = Regex.Replace(title, @"\b(?:WEB-DL|BRRip|HDRip|WEBRip|BluRay|Torrent|Download)\b", "", RegexOptions.IgnoreCase); + + // Remove language info + title = Regex.Replace(title, @"\b(?:Legendado|Leg|Dublado|Dub|[AÁ]udio)\b", "", RegexOptions.IgnoreCase); + + // Clean up torrent group names + title = Regex.Replace(title, @"HIDRATORRENTS\.ORG|\[?Erai-raws\]?|\[?Anime Time\]?|COMANDO4K\.COM|COMANDO\.TO|VEMTORRENT\.COM|VACATORRENT\.COM", "", RegexOptions.IgnoreCase); + + // Remove brackets/parentheses content + title = Regex.Replace(title, @"\[(?:.*?)\]|\((?:.*?)\)", "", RegexOptions.IgnoreCase); + + // Remove dangling punctuation and separators + title = Regex.Replace(title, @"[\\/,|~_-]+\s*|\s*[\\/,|~_-]+", " ", RegexOptions.IgnoreCase); + + // Clean up multiple spaces + title = Regex.Replace(title, @"\s+", " "); + + // Remove file extension from the beginning of title + title = Regex.Replace(title, @"MKV|MP4", "", RegexOptions.IgnoreCase); + + // Remove dots between words but keep dots in version numbers + title = Regex.Replace(title, @"(? + (description.NodeType != NodeType.Element || ((Element)description).TagName != "SPAN"); + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/ApacheTorrent.cs b/src/Jackett.Common/Indexers/Definitions/ApacheTorrent.cs new file mode 100644 index 000000000..2293c8901 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/ApacheTorrent.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using NLog; + +namespace Jackett.Common.Indexers.Definitions +{ + public class ApacheTorrent : PublicBrazilianIndexerBase + { + public override string Id => "apachetorrent"; + + public override string Name => "ApacheTorrent"; + + public override string SiteLink { get; protected set; } = "https://apachetorrent.com/"; + + public ApacheTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) : base(configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => + new ApacheTorrentParser(webclient); + + public override IIndexerRequestGenerator GetRequestGenerator() => new SimpleRequestGenerator(SiteLink, searchQueryParamsKey: "index.php?s="); + } + + public class ApacheTorrentParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + public string Tracker { get; } + + public ApacheTorrentParser(WebClient webclient) + { + _webclient = webclient; + Tracker = "ApacheTorrent"; + } + + private Dictionary ExtractFileInfo(IDocument detailsDom) + { + var fileInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + var infoSection = detailsDom.QuerySelector("#informacoes p"); + if (infoSection == null) + return fileInfo; + + var lines = infoSection.InnerHtml.Split(new[] { "
" }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Contains("") && line.Contains(":")) + { + var parts = line.Split(new[] { ':' }, 2); + if (parts.Length == 2) + { + var key = parts[0].Replace("", "").Replace("", "").Trim(); + var value = parts[1] + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Trim(); + value = value switch + { + var v when v.Contains("Dual Áudio") => v.Replace("Dual Áudio", "Dual"), + var v when v.Contains("Dual Audio") => v.Replace("Dual Audio", "Dual"), + var v when v.Contains("Full HD") => v.Replace("Full HD", "1080p"), + var v when v.Contains("4K") => v.Replace("4K", "2160p"), + var v when v.Contains("SD") => v.Replace("SD", "480p"), + var v when v.Contains("WEB") => v.Replace("WEB", "WEB-DL"), + _ => value + }; + + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) + { + fileInfo[key] = value; + } + } + } + } + + return fileInfo; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.capaname"); + + foreach (var row in rows) + { + var detailAnchor = row.QuerySelector("a[href^=\"https://\"]"); + if (detailAnchor == null) + continue; + + var detailUrl = new Uri(detailAnchor.GetAttribute("href") ?? string.Empty); + var title = detailAnchor.GetAttribute("title")?.Trim() ?? string.Empty; + + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Details = detailUrl, + Guid = detailUrl, + Seeders = 1 + }; + + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = parser.ParseDocument(detailsPage.ContentString); + + var fileInfoDict = ExtractFileInfo(detailsDom); + var fileInfo = PublicBrazilianIndexerBase.FileInfo.FromDictionary(fileInfoDict); + releaseCommonInfo.PublishDate = fileInfo.ReleaseYear != null ? DateTime.ParseExact(fileInfo.ReleaseYear, "yyyy", null) : DateTime.Today; + + var magnetLinks = detailsDom.QuerySelectorAll("a.btn[href^=\"magnet:?xt\"]"); + foreach (var magnetLink in magnetLinks) + { + var magnet = magnetLink.GetAttribute("href"); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Link = release.MagnetUri = new Uri(magnet ?? ""); + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 1; + + // Extract resolution from file info + var resolution = fileInfo.Quality ?? fileInfo.VideoQuality ?? string.Empty; + + // Format the title + release.Title = $"{release.Title} {resolution}".Trim(); + release.Title = ExtractTitleOrDefault(magnetLink, release.Title); + release.Category = magnetLink.ExtractCategory(release.Title); + + // Additional metadata + release.Languages = fileInfo.Audio?.ToList() ?? release.Languages; + release.Genres = fileInfo.Genres?.ToList() ?? release.Genres; + release.Subs = string.IsNullOrEmpty(fileInfo.Subtitle) ? release.Subs : new[] { fileInfo.Subtitle }; + var size = RowParsingExtensions.GetBytes(fileInfo.Size ?? string.Empty); + release.Size = size > 0 ? size : ExtractSizeByResolution(release.Title); + + if (!string.IsNullOrWhiteSpace(release.Title)) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && description.NodeType != NodeType.Text) + { + description = description.PreviousSibling; + } + + return description; + } + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/BluDV.cs b/src/Jackett.Common/Indexers/Definitions/BluDV.cs new file mode 100644 index 000000000..1b1d11ae1 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/BluDV.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Common.Services.Interfaces; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; +using WebRequest = Jackett.Common.Utils.Clients.WebRequest; + +namespace Jackett.Common.Indexers.Definitions +{ + [ExcludeFromCodeCoverage] + public class BluDV : PublicBrazilianIndexerBase + { + public override string Id => "bludv"; + public override string Name => "BluDV"; + public override string SiteLink { get; protected set; } = "https://bludv.xyz/"; + + public BluDV(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) + : base(configService, wc, l, ps, cs) + { + configData.AddDynamic( + "flaresolverr", + new ConfigurationData.DisplayInfoConfigurationItem("FlareSolverr", + "This site may use Cloudflare DDoS Protection, therefore Jackett requires FlareSolverr to access it.")); + } + + public override IParseIndexerResponse GetParser() => new BluDVParser(webclient); + } + + public class BluDVParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + + public BluDVParser(WebClient webclient) + { + _webclient = webclient; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.post"); + + foreach (var row in rows) + { + // Get the details page to extract the magnet link + var detailsParser = new HtmlParser(); + var detailAnchor = row.QuerySelector("a.more-link"); + var detailUrl = new Uri(detailAnchor?.GetAttribute("href") ?? string.Empty); + var title = row.QuerySelector("div.title > a")?.TextContent.Trim(); + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Genres = row.ExtractGenres(), + Subs = row.ExtractSubtitles(), + Size = row.ExtractSize(), + Languages = row.ExtractLanguages(), + Details = detailUrl, + Guid = detailUrl, + PublishDate = row.ExtractReleaseDate(), + Seeders = 1 + }; + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = detailsParser.ParseDocument(detailsPage.ContentString); + foreach (var downloadButton in detailsDom.QuerySelectorAll("a.customButton[href^=\"magnet:\"]")) + { + var magnet = downloadButton.ExtractMagnet(); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Title = ExtractTitleOrDefault(downloadButton, release.Title); + release.Category = downloadButton.ExtractCategory(release.Title); + release.Size = release.Size > 0 ? release.Size : ExtractSizeByResolution(release.Title); + release.Languages = row.ExtractLanguages(); + release.Link = release.Guid = release.MagnetUri = magnet; + release.DownloadVolumeFactor = 0; // Free + release.UploadVolumeFactor = 1; + + if (release.Title.IsNotNullOrWhiteSpace()) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && NotSpanTag(description)) + { + description = description.PreviousSibling; + } + + return description; + } + } + + +} diff --git a/src/Jackett.Common/Indexers/Definitions/FilmesHdTorrent.cs b/src/Jackett.Common/Indexers/Definitions/FilmesHdTorrent.cs new file mode 100644 index 000000000..2e65366ab --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/FilmesHdTorrent.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers.Definitions +{ + public class FilmesHdTorrent : PublicBrazilianIndexerBase + { + public override string Id => "filmeshdtorrent"; + public override string Name => "Filmes HD Torrent"; + public override string SiteLink { get; protected set; } = "https://www.filmeshdtorrent.vip/"; + + public override string[] AlternativeSiteLinks { get; protected set; } = { + "https://www.filmeshdtorrent.vip/", + "https://torrentalerta.net/", + }; + + public override string[] LegacySiteLinks { get; protected set; } = { + "https://baixarfilmestorrents.net/", + "https://comandofilmes.life/" + }; + + public FilmesHdTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) : base(configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => new FilmesHdTorrentParser(webclient); + } + public class FilmesHdTorrentParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + + public FilmesHdTorrentParser(WebClient webclient) + { + _webclient = webclient; + } + + private Dictionary ExtractFileInfo(IDocument detailsDom) + { + var fileInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + var content = detailsDom.QuerySelector("div.content"); + if (content == null) + return fileInfo; + + var lines = content.InnerHtml.Split(new[] { "
" }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Contains("") && line.Contains("") && line.Contains(":")) + { + var cleanLine = Regex.Replace(line, @"<[^>]+>", ""); // Remove HTML tags + var parts = cleanLine.Split(new[] { ':' }, 2); + if (parts.Length == 2) + { + var key = parts[0].Trim(); + var value = parts[1].Trim(); + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) + { + fileInfo[key] = value; + } + } + } + } + + return fileInfo; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.item"); + + foreach (var row in rows) + { + var detailsParser = new HtmlParser(); + var detailAnchor = row.QuerySelector("a[title]"); + var detailUrl = new Uri(detailAnchor?.GetAttribute("href") ?? string.Empty); + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(row.QuerySelector("div.titulo span")?.TextContent.Trim() ?? detailAnchor?.GetAttribute("title")?.Trim() ?? string.Empty), + Details = detailUrl, + Guid = detailUrl, + PublishDate = row.ExtractReleaseDate(), + Seeders = 1 + }; + + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = detailsParser.ParseDocument(detailsPage.ContentString); + + var fileInfoDict = ExtractFileInfo(detailsDom); + var fileInfo = PublicBrazilianIndexerBase.FileInfo.FromDictionary(fileInfoDict); + var querySelectorAll = detailsDom.QuerySelectorAll("a[href^=\"magnet:?xt\"]"); + foreach (var downloadButton in querySelectorAll) + { + var magnet = downloadButton.ExtractMagnet(); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Title = ExtractTitleOrDefault(downloadButton, release.Title); + release.Category = downloadButton.ExtractCategory(release.Title); + release.Languages = fileInfo.Audio?.ToList() ?? release.Languages; + release.Genres = fileInfo.Genres?.ToList() ?? release.Genres; + release.Subs = string.IsNullOrEmpty(fileInfo.Subtitle) ? release.Subs : new[] { fileInfo.Subtitle }; + var size = RowParsingExtensions.GetBytes(fileInfo.Size ?? string.Empty); + release.Size = size > 0 ? size : ExtractSizeByResolution(release.Title); + release.Link = release.Guid = release.MagnetUri = magnet; + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 1; + + if (release.Title.IsNotNullOrWhiteSpace()) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && NotSpanTag(description)) + { + description = description.PreviousSibling; + } + + return description; + } + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/LAPUMiA.cs b/src/Jackett.Common/Indexers/Definitions/LAPUMiA.cs new file mode 100644 index 000000000..ca3b181fe --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/LAPUMiA.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using NLog; +using static System.Linq.Enumerable; +using WebClient = Jackett.Common.Utils.Clients.WebClient; +using WebRequest = Jackett.Common.Utils.Clients.WebRequest; + +namespace Jackett.Common.Indexers.Definitions +{ + [ExcludeFromCodeCoverage] + public class LAPUMiA : PublicBrazilianIndexerBase + { + public override string Id => "lapumia"; + public override string Name => "LAPUMiA"; + public override string SiteLink { get; protected set; } = "https://lapumia.net/"; + + public LAPUMiA(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) : base(configService: configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => new LAPUMiAParser(webclient); + } + + public class LAPUMiAParser : PublicBrazilianParser + { + private WebClient _webclient; + + public LAPUMiAParser(WebClient webclient) + { + _webclient = webclient; + } + + private Dictionary ExtractFileInfo(IDocument detailsDom) + { + var fileInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + var infoItems = detailsDom.QuerySelectorAll("div.info li"); + foreach (var item in infoItems) + { + var text = item.TextContent.Trim(); + var parts = text.Split( + new[] + { + ':' + }, 2); + if (parts.Length == 2) + { + var key = parts[0].Trim(); + var value = parts[1].Trim(); + fileInfo[key] = value; + } + } + + return fileInfo; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.item"); + foreach (var row in rows) + { + // Get the details page to extract the magnet link + var detailsParser = new HtmlParser(); + var detailAnchor = row.QuerySelector("a[title]"); + var detailUrl = new Uri(detailAnchor?.GetAttribute("href")); + var title = detailAnchor.GetAttribute("title"); + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Details = detailUrl, + Guid = detailUrl, + PublishDate = row.ExtractReleaseDate(), + Seeders = 1 + }; + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = detailsParser.ParseDocument(detailsPage.ContentString); + foreach (var downloadButton in detailsDom.QuerySelectorAll("ul.buttons a[href^=\"magnet:?xt\"]")) + { + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Title = ExtractTitleOrDefault(downloadButton, release.Title); + release.Category = downloadButton.ExtractCategory(release.Title); + var fileInfoDict = ExtractFileInfo(detailsDom); + var fileInfo = PublicBrazilianIndexerBase.FileInfo.FromDictionary(fileInfoDict); + release.Languages = fileInfo.Audio?.ToList() ?? release.Languages; + release.Genres = fileInfo.Genres?.ToList() ?? release.Genres; + release.Subs = string.IsNullOrEmpty(fileInfo.Subtitle) + ? release.Subs + : new[] + { + fileInfo.Subtitle + }; + var size = RowParsingExtensions.GetBytes(fileInfo.Size ?? string.Empty); + release.Size = size > 0 ? size : ExtractSizeByResolution(release.Title); + var magnet = downloadButton.ExtractMagnet(); + release.Link = release.Guid = release.MagnetUri = magnet; + release.DownloadVolumeFactor = 0; // Free + release.UploadVolumeFactor = 1; + if (release.Title.IsNotNullOrWhiteSpace()) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && description.NodeType != NodeType.Text) + { + description = description.PreviousSibling; + } + + return description; + } + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/RedeTorrent.cs b/src/Jackett.Common/Indexers/Definitions/RedeTorrent.cs new file mode 100644 index 000000000..2bd1d4c74 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/RedeTorrent.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers.Definitions +{ + public class RedeTorrent : PublicBrazilianIndexerBase + { + public override string Id => "redetorrent"; + + public override string Name => "RedeTorrent"; + + public override string SiteLink { get; protected set; } = "https://redetorrent.com/"; + + public RedeTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) + : base(configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => new RedeTorrentParser(webclient); + + public override IIndexerRequestGenerator GetRequestGenerator() => new SimpleRequestGenerator(SiteLink, searchQueryParamsKey: "index.php?s="); + } + + public class RedeTorrentParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + protected string Tracker; + + public RedeTorrentParser(WebClient webclient) + { + _webclient = webclient; + Tracker = "RedeTorrent"; + } + + private Dictionary ExtractFileInfo(IDocument detailsDom) + { + var fileInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + var infoSection = detailsDom.QuerySelector("#informacoes p"); + if (infoSection == null) + return fileInfo; + + var lines = infoSection.InnerHtml.Split(new[] { "
" }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Contains("") && line.Contains(":")) + { + var parts = line.Split(new[] { ':' }, 2); + if (parts.Length == 2) + { + var key = parts[0].Replace("", "").Replace("", "").Trim(); + var value = parts[1] + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Replace("", "") + .Trim(); + value = value switch + { + var v when v.Contains("Dual Áudio") => v.Replace("Dual Áudio", "Dual"), + var v when v.Contains("Dual Audio") => v.Replace("Dual Audio", "Dual"), + var v when v.Contains("Full HD") => v.Replace("Full HD", "1080p"), + var v when v.Contains("4K") => v.Replace("4K", "2160p"), + var v when v.Contains("SD") => v.Replace("SD", "480p"), + var v when v.Contains("WEB") => v.Replace("WEB", "WEB-DL"), + _ => value + }; + + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) + { + fileInfo[key] = value; + } + } + } + } + + return fileInfo; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.capa_lista"); + + foreach (var row in rows) + { + var detailAnchor = row.QuerySelector("a[href^=\"https://\"]"); + if (detailAnchor == null) + continue; + + var detailUrl = new Uri(detailAnchor.GetAttribute("href") ?? string.Empty); + var titleElement = row.QuerySelector("h2[itemprop='headline']"); + var title = titleElement?.TextContent.Trim() ?? detailAnchor.GetAttribute("title")?.Trim() ?? string.Empty; + + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Details = detailUrl, + Guid = detailUrl, + Seeders = 1 + }; + + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = parser.ParseDocument(detailsPage.ContentString); + + var fileInfoDict = ExtractFileInfo(detailsDom); + var fileInfo = PublicBrazilianIndexerBase.FileInfo.FromDictionary(fileInfoDict); + releaseCommonInfo.PublishDate = fileInfo.ReleaseYear != null ? DateTime.ParseExact(fileInfo.ReleaseYear, "yyyy", null) : DateTime.Today; + + var magnetLinks = detailsDom.QuerySelectorAll("a.btn[href^=\"magnet:?xt\"]"); + foreach (var magnetLink in magnetLinks) + { + var magnet = magnetLink.GetAttribute("href"); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Guid = release.Link = release.MagnetUri = new Uri(magnet ?? string.Empty); + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 1; + + // Extract resolution from file info + var resolution = fileInfo.Quality ?? fileInfo.VideoQuality ?? string.Empty; + + // Format the title + release.Title = $"{release.Title} {resolution}".Trim(); + release.Title = ExtractTitleOrDefault(magnetLink, release.Title); + release.Category = magnetLink.ExtractCategory(release.Title); + + // Additional metadata + release.Languages = fileInfo.Audio?.ToList() ?? release.Languages; + release.Genres = fileInfo.Genres?.ToList() ?? release.Genres; + release.Subs = string.IsNullOrEmpty(fileInfo.Subtitle) ? release.Subs : new[] { fileInfo.Subtitle }; + var size = RowParsingExtensions.GetBytes(fileInfo.Size ?? string.Empty); + release.Size = size > 0 ? size : ExtractSizeByResolution(release.Title); + + if (release.Title.IsNotNullOrWhiteSpace()) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && description.NodeType != NodeType.Text) + { + description = description.PreviousSibling; + } + + return description; + } + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/TorrentDosFilmes.cs b/src/Jackett.Common/Indexers/Definitions/TorrentDosFilmes.cs new file mode 100644 index 000000000..dbcefc597 --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/TorrentDosFilmes.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; +using WebRequest = Jackett.Common.Utils.Clients.WebRequest; + +namespace Jackett.Common.Indexers.Definitions +{ + [ExcludeFromCodeCoverage] + public class TorrentDosFilmes : PublicBrazilianIndexerBase + { + public override string Id => "torrentdosfilmes"; + public override string Name => "TorrentDosFilmes"; + public override string SiteLink { get; protected set; } = "https://torrentsdosfilmes.to/"; + + public override string[] AlternativeSiteLinks { get; protected set; } = { + "https://torrentsdosfilmes.to/", + "https://ComandoFilmes.xyz/" + }; + + public override string[] LegacySiteLinks { get; protected set; } = { + "https://torrentdosfilmes.site/" + }; + + public TorrentDosFilmes(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) : base(configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => new TorrentDosFilmesParser(webclient); + } + + public class TorrentDosFilmesParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + + public TorrentDosFilmesParser(WebClient webclient) + { + _webclient = webclient; + } + + private Dictionary ExtractFileInfo(IDocument detailsDom) + { + var fileInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + var infoSpans = detailsDom.QuerySelectorAll("span[style*='color: black']"); + + foreach (var span in infoSpans) + { + var text = span.TextContent.Trim(); + var parts = text.Split(new[] { ':' }, 2); + if (parts.Length == 2) + { + var key = parts[0].Replace("", "").Replace("", "").Replace("", "").Replace("", "").Trim(); + var value = parts[1].Trim(); + fileInfo[key] = value; + } + } + + return fileInfo; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var rows = dom.QuerySelectorAll("div.post"); + + foreach (var row in rows) + { + var detailsParser = new HtmlParser(); + var detailAnchor = row.QuerySelector("div.title a[title]"); + var detailUrl = new Uri(detailAnchor?.GetAttribute("href") ?? string.Empty); + var title = detailAnchor?.TextContent.Trim() ?? string.Empty; + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Details = detailUrl, + Guid = detailUrl, + PublishDate = row.ExtractReleaseDate(), + Seeders = 1 + }; + + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = detailsParser.ParseDocument(detailsPage.ContentString); + + var fileInfoDict = ExtractFileInfo(detailsDom); + var fileInfo = PublicBrazilianIndexerBase.FileInfo.FromDictionary(fileInfoDict); + + foreach (var downloadButton in detailsDom.QuerySelectorAll("a.customButton[href^=\"magnet:\"]")) + { + var magnet = downloadButton.ExtractMagnet(); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Title = ExtractTitleOrDefault(downloadButton, release.Title); + release.Category = downloadButton.ExtractCategory(release.Title); + release.Languages = fileInfo.Audio?.ToList() ?? release.Languages; + release.Genres = fileInfo.Genres?.ToList() ?? release.Genres; + release.Subs = string.IsNullOrEmpty(fileInfo.Subtitle) ? release.Subs : new[] { fileInfo.Subtitle }; + var size = RowParsingExtensions.GetBytes(fileInfo.Size ?? string.Empty); + release.Size = size > 0 ? size : ExtractSizeByResolution(release.Title); + release.Link = release.Guid = release.MagnetUri = magnet; + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 1; + + if (release.Title.IsNotNullOrWhiteSpace()) + releases.Add(release); + } + } + + return releases; + } + + protected override INode GetTitleElementOrNull(IElement downloadButton) + { + var description = downloadButton.PreviousSibling; + while (description != null && NotSpanTag(description)) + { + description = description.PreviousSibling; + } + + return description; + } + } +} diff --git a/src/Jackett.Common/Indexers/Definitions/TorrentsMegaFilmes.cs b/src/Jackett.Common/Indexers/Definitions/TorrentsMegaFilmes.cs new file mode 100644 index 000000000..c48e6668c --- /dev/null +++ b/src/Jackett.Common/Indexers/Definitions/TorrentsMegaFilmes.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Extensions; +using Jackett.Common.Indexers.Definitions.Abstract; +using Jackett.Common.Models; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils.Clients; +using NLog; + +namespace Jackett.Common.Indexers.Definitions +{ + [ExcludeFromCodeCoverage] + public class TorrentsMegaFilmes : PublicBrazilianIndexerBase + { + public override string Id => "torrentsmegafilmes"; + public override string Name => "Torrents Mega Filmes"; + public override string SiteLink { get; protected set; } = "https://torrentsmegafilmes.top/"; + + public TorrentsMegaFilmes(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) : base(configService, wc, l, ps, cs) + { + } + + public override IParseIndexerResponse GetParser() => new TorrentsMegaFilmesParser(webclient); + } + + public class TorrentsMegaFilmesParser : PublicBrazilianParser + { + private readonly WebClient _webclient; + public TorrentsMegaFilmesParser(WebClient webclient) + { + _webclient = webclient; + } + + public override IList ParseResponse(IndexerResponse indexerResponse) + { + var releases = new List(); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(indexerResponse.Content); + var detailAnchors = dom.QuerySelectorAll("div.title > a"); + foreach (var detailAnchor in detailAnchors) + { + var detailUrl = new Uri(detailAnchor?.GetAttribute("href") ?? string.Empty); + var title = detailAnchor?.TextContent.Trim(); + var detailsPage = _webclient.GetResultAsync(new WebRequest(detailUrl.ToString())).Result; + var detailsDom = parser.ParseDocument(detailsPage.ContentString); + var detailsInfo = detailsDom.QuerySelector("div.info"); + var releaseCommonInfo = new ReleaseInfo + { + Title = CleanTitle(title), + Genres = detailsInfo.ExtractGenres(), + Subs = detailsInfo.ExtractSubtitles(), + Size = detailsInfo.ExtractSize(), + Languages = detailsInfo.ExtractLanguages(), + Details = detailUrl, + Guid = detailUrl, + PublishDate = detailsInfo.ExtractReleaseDate(), + Seeders = 1 + }; + foreach (var downloadButton in detailsDom.QuerySelectorAll("ul.buttons a[href]")) + { + var magnet = downloadButton.ExtractMagnet(); + var release = releaseCommonInfo.Clone() as ReleaseInfo; + release.Link = release.Guid = release.MagnetUri = magnet; + release.Title = ExtractTitleOrDefault(downloadButton, release.Title + " " + downloadButton.TextContent); + release.Category = downloadButton.ExtractCategory(release.Title); + release.DownloadVolumeFactor = 0; // Free + release.UploadVolumeFactor = 1; + + if (release.Title.IsNotNullOrWhiteSpace()) + { + releases.Add(release); + } + } + } + return releases; + } + + /** + * Return null to concatenate titles rather than ranking, i.e., button only contains resolution, season, and episode. + */ + protected override INode GetTitleElementOrNull(IElement downloadButton) => null; + } +}