diff --git a/src/Jackett.Common/Definitions/hdspace.yml b/src/Jackett.Common/Definitions/hdspace.yml
new file mode 100644
index 000000000..d87e9f68d
--- /dev/null
+++ b/src/Jackett.Common/Definitions/hdspace.yml
@@ -0,0 +1,180 @@
+---
+id: hdspace
+name: HD-Space
+description: "HD-Space is a Private Torrent Tracker for MOVIES / TV / GENERAL"
+language: en-US
+type: private
+encoding: UTF-8
+links:
+ - https://hd-space.org/
+
+caps:
+ categorymappings:
+ - {id: 15, cat: Movies/BluRay, desc: "Movie / Blu-ray"}
+ - {id: 40, cat: Movies/HD, desc: "Movie / Remux"}
+ - {id: 18, cat: Movies/HD, desc: "Movie / 720p"}
+ - {id: 19, cat: Movies/HD, desc: "Movie / 1080p"}
+ - {id: 46, cat: Movies/UHD, desc: "Movie / 2160p"}
+ - {id: 21, cat: TV/HD, desc: "TV Show / 720p HDTV"}
+ - {id: 22, cat: TV/HD, desc: "TV Show / 1080p HDTV"}
+ - {id: 45, cat: TV/UHD, desc: "TV Show / 2160p HDTV"}
+ - {id: 24, cat: TV/Documentary, desc: "Documentary / 720p"}
+ - {id: 25, cat: TV/Documentary, desc: "Documentary / 1080p"}
+ - {id: 47, cat: TV/Documentary, desc: "Documentary / 2160p"}
+ - {id: 27, cat: TV/Anime, desc: "Animation / 720p"}
+ - {id: 28, cat: TV/Anime, desc: "Animation / 1080p"}
+ - {id: 48, cat: TV/Anime, desc: "Animation / 2160p"}
+ - {id: 30, cat: Audio/Lossless, desc: "Music / HQ Audio"}
+ - {id: 31, cat: Audio/Video, desc: "Music / Videos"}
+ - {id: 33, cat: XXX, desc: "XXX / 720p"}
+ - {id: 34, cat: XXX, desc: "XXX / 1080p"}
+ - {id: 49, cat: XXX, desc: "XXX / 2160p"}
+ - {id: 36, cat: Movies/Other, desc: "Trailers"}
+ - {id: 37, cat: PC, desc: "Software"}
+ - {id: 38, cat: Other, desc: "Others"}
+ - {id: 41, cat: Movies/UHD, desc: "Movie / 4K UHD"}
+
+ 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: Filter freeleech only
+ default: false
+ - name: sort
+ type: select
+ label: Sort requested from site
+ default: 3
+ options:
+ 3: created
+ 5: seeders
+ 4: size
+ 2: title
+ - name: type
+ type: select
+ label: Order requested from site
+ default: 2
+ options:
+ 2: desc
+ 1: asc
+ - name: flaresolverr
+ type: info
+ label: FlareSolverr
+ default: This site may use Cloudflare DDoS Protection, therefore Jackett requires FlareSolverr to access it.
+
+login:
+ path: index.php?page=login
+ method: post
+ inputs:
+ uid: "{{ .Config.username }}"
+ pwd: "{{ .Config.password }}"
+ logout: ""
+ error:
+ - selector: tr td span[style="color:#FF0000;"]
+ test:
+ path: index.php
+ selector: a[href="logout.php"]
+
+search:
+ paths:
+ # https://hd-space.org/index.php?page=torrents&search=&active=0&options=0&category=15;18;19
+ - path: index.php
+ inputs:
+ page: torrents
+ search: "{{ if .Query.IMDBID }}{{ .Query.IMDBIDShort }}{{ else }}{{ .Keywords }}{{ end }}"
+ category: "{{ if .Categories }}{{ range .Categories }}{{.}};{{end}}{{ else }}0{{ end }}"
+ # 0 default, 1 genre, 2 imdb, 3 uploader
+ options: "{{ if .Query.IMDBID }}2{{ else }}0{{ end }}"
+ # 0 all, 1 activeonly, 2 deadonly
+ active: 0
+ order: "{{ .Config.sort }}"
+ by: "{{ .Config.type }}"
+
+ rows:
+ selector: "table.lista[width=\"100%\"] > tbody > style ~ tr{{ if .Config.freeleech }}:has(img[src=\"gold/gold.png\"]){{ else }}{{ end }}, table.lista[width=\"100%\"] > tbody > style ~ tr{{ if .Config.freeleech }}:has(img[src=\"images/sf.png\"]){{ else }}{{ end }}"
+
+ fields:
+ category:
+ selector: td a[href^="index.php?page=torrents&category="]
+ attribute: href
+ filters:
+ - name: querystring
+ args: category
+ title:
+ selector: td a[href^="index.php?page=torrent-details"]
+ details:
+ selector: td a[href^="index.php?page=torrent-details"]
+ attribute: href
+ download:
+ selector: a[href^="download.php?id="]
+ attribute: href
+ poster:
+ selector: td a[href^="index.php?page=torrent-details"]
+ attribute: onmouseover
+ filters:
+ - name: regexp
+ args: src=\./(.+?)\s
+ imdbid:
+ selector: td a[href^="index.php?page=torrent-details"]
+ attribute: onmouseover
+ filters:
+ - name: regexp
+ args: /(\d{8}).jpg
+ date_day:
+ # Today at 09:17:08
+ # Yesterday at 17:11:03
+ selector: td:nth-child(5):contains("day")
+ # auto adjusted by site account profile
+ optional: true
+ filters:
+ - name: re_replace
+ args: ["[ ]at|[//\xa0\\s,]+", " "]
+ date_year:
+ # January 30, 2024, 20:23:21
+ selector: td:nth-child(5):not(:contains("day"))
+ # auto adjusted by site account profile
+ optional: true
+ filters:
+ - name: re_replace
+ args: ["[//\xa0\\s,]+", " "]
+ - name: dateparse
+ args: "MMMM dd yyyy HH:mm:ss"
+ date:
+ text: "{{ if or .Result.date_day .Result.date_year }}{{ or .Result.date_day .Result.date_year }}{{ else }}now{{ end }}"
+ size:
+ selector: td:nth-child(6)
+ seeders:
+ selector: td:nth-child(8)
+ leechers:
+ selector: td:nth-child(9)
+ grabs:
+ selector: td:nth-child(10)
+ genre:
+ selector: td:nth-child(2)
+ remove: a
+ description:
+ text: "{{ .Result.genre }}"
+ downloadvolumefactor:
+ case:
+ img[src="images/sf.png"]: 0 # side freeleech
+ img[src="gold/gold.png"]: 0
+ img[src="gold/silver.png"]: 0.5
+ "*": 1
+ uploadvolumefactor:
+ text: 1
+ minimumratio:
+ text: 1.0
+ minimumseedtime:
+ # 1 day (as seconds = 1 x 24 x 60 x 60)
+ text: 86400
+# xbtit
diff --git a/src/Jackett.Common/Indexers/HDSpace.cs b/src/Jackett.Common/Indexers/HDSpace.cs
deleted file mode 100644
index 93526905c..000000000
--- a/src/Jackett.Common/Indexers/HDSpace.cs
+++ /dev/null
@@ -1,231 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-using AngleSharp.Html.Parser;
-using Jackett.Common.Extensions;
-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 Jackett.Common.Models.IndexerConfig.ConfigurationData;
-using WebClient = Jackett.Common.Utils.Clients.WebClient;
-
-namespace Jackett.Common.Indexers
-{
- [ExcludeFromCodeCoverage]
- public class HDSpace : IndexerBase
- {
- public override string Id => "hdspace";
- public override string Name => "HD-Space";
- public override string Description => "Sharing The Universe";
- public override string SiteLink { get; protected set; } = "https://hd-space.org/";
- public override string Language => "en-US";
- public override string Type => "private";
-
- public override TorznabCapabilities TorznabCaps => SetCapabilities();
-
- private string LoginUrl => SiteLink + "index.php?page=login";
- private string SearchUrl => SiteLink + "index.php?page=torrents";
-
- private new ConfigurationDataBasicLogin configData => (ConfigurationDataBasicLogin)base.configData;
-
- public HDSpace(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps,
- ICacheService cs)
- : base(configService: configService,
- client: wc,
- logger: l,
- p: ps,
- cacheService: cs,
- configData: new ConfigurationDataBasicLogin())
- {
- configData.AddDynamic("freeleech", new BoolConfigurationItem("Filter freeleech only") { Value = false });
- configData.AddDynamic("flaresolverr", new DisplayInfoConfigurationItem("FlareSolverr", "This site may use Cloudflare DDoS Protection, therefore Jackett requires FlareSolverr to access it."));
- }
-
- private TorznabCapabilities SetCapabilities()
- {
- var caps = new TorznabCapabilities
- {
- TvSearchParams = new List
- {
- TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
- },
- MovieSearchParams = new List
- {
- MovieSearchParam.Q, MovieSearchParam.ImdbId
- },
- MusicSearchParams = new List
- {
- MusicSearchParam.Q
- }
- };
-
- caps.Categories.AddCategoryMapping(15, TorznabCatType.MoviesBluRay, "Movie / Blu-ray");
- caps.Categories.AddCategoryMapping(40, TorznabCatType.MoviesHD, "Movie / Remux");
- caps.Categories.AddCategoryMapping(18, TorznabCatType.MoviesHD, "Movie / 720p");
- caps.Categories.AddCategoryMapping(19, TorznabCatType.MoviesHD, "Movie / 1080p");
- caps.Categories.AddCategoryMapping(46, TorznabCatType.MoviesUHD, "Movie / 2160p");
- caps.Categories.AddCategoryMapping(21, TorznabCatType.TVHD, "TV Show / 720p HDTV");
- caps.Categories.AddCategoryMapping(22, TorznabCatType.TVHD, "TV Show / 1080p HDTV");
- caps.Categories.AddCategoryMapping(45, TorznabCatType.TVUHD, "TV Show / 2160p HDTV");
- caps.Categories.AddCategoryMapping(24, TorznabCatType.TVDocumentary, "Documentary / 720p");
- caps.Categories.AddCategoryMapping(25, TorznabCatType.TVDocumentary, "Documentary / 1080p");
- caps.Categories.AddCategoryMapping(47, TorznabCatType.TVDocumentary, "Documentary / 2160p");
- caps.Categories.AddCategoryMapping(27, TorznabCatType.TVAnime, "Animation / 720p");
- caps.Categories.AddCategoryMapping(28, TorznabCatType.TVAnime, "Animation / 1080p");
- caps.Categories.AddCategoryMapping(48, TorznabCatType.TVAnime, "Animation / 2160p");
- caps.Categories.AddCategoryMapping(30, TorznabCatType.AudioLossless, "Music / HQ Audio");
- caps.Categories.AddCategoryMapping(31, TorznabCatType.AudioVideo, "Music / Videos");
- caps.Categories.AddCategoryMapping(33, TorznabCatType.XXX, "XXX / 720p");
- caps.Categories.AddCategoryMapping(34, TorznabCatType.XXX, "XXX / 1080p");
- caps.Categories.AddCategoryMapping(49, TorznabCatType.XXX, "XXX / 2160p");
- caps.Categories.AddCategoryMapping(36, TorznabCatType.MoviesOther, "Trailers");
- caps.Categories.AddCategoryMapping(37, TorznabCatType.PC, "Software");
- caps.Categories.AddCategoryMapping(38, TorznabCatType.Other, "Others");
- caps.Categories.AddCategoryMapping(41, TorznabCatType.MoviesUHD, "Movie / 4K UHD");
-
- return caps;
- }
-
- public override async Task ApplyConfiguration(JToken configJson)
- {
- LoadValuesFromJson(configJson);
-
- var loginPage = await RequestWithCookiesAsync(LoginUrl, string.Empty);
-
- var pairs = new Dictionary
- {
- { "uid", configData.Username.Value },
- { "pwd", configData.Password.Value }
- };
-
- // Send Post
- var response = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true, referer: LoginUrl);
-
- await ConfigureIfOK(response.Cookies, response.ContentString?.Contains("logout.php") == true || response.ContentString?.Contains("Rank: Parked") == true, () =>
- {
- var parser = new HtmlParser();
- using var dom = parser.ParseDocument(response.ContentString);
- var errorMessages = dom
- .QuerySelectorAll("table.lista td.lista span[style*=\"#FF0000\"], table.lista td.header:contains(\"login attempts\")")
- .Select(r => r.TextContent.Trim())
- .Where(m => m.IsNotNullOrWhiteSpace())
- .ToArray();
-
- throw new ExceptionWithConfigData(errorMessages.Any() ? errorMessages.Join(" ") : "Unknown error message, please report.", configData);
- });
-
- return IndexerConfigurationStatus.RequiresTesting;
- }
-
- protected override async Task> PerformQuery(TorznabQuery query)
- {
- var releases = new List();
- var queryCollection = new NameValueCollection
- {
- {"active", "0"},
- {"category", string.Join(";", MapTorznabCapsToTrackers(query))}
- };
-
- if (query.IsImdbQuery)
- {
- queryCollection.Set("options", "2");
- queryCollection.Set("search", query.ImdbIDShort);
- }
- else
- {
- queryCollection.Set("options", "0");
- queryCollection.Set("search", query.GetQueryString().Replace(".", " "));
- }
-
- var response = await RequestWithCookiesAndRetryAsync($"{SearchUrl}&{queryCollection.GetQueryString()}");
-
- try
- {
- var resultParser = new HtmlParser();
- using var searchResultDocument = resultParser.ParseDocument(response.ContentString);
- var rows = searchResultDocument.QuerySelectorAll("table.lista > tbody > tr");
-
- foreach (var row in rows)
- {
- // this tracker has horrible markup, find the result rows by looking for the style tag before each one
- var prev = row.PreviousElementSibling;
- if (prev == null || !string.Equals(prev.NodeName, "style", StringComparison.OrdinalIgnoreCase))
- continue;
-
- var release = new ReleaseInfo
- {
- MinimumRatio = 1,
- MinimumSeedTime = 86400 // 24 hours
- };
-
- if (row.QuerySelector("img[title=\"FreeLeech\"]") != null)
- release.DownloadVolumeFactor = 0;
- else if (row.QuerySelector("img[src=\"images/sf.png\"]") != null) // side freeleech
- release.DownloadVolumeFactor = 0;
- else if (row.QuerySelector("img[title=\"Half FreeLeech\"]") != null)
- release.DownloadVolumeFactor = 0.5;
- else
- release.DownloadVolumeFactor = 1;
- if (((BoolConfigurationItem)configData.GetDynamic("freeleech")).Value &&
- release.DownloadVolumeFactor != 0)
- continue;
- release.UploadVolumeFactor = 1;
-
- var qLink = row.Children[1].FirstElementChild;
- release.Title = qLink.TextContent.Trim();
- release.Details = new Uri(SiteLink + qLink.GetAttribute("href"));
- release.Guid = release.Details;
- release.Link = new Uri(SiteLink + row.Children[3].FirstElementChild.GetAttribute("href"));
-
- var torrentTitle = ParseUtil.GetArgumentFromQueryString(release.Link.ToString(), "f")?.Replace(".torrent", "").Trim();
- if (!string.IsNullOrWhiteSpace(torrentTitle))
- release.Title = WebUtility.HtmlDecode(torrentTitle);
-
- var qGenres = row.QuerySelector("span[style=\"color: #000000 \"]");
- var description = "";
- if (qGenres != null)
- description = qGenres.TextContent.Split('\xA0').Last().Replace(" ", "");
-
- var imdbLink = row.Children[1].QuerySelector("a[href*=imdb]");
- if (imdbLink != null)
- release.Imdb = ParseUtil.GetImdbId(imdbLink.GetAttribute("href").Split('/').Last());
-
- var dateStr = row.Children[4].TextContent.Trim();
- //"July 11, 2015, 13:34:09", "Today|Yesterday at 20:04:23"
- release.PublishDate = DateTimeUtil.FromUnknown(dateStr);
- var sizeStr = row.Children[5].TextContent;
- release.Size = ParseUtil.GetBytes(sizeStr);
- release.Seeders = ParseUtil.CoerceInt(row.Children[7].TextContent);
- release.Peers = ParseUtil.CoerceInt(row.Children[8].TextContent) + release.Seeders;
- var grabs = row.QuerySelector("td:nth-child(10)").TextContent;
- grabs = grabs.Replace("---", "0");
- release.Grabs = ParseUtil.CoerceInt(grabs);
-
- var categoryLink = row.QuerySelector("a[href^=\"index.php?page=torrents&category=\"]").GetAttribute("href");
- var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
- release.Category = MapTrackerCatToNewznab(cat);
-
- release.Description = description;
- if (release.Genres == null)
- release.Genres = new List();
- release.Genres = release.Genres.Union(description.Split(',')).ToList();
-
- releases.Add(release);
- }
- }
- catch (Exception ex)
- {
- OnParseError(response.ContentString, ex);
- }
-
- return releases;
- }
- }
-}