diff --git a/src/Jackett.Common/Indexers/SpeedCDcookie.cs b/src/Jackett.Common/Indexers/SpeedCDcookie.cs
new file mode 100644
index 000000000..a60513597
--- /dev/null
+++ b/src/Jackett.Common/Indexers/SpeedCDcookie.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Collections.Generic;
+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.Helpers;
+using Jackett.Common.Models;
+using Jackett.Common.Models.IndexerConfig;
+using Jackett.Common.Models.IndexerConfig.Bespoke;
+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
+{
+ [ExcludeFromCodeCoverage]
+ public class SpeedCDcookie : IndexerBase
+ {
+ public override string Id => "speedcdcookie";
+ public override string Name => "Speed.cd (cookie)";
+ public override string Description => "Your home now!";
+ public override string SiteLink { get; protected set; } = "https://speed.cd/";
+ public override string[] AlternativeSiteLinks => new[]
+ {
+ "https://speed.cd/",
+ "https://speed.click/",
+ "https://speeders.me/"
+ };
+ public override string Language => "en-US";
+ public override string Type => "private";
+
+ public override bool SupportsPagination => true;
+
+ public override TorznabCapabilities TorznabCaps => SetCapabilities();
+
+ private string LoginUrl1 => SiteLink + "login";
+ private string LoginUrl2 => SiteLink + "login/API";
+ private string SearchUrl => SiteLink + "browse/";
+ private new ConfigurationDataCookie configData => (ConfigurationDataCookie)base.configData;
+
+
+ public SpeedCDcookie(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps,
+ ICacheService cs)
+ : base(configService: configService,
+ client: wc,
+ logger: l,
+ p: ps,
+ cacheService: cs,
+ configData: new ConfigurationDataCookie(
+ @"Speed.Cd have increased their security. If you are having problems please check the security tab in your
+ Speed.Cd profile. Eg. Geo Locking, your seedbox may be in a different country to the one where you login via your
+ web browser.
For best results, change the 'Torrents per page' setting to 100 in
'Profile Settings > Torrents'.
+
This site may use Cloudflare DDoS Protection, therefore Jackett requires FlareSolverr to access it."))
+ {
+ configData.AddDynamic("Freeleech", new BoolConfigurationItem("Search freeleech only") { Value = false });
+ configData.AddDynamic("ExcludeArchives", new BoolConfigurationItem("Exclude torrents with RAR files") { Value = false });
+ configData.AddDynamic("AccountActivity", new DisplayInfoConfigurationItem("Account Inactivity", "Accounts not being used for 3 months will be removed to make room for active members."));
+ }
+
+ 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
+ },
+ BookSearchParams = new List
+ {
+ BookSearchParam.Q
+ }
+ };
+
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesOther, "Movies/XviD");
+ caps.Categories.AddCategoryMapping(42, TorznabCatType.Movies, "Movies/Packs");
+ caps.Categories.AddCategoryMapping(32, TorznabCatType.Movies, "Movies/Kids");
+ caps.Categories.AddCategoryMapping(43, TorznabCatType.MoviesHD, "Movies/HD");
+ caps.Categories.AddCategoryMapping(47, TorznabCatType.Movies, "Movies/DiVERSiTY");
+ caps.Categories.AddCategoryMapping(28, TorznabCatType.MoviesBluRay, "Movies/B-Ray");
+ caps.Categories.AddCategoryMapping(48, TorznabCatType.Movies3D, "Movies/3D");
+ caps.Categories.AddCategoryMapping(40, TorznabCatType.MoviesDVD, "Movies/DVD-R");
+ caps.Categories.AddCategoryMapping(56, TorznabCatType.Movies, "Movies/Anime");
+ caps.Categories.AddCategoryMapping(50, TorznabCatType.TVSport, "TV/Sports");
+ caps.Categories.AddCategoryMapping(52, TorznabCatType.TVHD, "TV/B-Ray");
+ caps.Categories.AddCategoryMapping(53, TorznabCatType.TVSD, "TV/DVD-R");
+ caps.Categories.AddCategoryMapping(41, TorznabCatType.TV, "TV/Packs");
+ caps.Categories.AddCategoryMapping(55, TorznabCatType.TV, "TV/Kids");
+ caps.Categories.AddCategoryMapping(57, TorznabCatType.TV, "TV/DiVERSiTY");
+ caps.Categories.AddCategoryMapping(49, TorznabCatType.TVHD, "TV/HD");
+ caps.Categories.AddCategoryMapping(2, TorznabCatType.TVSD, "TV/Episodes");
+ caps.Categories.AddCategoryMapping(30, TorznabCatType.TVAnime, "TV/Anime");
+ caps.Categories.AddCategoryMapping(25, TorznabCatType.PCISO, "Games/PC ISO");
+ caps.Categories.AddCategoryMapping(39, TorznabCatType.ConsoleWii, "Games/Wii");
+ caps.Categories.AddCategoryMapping(45, TorznabCatType.ConsolePS3, "Games/PS3");
+ caps.Categories.AddCategoryMapping(35, TorznabCatType.Console, "Games/Nintendo");
+ caps.Categories.AddCategoryMapping(33, TorznabCatType.ConsoleXBox360, "Games/XboX360");
+ caps.Categories.AddCategoryMapping(46, TorznabCatType.PCMobileOther, "Mobile");
+ caps.Categories.AddCategoryMapping(24, TorznabCatType.PC0day, "Apps/0DAY");
+ caps.Categories.AddCategoryMapping(51, TorznabCatType.PCMac, "Mac");
+ caps.Categories.AddCategoryMapping(54, TorznabCatType.Books, "Educational");
+ caps.Categories.AddCategoryMapping(27, TorznabCatType.Books, "Books-Mags");
+ caps.Categories.AddCategoryMapping(26, TorznabCatType.Audio, "Music/Audio");
+ caps.Categories.AddCategoryMapping(3, TorznabCatType.Audio, "Music/Flac");
+ caps.Categories.AddCategoryMapping(44, TorznabCatType.Audio, "Music/Pack");
+ caps.Categories.AddCategoryMapping(29, TorznabCatType.AudioVideo, "Music/Video");
+
+ return caps;
+ }
+
+ public override async Task ApplyConfiguration(JToken configJson)
+ {
+ LoadValuesFromJson(configJson);
+
+ CookieHeader = configData.Cookie.Value;
+ try
+ {
+ var results = await PerformQuery(new TorznabQuery());
+ if (!results.Any())
+ throw new Exception("Found 0 results in the tracker");
+
+ IsConfigured = true;
+ SaveConfig();
+ return IndexerConfigurationStatus.Completed;
+ }
+ catch (Exception e)
+ {
+ IsConfigured = false;
+ throw new Exception("Your cookie did not work: " + e.Message);
+ }
+ }
+ protected override async Task> PerformQuery(TorznabQuery query)
+ {
+ var releases = new List();
+
+ // the order of the params is important!
+ var qc = new List();
+
+ var catList = MapTorznabCapsToTrackers(query);
+ foreach (var cat in catList)
+ qc.Add(cat);
+
+ if (((BoolConfigurationItem)configData.GetDynamic("Freeleech")).Value)
+ qc.Add("freeleech");
+
+ if (((BoolConfigurationItem)configData.GetDynamic("ExcludeArchives")).Value)
+ qc.Add("norar");
+
+ if (query.IsImdbQuery)
+ {
+ var term = query.ImdbID;
+
+ if (!string.IsNullOrWhiteSpace(query.GetEpisodeSearchString()))
+ {
+ term += $" {query.GetEpisodeSearchString()}";
+
+ if (query.Season > 0 && string.IsNullOrEmpty(query.Episode))
+ term += "*";
+ }
+
+ qc.Add("deep");
+ qc.Add("q");
+ qc.Add(WebUtilityHelpers.UrlEncode(term.Trim(), Encoding));
+ }
+ else
+ {
+ var term = query.GetQueryString();
+
+ if (!string.IsNullOrWhiteSpace(query.GetEpisodeSearchString()) && query.Season > 0 && string.IsNullOrEmpty(query.Episode))
+ term += "*";
+
+ qc.Add("q");
+ qc.Add(WebUtilityHelpers.UrlEncode(term.Trim(), Encoding));
+ }
+
+ if (query.Limit > 0 && query.Offset > 0)
+ {
+ var page = query.Offset / query.Limit + 1;
+
+ qc.Add("p");
+ qc.Add(page.ToString());
+ }
+
+ var searchUrl = SearchUrl + string.Join("/", qc);
+ var response = await RequestWithCookiesAndRetryAsync(searchUrl);
+
+ try
+ {
+ var parser = new HtmlParser();
+ using var dom = parser.ParseDocument(response.ContentString);
+
+ var rows = dom.QuerySelectorAll("div.boxContent > table > tbody > tr");
+ foreach (var row in rows)
+ {
+ var title = CleanTitle(row.QuerySelector("td:nth-child(2) > div > a[href^=\"/t/\"]")?.TextContent);
+ var link = new Uri(SiteLink + row.QuerySelector("td:nth-child(4) a[href^=\"/download/\"]")?.GetAttribute("href")?.TrimStart('/'));
+ var details = new Uri(SiteLink + row.QuerySelector("td:nth-child(2) > div > a[href^=\"/t/\"]")?.GetAttribute("href")?.TrimStart('/'));
+ var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)")?.TextContent);
+ var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)")?.TextContent);
+ var pubDateStr = row.QuerySelector("td:nth-child(2) span[class^=\"elapsedDate\"]")?.GetAttribute("title")?.Replace(" at", "");
+ var cat = row.QuerySelector("td:nth-child(1) a")?.GetAttribute("href")?.Split('/').Last();
+ var downloadVolumeFactor = row.QuerySelector("td:nth-child(2) span:contains(\"[Freeleech]\")") != null ? 0 : 1;
+
+ var release = new ReleaseInfo
+ {
+ Guid = link,
+ Link = link,
+ Details = details,
+ Title = title,
+ Category = MapTrackerCatToNewznab(cat),
+ PublishDate = DateTime.ParseExact(pubDateStr, "dddd, MMMM d, yyyy h:mmtt", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
+ Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(6)")?.TextContent),
+ Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)")?.TextContent),
+ Seeders = seeders,
+ Peers = seeders + leechers,
+ DownloadVolumeFactor = downloadVolumeFactor,
+ UploadVolumeFactor = 1,
+ MinimumRatio = 1,
+ MinimumSeedTime = 259200 // 72 hours
+ };
+
+ releases.Add(release);
+ }
+ }
+ catch (Exception ex)
+ {
+ OnParseError(response.ContentString, ex);
+ }
+ return releases;
+ }
+
+ private static string CleanTitle(string title)
+ {
+ title = Regex.Replace(title, @"\[REQ(UEST)?\]", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ return title.Trim(' ', '.');
+ }
+ }
+}