hdtorrents: c# -> yaml resolves #16002

This commit is contained in:
Garfield69
2025-05-22 07:46:28 +12:00
parent 2b41fa7e87
commit cf4134d3af
2 changed files with 190 additions and 287 deletions

View File

@@ -0,0 +1,190 @@
---
id: hdtorrents
name: HD-Torrents
description: "HD-Torrents (HDT) is a Private Torrent Tracker for HD MOVIES / TV / MUSIC / 3X"
language: en-US
type: private
encoding: UTF-8
links:
- https://hdts.ru/
- https://hd-torrents.org/
- https://hd-torrents.net/
- https://hd-torrents.me/
caps:
categorymappings:
- {id: 70, cat: Movies/BluRay, desc: "Movie/UHD/Blu-Ray"}
- {id: 1, cat: Movies/BluRay, desc: "Movie/Blu-Ray"}
- {id: 71, cat: Movies/UHD, desc: "Movie/UHD/Remux"}
- {id: 2, cat: Movies/HD, desc: "Movie/Remux"}
- {id: 5, cat: Movies/HD, desc: "Movie/1080p/i"}
- {id: 3, cat: Movies/HD, desc: "Movie/720p"}
- {id: 64, cat: Movies/UHD, desc: "Movie/2160p"}
- {id: 63, cat: Audio, desc: "Movie/Audio Track"}
- {id: 72, cat: TV/UHD, desc: "TV Show/UHD/Blu-ray"}
- {id: 59, cat: TV/HD, desc: "TV Show/Blu-ray"}
- {id: 73, cat: TV/UHD, desc: "TV Show/UHD/Remux"}
- {id: 60, cat: TV/HD, desc: "TV Show/Remux"}
- {id: 30, cat: TV/HD, desc: "TV Show/1080p/i"}
- {id: 38, cat: TV/HD, desc: "TV Show/720p"}
- {id: 65, cat: TV/UHD, desc: "TV Show/2160p"}
- {id: 44, cat: Audio, desc: "Music/Album"}
- {id: 61, cat: Audio/Video, desc: "Music/Blu-Ray"}
- {id: 62, cat: Audio/Video, desc: "Music/Remux"}
- {id: 57, cat: Audio/Video, desc: "Music/1080p/i"}
- {id: 45, cat: Audio/Video, desc: "Music/720p"}
- {id: 66, cat: Audio/Video, desc: "Music/2160p"}
- {id: 58, cat: XXX, desc: "XXX/Blu-ray"}
- {id: 78, cat: XXX, desc: "XXX/Remux"}
- {id: 74, cat: XXX, desc: "XXX/UHD/Blu-ray"}
- {id: 48, cat: XXX, desc: "XXX/1080p/i"}
- {id: 47, cat: XXX, desc: "XXX/720p"}
- {id: 67, cat: XXX, desc: "XXX/2160p"}
modes:
search: [q]
tv-search: [q, season, ep, imdbid]
movie-search: [q, imdbid]
music-search: [q]
settings:
- name: username
type: text
label: Username
- name: password
type: password
label: Password
- name: freeleech
type: checkbox
label: Search freeleech only
default: false
- name: sort
type: select
label: Sort requested from site
default: data
options:
data: created
size: size
seeds: seeders
filename: title
- name: type
type: select
label: Order requested from site
default: desc
options:
desc: desc
asc: asc
- name: info
type: info
label: Results Per Page
default: For best results, change the <b>Torrents per page:</b> setting to <b>100</b> on your account profile.
- name: info_activity
type: info
label: Account Inactivity
default: "If you do not log in for 50 days, your account will be disabled for inactivity. If you are VIP you won't be disabled until the VIP period is over."
- name: info_flaresolverr
type: info_flaresolverr
login:
path: login.php
method: form
form: form
inputs:
uid: "{{ .Config.username }}"
pwd: "{{ .Config.password }}"
error:
- selector: div > font[color="#FF0000"]
test:
path: /
selector: a[href^="logout.php?check_hash="]
search:
paths:
- path: torrents.php
inputs:
$raw: "{{ range .Categories }}category[]={{.}}&{{end}}"
search: "{{ if .Query.IMDBID }}{{ .Query.IMDBID }}{{ else }}{{ .Keywords }}{{ end }}"
# 0 All, 1 ActiveOnly, 2 DeadOnly, 5 Free, 6 50, 7 25, 8 75
active: "{{ if .Config.freeleech }}5{{ else }}0{{ end }}"
# 0 title, 3 title and descr, 1 genre, 2 imdb
options: 0
order: "{{ .Config.sort }}"
by: "{{ .Config.type }}"
keywordsfilters:
# manually url encode parenthesis to prevent "hacking" detection, remove . as not used in titles
- name: re_replace
args: ["\\.", " "]
- name: re_replace
args: ["\\(", "%28"]
- name: re_replace
args: ["\\)", "%29"]
rows:
selector: "table.mainblockcontenttt > tbody > tr:has(td.mainblockcontent):not(:first-of-type){{ if .Config.freeleech }}:has(img[src=\"images/sign_free.png\"]){{ else }}{{ end }}"
fields:
_has_freeleech:
case:
":root table.navus tr td:nth-child(2):contains(\" VIP\")": yes
":root table.navus tr td:nth-child(2):contains(\" Uploader\")": yes
":root table.navus tr td:nth-child(2):contains(\" HD Internal\")": yes
":root table.navus tr td:nth-child(2):contains(\" Moderator\")": yes
":root table.navus tr td:nth-child(2):contains(\" Administrator\")": yes
":root table.navus tr td:nth-child(2):contains(\" Owner\")": yes
"*": no
category:
selector: a[href^="torrents.php?category="]
attribute: href
filters:
- name: querystring
args: category
title:
selector: a[href^="details.php?id="]
details:
selector: a[href^="details.php?id="]
attribute: href
download:
selector: a[href^="download.php?id="]
attribute: href
imdbid:
selector: a[href*="imdb.com/title/tt"]
attribute: href
genre:
selector: td:nth-child(3) span
description:
text: "{{ .Result.genre }}"
date:
# auto adjusted by site account profile
selector: td:nth-child(7)
filters:
- name: dateparse
args: "HH:mm:ss dd/MM/yyyy"
size:
selector: td:nth-child(8)
seeders:
selector: td:nth-last-child(3)
leechers:
selector: td:nth-last-child(2)
grabs:
selector: td:nth-last-child(1)
downloadvolumefactor_freeleech:
case:
img[src$="no_ratio.png"]: 0
img[src$="free.png"]: 0
img[src$="50.png"]: 0.5
img[src$="25.png"]: 0.75
img[src$="75.png"]: 0.25
"*": 1
downloadvolumefactor:
text: "{{ if eq .Result._has_freeleech \"yes\" }}0{{ else }}{{ .Result.downloadvolumefactor_freeleech }}{{ end }}"
uploadvolumefactor:
case:
img[src$="no_ratio.png"]: 0
"*": 1
minimumratio:
text: 1.0
minimumseedtime:
# 2 days (as seconds = 2 x 24 x 60 x 60)
text: 172800
# engine n/a

