1
0
Fork 0
mirror of https://github.com/Jackett/Jackett synced 2024-12-30 11:47:28 +00:00

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.
This commit is contained in:
vgveloso 2024-12-17 21:27:05 -05:00 committed by GitHub
parent 8779d57169
commit 14860ff396
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1331 additions and 0 deletions

View file

@ -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

View file

@ -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> { MovieSearchParam.Q },
TvSearchParams = new List<TvSearchParam> { 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<IndexerConfigurationStatus> 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<string, string> 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<string> ExtractGenres(this IElement row)
{
var genres = new List<string>();
row.ExtractFromRow(
"span:contains(\"Gênero:\")", genreText =>
{
ExtractPattern(
genreText, @"Gênero:\s*(.+)", genre => ExtractMultiValuesFromField(values: out genres, field: genre));
});
return genres;
}
public static List<int> ExtractCategory(this IElement row, string title = null)
{
var releaseCategory = new List<int>();
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<string> ExtractSubtitles(this IElement row)
{
var subtitles = new List<string>();
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<string> ExtractLanguages(this IElement row)
{
var languages = new List<string>();
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<string> 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<string> { field };
}
}
public static void ExtractFromRow(this IElement row, string selector, Action<string> extraction)
{
var element = row.QuerySelector(selector);
if (element != null)
{
extraction(element.TextContent);
}
}
public static void ExtractPattern(string text, string pattern, Action<string> extraction)
{
var match = Regex.Match(text, pattern);
if (match.Success)
{
extraction(match.Groups[1].Value.Trim());
}
}
}
public abstract class PublicBrazilianParser : IParseIndexerResponse
{
public abstract IList<ReleaseInfo> 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, @"(?<!\d)\.(?!\d)", " ", RegexOptions.IgnoreCase);
// Remove any remaining punctuation at start/end
title = title.Trim(' ', '.', ',', '-', '_', '~', '/', '\\', '|');
return title;
}
protected abstract INode GetTitleElementOrNull(IElement downloadButton);
protected static bool NotSpanTag(INode description) =>
(description.NodeType != NodeType.Element || ((Element)description).TagName != "SPAN");
}
}

View file

@ -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<string, string> ExtractFileInfo(IDocument detailsDom)
{
var fileInfo = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var infoSection = detailsDom.QuerySelector("#informacoes p");
if (infoSection == null)
return fileInfo;
var lines = infoSection.InnerHtml.Split(new[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("<strong>") && line.Contains(":"))
{
var parts = line.Split(new[] { ':' }, 2);
if (parts.Length == 2)
{
var key = parts[0].Replace("<strong>", "").Replace("</strong>", "").Trim();
var value = parts[1]
.Replace("<strong>", "")
.Replace("</strong>", "")
.Replace("<span style=\"12px arial,verdana,tahoma;\">", "")
.Replace("</span>", "")
.Replace("<span class=\"entry-date\">", "")
.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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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 <a href=\"https://github.com/Jackett/Jackett#configuring-flaresolverr\" target=\"_blank\">FlareSolverr</a> 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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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<string, string> ExtractFileInfo(IDocument detailsDom)
{
var fileInfo = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var content = detailsDom.QuerySelector("div.content");
if (content == null)
return fileInfo;
var lines = content.InnerHtml.Split(new[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("<strong>") && line.Contains("</strong>") && 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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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<string, string> ExtractFileInfo(IDocument detailsDom)
{
var fileInfo = new Dictionary<string, string>(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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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<string, string> ExtractFileInfo(IDocument detailsDom)
{
var fileInfo = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var infoSection = detailsDom.QuerySelector("#informacoes p");
if (infoSection == null)
return fileInfo;
var lines = infoSection.InnerHtml.Split(new[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("<strong>") && line.Contains(":"))
{
var parts = line.Split(new[] { ':' }, 2);
if (parts.Length == 2)
{
var key = parts[0].Replace("<strong>", "").Replace("</strong>", "").Trim();
var value = parts[1]
.Replace("<strong>", "")
.Replace("</strong>", "")
.Replace("<span style=\"12px arial,verdana,tahoma;\">", "")
.Replace("</span>", "")
.Replace("<span class=\"entry-date\">", "")
.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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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<string, string> ExtractFileInfo(IDocument detailsDom)
{
var fileInfo = new Dictionary<string, string>(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("<em>", "").Replace("</em>", "").Replace("<strong>", "").Replace("</strong>", "").Trim();
var value = parts[1].Trim();
fileInfo[key] = value;
}
}
return fileInfo;
}
public override IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}
}

View file

@ -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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releases = new List<ReleaseInfo>();
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;
}
}