diff --git a/src/Jackett.Common/Definitions/awesomehd.yml b/src/Jackett.Common/Definitions/awesomehd.yml
deleted file mode 100644
index f615f1008..000000000
--- a/src/Jackett.Common/Definitions/awesomehd.yml
+++ /dev/null
@@ -1,101 +0,0 @@
----
- site: awesomehd
- name: Awesome-HD
- description: "An HD tracker"
- language: en-us
- type: private
- encoding: UTF-8
- links:
- - https://awesome-hd.me/
-
- caps:
- categorymappings:
- - {id: 1, cat: Movies/HD, desc: "Movies"}
- - {id: 2, cat: TV/HD, desc: "TV-Shows"}
-
- modes:
- search: [q]
- tv-search: [q, season, ep]
- movie-search: [q]
-
- settings:
- - name: username
- type: text
- label: Username
- - name: password
- type: password
- label: Password
- - name: sort
- type: select
- label: Sort requested from site
- default: "time"
- options:
- "time": "created"
- "seeders": "seeders"
- "size": "size"
- - name: type
- type: select
- label: Order requested from site
- default: "desc"
- options:
- "desc": "desc"
- "asc": "asc"
- - name: info_login
- type: info
- label: "Password Changes"
- default: "This site forces you to change your Password every 90 days.
If you get a Login Failed, got redirected error, then access the site with your browser and check if you need to change your password. Logout after saving, and update this config to login."
-
- login:
- path: login.php
- method: form
- form: form#loginform
- inputs:
- username: "{{ .Config.username }}"
- password: "{{ .Config.password }}"
- keeplogged: 1
- error:
- - selector: form#loginform .warning
- test:
- path: torrents.php
-
- search:
- paths:
- - path: torrents.php
- inputs:
- $raw: "{{ range .Categories }}filter_cat[{{.}}]=1&{{end}}"
- searchstr: "{{ .Keywords }}"
- page: torrents
- order_by: "{{ .Config.sort }}"
- order_way: "{{ .Config.type }}"
-
- rows:
- selector: table#torrent_table > tbody > tr.group, tr.torrent, tr.group_torrent:not(.edition_info)
-
- fields:
- download:
- selector: a[href^="torrents.php?action=download&id="]
- attribute: href
- optional: true
- details:
- selector: a[href^="torrents.php?id="]
- attribute: href
- title:
- selector: td:nth-child(3) > a
- category:
- selector: td:nth-child(2)
- date:
- selector: td:nth-last-child(5)
- size:
- selector: td:nth-last-child(4)
- grabs:
- selector: td:nth-last-child(3)
- seeders:
- selector: td:nth-last-child(2)
- leechers:
- selector: td:nth-last-child(1)
- downloadvolumefactor:
- case:
- "*": 1
- uploadvolumefactor:
- case:
- "*": 1
diff --git a/src/Jackett.Common/Indexers/AwesomeHD.cs b/src/Jackett.Common/Indexers/AwesomeHD.cs
new file mode 100644
index 000000000..d25457353
--- /dev/null
+++ b/src/Jackett.Common/Indexers/AwesomeHD.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Jackett.Common.Models;
+using Jackett.Common.Models.IndexerConfig;
+using Jackett.Common.Services.Interfaces;
+using Jackett.Common.Utils;
+using Newtonsoft.Json.Linq;
+using NLog;
+
+namespace Jackett.Common.Indexers
+{
+ public class AwesomeHD : BaseWebIndexer
+ {
+ private string SearchUrl => SiteLink + "searchapi.php";
+ private string TorrentUrl => SiteLink + "torrents.php";
+ private new ConfigurationDataPasskey configData => (ConfigurationDataPasskey)base.configData;
+
+ public AwesomeHD(IIndexerConfigurationService configService, Utils.Clients.WebClient c, Logger l, IProtectionService ps)
+ : base("Awesome-HD",
+ description: "An HD tracker",
+ link: "https://awesome-hd.me/",
+ caps: new TorznabCapabilities(),
+ configService: configService,
+ client: c,
+ logger: l,
+ p: ps,
+ configData: new ConfigurationDataPasskey("Note: You can find the Passkey in your profile, next to Personal information."))
+ {
+ Encoding = Encoding.UTF8;
+ Language = "en-us";
+ Type = "private";
+
+ TorznabCaps.SupportsImdbMovieSearch = true;
+ TorznabCaps.SupportsImdbTVSearch = true;
+
+ AddCategoryMapping(1, TorznabCatType.MoviesHD);
+ AddCategoryMapping(2, TorznabCatType.TVHD);
+ }
+
+ public override async Task ApplyConfiguration(JToken configJson)
+ {
+ LoadValuesFromJson(configJson);
+
+ if (configData.Passkey.Value.Length != 32)
+ throw new Exception("Invalid Passkey configured. Expected length: 32");
+
+ var releases = await PerformQuery(new TorznabQuery());
+ await ConfigureIfOK(string.Empty, releases.Any(),
+ () => throw new Exception("Could not find release from this URL."));
+
+ return IndexerConfigurationStatus.Completed;
+ }
+
+ protected override async Task> PerformQuery(TorznabQuery query)
+ {
+ var releases = new List();
+ var passkey = configData.Passkey.Value;
+
+ var qc = new NameValueCollection
+ {
+ {"passkey", passkey}
+ };
+
+ if (!string.IsNullOrWhiteSpace(query.ImdbID))
+ {
+ qc.Add("action", "imdbsearch");
+ qc.Add("imdb", query.ImdbID);
+ }
+ else if (!string.IsNullOrWhiteSpace(query.GetQueryString()))
+ {
+ qc.Add("action", "titlesearch");
+ qc.Add("title", query.SearchTerm); // not use query.GetQueryString(), see the season code below
+ }
+ else
+ {
+ qc.Add("action", "latestmovies");
+ // the endpoint 'latestmovies' only returns movies, this hack overwrites categories to get movies even if
+ // you are searching for tv series. this allows to configure the tracker in Sonarr
+ query.Categories = new int[] {};
+ }
+
+ var searchUrl = SearchUrl + "?" + qc.GetQueryString();
+ var results = await RequestStringWithCookies(searchUrl);
+ if (string.IsNullOrWhiteSpace(results.Content))
+ throw new Exception("Empty response. Please, check the Passkey.");
+
+ try
+ {
+ var doc = XDocument.Parse(results.Content);
+
+ var errorMsg = doc.Descendants("error").FirstOrDefault()?.Value;
+ if (errorMsg?.Contains("No Results") == true)
+ return releases; // no results
+ if (errorMsg != null)
+ throw new Exception(errorMsg);
+
+ var authkey = doc.Descendants("authkey").First().Value;
+ var torrents = doc.Descendants("torrent");
+ foreach (var torrent in torrents)
+ {
+ var torrentName = torrent.FirstValue("name").Trim();
+
+ // the field is always Movie, so we have to guess if it's a tv series
+ var isSerie = torrentName.Contains(": Season ");
+ if (isSerie)
+ torrentName = torrentName.Replace(": Season ", " S");
+
+ // if the category is not in the search categories, skip
+ var cat = new List {isSerie ? TorznabCatType.TVHD.ID : TorznabCatType.MoviesHD.ID};
+ if (query.Categories.Any() && !query.Categories.Intersect(cat).Any())
+ continue;
+
+ // if it's a tv series season search, skip movies and other seasons
+ if (query.Season > 0 && (!isSerie || !torrentName.EndsWith($" S{query.Season:D2}")))
+ continue;
+
+ var title = new StringBuilder(torrentName);
+ if (!isSerie && torrent.Element("year") != null) // only for movies
+ title.Append($" {torrent.FirstValue("year")}");
+ if (torrent.Element("internal")?.Value == "1")
+ title.Append(" iNTERNAL");
+ if (torrent.Element("resolution") != null)
+ title.Append($" {torrent.FirstValue("resolution")}");
+ if (torrent.Element("media") != null)
+ title.Append($" {torrent.FirstValue("media")}");
+ if (torrent.Element("encoding") != null)
+ title.Append($" {torrent.FirstValue("encoding")}");
+ if (torrent.Element("audioformat") != null)
+ title.Append($" {torrent.FirstValue("audioformat")}");
+ if (torrent.Element("releasegroup") != null)
+ title.Append($"-{torrent.FirstValue("releasegroup")}");
+
+ var torrentId = torrent.FirstValue("id");
+ var groupId = torrent.FirstValue("groupid");
+ var comments = new Uri($"{TorrentUrl}?id={groupId}&torrentid={torrentId}");
+ var link = new Uri($"{TorrentUrl}?action=download&id={torrentId}&authkey={authkey}&torrent_pass={passkey}");
+
+ var publishDate = DateTime.Parse(torrent.FirstValue("time"));
+ var size = long.Parse(torrent.FirstValue("size"));
+ var grabs = int.Parse(torrent.FirstValue("snatched"));
+ var seeders = int.Parse(torrent.FirstValue("seeders"));
+ var peers = seeders + int.Parse(torrent.FirstValue("leechers"));
+ var freeleech = double.Parse(torrent.FirstValue("freeleech"));
+
+ Uri banner = null;
+ // small cover only for movies
+ if (!isSerie && !string.IsNullOrWhiteSpace(torrent.Element("smallcover")?.Value))
+ banner = new Uri(torrent.FirstValue("smallcover"));
+ else if (!string.IsNullOrWhiteSpace(torrent.Element("cover")?.Value))
+ banner = new Uri(torrent.FirstValue("cover"));
+
+ var description = torrent.Element("encodestatus") != null ?
+ $"Encode status: {torrent.FirstValue("encodestatus")}" : null;
+
+ var imdb = ParseUtil.GetImdbID(torrent.Element("imdb")?.Value);
+
+ var release = new ReleaseInfo
+ {
+ Title = title.ToString(),
+ Comments = comments,
+ Link = link,
+ Guid = link,
+ PublishDate = publishDate,
+ Category = cat,
+ BannerUrl = banner,
+ Description = description,
+ Imdb = imdb,
+ Size = size,
+ Grabs = grabs,
+ Seeders = seeders,
+ Peers = peers,
+ MinimumRatio = 1,
+ MinimumSeedTime = 259200, // 72 hours
+ DownloadVolumeFactor = freeleech,
+ UploadVolumeFactor = 1
+ };
+
+ releases.Add(release);
+ }
+ }
+ catch (Exception ex)
+ {
+ OnParseError(results.Content, ex);
+ }
+
+ return releases;
+ }
+ }
+}
diff --git a/src/Jackett.Updater/Program.cs b/src/Jackett.Updater/Program.cs
index f0aa491a6..f305ef8a9 100644
--- a/src/Jackett.Updater/Program.cs
+++ b/src/Jackett.Updater/Program.cs
@@ -282,6 +282,7 @@ namespace Jackett.Updater
"Definitions/archetorrent.yml",
"Definitions/asiandvdclub.yml",
"Definitions/avg.yml",
+ "Definitions/awesomehd.yml", // migrated to C#
"Definitions/b2s-share.yml",
"Definitions/bithq.yml",
"Definitions/bitme.yml",