View File

@@ -1,287 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Jackett.Common.Utils.Clients;
using Newtonsoft.Json.Linq;
using NLog;
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
namespace Jackett.Common.Indexers.Definitions
{
[ExcludeFromCodeCoverage]
public class HDTorrents : IndexerBase
{
public override string Id => "hdtorrents";
public override string Name => "HD-Torrents";
public override string Description => "HD-Torrents (HDT) is a Private Torrent Tracker for HD MOVIES / TV / MUSIC / 3X";
public override string SiteLink { get; protected set; } = "https://hdts.ru/"; // Domain https://hdts.ru/ seems more reliable
public override string[] AlternativeSiteLinks => new[]
{
"https://hdts.ru/",
"https://hd-torrents.org/",
"https://hd-torrents.net/",
"https://hd-torrents.me/"
};
public override string Language => "en-US";
public override string Type => "private";
public override TorznabCapabilities TorznabCaps => SetCapabilities();
private string SearchUrl => SiteLink + "torrents.php?";
private string LoginUrl => SiteLink + "login.php";
private readonly Regex _posterRegex = new Regex(@"src=\\'./([^']+)\\'", RegexOptions.IgnoreCase);
private readonly HashSet<string> _freeleechRanks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"VIP",
"Uploader",
"HD Internal",
"Moderator",
"Administrator",
"Owner"
};
private new ConfigurationDataBasicLogin configData => (ConfigurationDataBasicLogin)base.configData;
public HDTorrents(IIndexerConfigurationService configService, WebClient w, Logger l, IProtectionService ps,
ICacheService cs)
: base(configService: configService,
client: w,
logger: l,
p: ps,
cacheService: cs,
configData: new ConfigurationDataBasicLogin("For best results, change the <b>Torrents per page:</b> setting to <b>100</b> on your account profile."))
{
configData.AddDynamic("freeleech", new BoolConfigurationItem("Search freeleech only") { Value = false });
configData.AddDynamic("flaresolverr", new 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."));
configData.AddDynamic("accountinactivity", new DisplayInfoConfigurationItem("Account Inactivity", "If you do not log in for 50 days, your account will be disabled for inactivity. If you are VIP you won't be disabled until the VIP period is over."));
}
private TorznabCapabilities SetCapabilities()
{
var caps = new TorznabCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
}
};
// Movie
caps.Categories.AddCategoryMapping("70", TorznabCatType.MoviesBluRay, "Movie/UHD/Blu-Ray");
caps.Categories.AddCategoryMapping("1", TorznabCatType.MoviesBluRay, "Movie/Blu-Ray");
caps.Categories.AddCategoryMapping("71", TorznabCatType.MoviesUHD, "Movie/UHD/Remux");
caps.Categories.AddCategoryMapping("2", TorznabCatType.MoviesHD, "Movie/Remux");
caps.Categories.AddCategoryMapping("5", TorznabCatType.MoviesHD, "Movie/1080p/i");
caps.Categories.AddCategoryMapping("3", TorznabCatType.MoviesHD, "Movie/720p");
caps.Categories.AddCategoryMapping("64", TorznabCatType.MoviesUHD, "Movie/2160p");
caps.Categories.AddCategoryMapping("63", TorznabCatType.Audio, "Movie/Audio Track");
// TV Show
caps.Categories.AddCategoryMapping("72", TorznabCatType.TVUHD, "TV Show/UHD/Blu-ray");
caps.Categories.AddCategoryMapping("59", TorznabCatType.TVHD, "TV Show/Blu-ray");
caps.Categories.AddCategoryMapping("73", TorznabCatType.TVUHD, "TV Show/UHD/Remux");
caps.Categories.AddCategoryMapping("60", TorznabCatType.TVHD, "TV Show/Remux");
caps.Categories.AddCategoryMapping("30", TorznabCatType.TVHD, "TV Show/1080p/i");
caps.Categories.AddCategoryMapping("38", TorznabCatType.TVHD, "TV Show/720p");
caps.Categories.AddCategoryMapping("65", TorznabCatType.TVUHD, "TV Show/2160p");
// Music
caps.Categories.AddCategoryMapping("44", TorznabCatType.Audio, "Music/Album");
caps.Categories.AddCategoryMapping("61", TorznabCatType.AudioVideo, "Music/Blu-Ray");
caps.Categories.AddCategoryMapping("62", TorznabCatType.AudioVideo, "Music/Remux");
caps.Categories.AddCategoryMapping("57", TorznabCatType.AudioVideo, "Music/1080p/i");
caps.Categories.AddCategoryMapping("45", TorznabCatType.AudioVideo, "Music/720p");
caps.Categories.AddCategoryMapping("66", TorznabCatType.AudioVideo, "Music/2160p");
// XXX
caps.Categories.AddCategoryMapping("58", TorznabCatType.XXX, "XXX/Blu-ray");
caps.Categories.AddCategoryMapping("78", TorznabCatType.XXX, "XXX/Remux");
caps.Categories.AddCategoryMapping("74", TorznabCatType.XXX, "XXX/UHD/Blu-ray");
caps.Categories.AddCategoryMapping("48", TorznabCatType.XXX, "XXX/1080p/i");
caps.Categories.AddCategoryMapping("47", TorznabCatType.XXX, "XXX/720p");
caps.Categories.AddCategoryMapping("67", TorznabCatType.XXX, "XXX/2160p");
return caps;
}
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
{
LoadValuesFromJson(configJson);
var loginPage = await RequestWithCookiesAsync(LoginUrl, string.Empty);
var pairs = new Dictionary<string, string> {
{ "uid", configData.Username.Value },
{ "pwd", configData.Password.Value }
};
var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true, null, LoginUrl);
await ConfigureIfOK(
result.Cookies, result.ContentString?.Contains("If your browser doesn't have javascript enabled") == true, () =>
{
var parser = new HtmlParser();
using var dom = parser.ParseDocument(result.ContentString);
var errorMessage = dom.QuerySelector("div > font[color=\"#FF0000\"]")?.TextContent.Trim();
throw new ExceptionWithConfigData(errorMessage ?? "Couldn't login", configData);
});
return IndexerConfigurationStatus.RequiresTesting;
}
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var releases = new List<ReleaseInfo>();
var searchUrl = SearchUrl + string.Join(string.Empty, MapTorznabCapsToTrackers(query).Select(cat => $"category[]={cat}&"));
var queryCollection = new NameValueCollection
{
{ "search", query.ImdbID ?? query.GetQueryString() },
{ "active", ((BoolConfigurationItem)configData.GetDynamic("freeleech")).Value ? "5" : "0" },
{ "options", "0" }
};
// manually url encode parenthesis to prevent "hacking" detection, remove . as not used in titles
searchUrl += queryCollection.GetQueryString().Replace("(", "%28").Replace(")", "%29").Replace(".", " ");
var results = await RequestWithCookiesAndRetryAsync(searchUrl);
// Occasionally the cookies become invalid, login again if that happens
if (results.ContentString.Contains("Error:You're not authorized"))
{
await ApplyConfiguration(null);
results = await RequestWithCookiesAndRetryAsync(searchUrl);
}
try
{
var parser = new HtmlParser();
using var dom = parser.ParseDocument(results.ContentString);
var userInfo = dom.QuerySelector("table.navus tr");
var userRank = userInfo.Children[1].TextContent.Replace("Rank:", string.Empty).Trim();
var hasFreeleech = _freeleechRanks.Contains(userRank);
var rows = dom.QuerySelectorAll("table.mainblockcontenttt tr:has(td.mainblockcontent)");
foreach (var row in rows.Skip(1))
{
if (row.Children.Length == 2)
{
// fix bug with search: cohen
continue;
}
var mainLink = row.Children[2].QuerySelector("a");
var title = mainLink.TextContent;
var details = new Uri(SiteLink + mainLink.GetAttribute("href"));
// posters, when fetched on the dashboard search results page, generate cloudflare challenges
// var posterMatch = _posterRegex.Match(mainLink.GetAttribute("onmouseover"));
// var poster = posterMatch.Success ? new Uri(SiteLink + posterMatch.Groups[1].Value.Replace("\\", "/")) : null;
var link = new Uri(SiteLink + row.Children[4].FirstElementChild.GetAttribute("href"));
var description = row.Children[2].QuerySelector("span")?.TextContent.Trim();
var size = ParseUtil.GetBytes(row.Children[7].TextContent);
var dateAdded = string.Join(" ", row.Children[6].FirstElementChild.Attributes.Select(a => a.Name).Take(4));
var publishDate = DateTime.ParseExact(dateAdded, "dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
var categoryLink = row.FirstElementChild.FirstElementChild.GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
// Sometimes the uploader column is missing, so seeders, leechers, and grabs may be at a different index.
// There's room for improvement, but this works for now.
var endIndex = row.Children.Length;
//Maybe use row.Children.Index(Node) after searching for an element instead?
if (row.Children[endIndex - 1].TextContent == "Edit")
endIndex -= 1;
// moderators get additional delete, recommend and like links
else if (row.Children[endIndex - 4].TextContent == "Edit")
endIndex -= 4;
int? seeders = null;
int? peers = null;
if (ParseUtil.TryCoerceInt(row.Children[endIndex - 3].TextContent, out var rSeeders))
{
seeders = rSeeders;
if (ParseUtil.TryCoerceInt(row.Children[endIndex - 2].TextContent, out var rLeechers))
peers = rLeechers + rSeeders;
}
var grabs = ParseUtil.TryCoerceLong(row.Children[endIndex - 1].TextContent, out var rGrabs)
? (long?)rGrabs
: null;
var dlVolumeFactor = 1.0;
var upVolumeFactor = 1.0;
if (row.QuerySelector("img[src$=\"no_ratio.png\"]") != null)
{
dlVolumeFactor = 0;
upVolumeFactor = 0;
}
else if (hasFreeleech || row.QuerySelector("img[src$=\"free.png\"]") != null)
dlVolumeFactor = 0;
else if (row.QuerySelector("img[src$=\"50.png\"]") != null)
dlVolumeFactor = 0.5;
else if (row.QuerySelector("img[src$=\"25.png\"]") != null)
dlVolumeFactor = 0.75;
else if (row.QuerySelector("img[src$=\"75.png\"]") != null)
dlVolumeFactor = 0.25;
var imdb = ParseUtil.GetImdbId(row.QuerySelector("a[href*=\"www.imdb.com/title/\"]")?.GetAttribute("href")?.TrimEnd('/')?.Split('/')?.LastOrDefault());
var release = new ReleaseInfo
{
Title = title,
Details = details,
Guid = details,
Link = link,
PublishDate = publishDate,
Category = MapTrackerCatToNewznab(cat),
Description = description,
// Poster = poster,
Imdb = imdb,
Size = size,
Grabs = grabs,
Seeders = seeders,
Peers = peers,
DownloadVolumeFactor = dlVolumeFactor,
UploadVolumeFactor = upVolumeFactor,
MinimumRatio = 1,
MinimumSeedTime = 172800 // 48 hours
};
releases.Add(release);
}
}
catch (Exception ex)
{
OnParseError(results.ContentString, ex);
}
return releases;
}
}
}