diff --git a/README.md b/README.md
index c7d680265..3bd66075e 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ We were previously focused on TV but are working on extending searches to allow
* [BeyondHD](https://beyondhd.me/)
* [BIT-HDTV](https://www.bit-hdtv.com)
* [BitMeTV](http://www.bitmetv.org/)
+ * [BlueTigers](https://www.bluetigers.ca/)
* [BTN](http://broadcasthe.net)
* [Demonoid](http://www.demonoid.pw/)
* [EuTorrents](https://eutorrents.to/)
diff --git a/src/Jackett/Content/logos/bluetigers.png b/src/Jackett/Content/logos/bluetigers.png
new file mode 100644
index 000000000..60b131479
Binary files /dev/null and b/src/Jackett/Content/logos/bluetigers.png differ
diff --git a/src/Jackett/Indexers/BlueTigers.cs b/src/Jackett/Indexers/BlueTigers.cs
new file mode 100644
index 000000000..543781ea8
--- /dev/null
+++ b/src/Jackett/Indexers/BlueTigers.cs
@@ -0,0 +1,211 @@
+using CsQuery;
+using Jackett.Models;
+using Jackett.Services;
+using Jackett.Utils;
+using Jackett.Utils.Clients;
+using Newtonsoft.Json.Linq;
+using NLog;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Jackett.Models.IndexerConfig.Bespoke;
+
+namespace Jackett.Indexers
+{
+ public class BlueTigers : BaseIndexer, IIndexer
+ {
+ private string LoginUrl => SiteLink + "account-login.php";
+ private string TorrentSearchUrl => SiteLink + "torrents-search.php";
+ private string IndexUrl => SiteLink + "index.php";
+
+ private ConfigurationDataBlueTigers ConfigData
+ {
+ get { return (ConfigurationDataBlueTigers)configData; }
+ set { base.configData = value; }
+ }
+
+ public BlueTigers(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps)
+ : base(name: "BlueTigers",
+ description: "BlueTigers - No Ratio - Private",
+ link: "https://www.bluetigers.ca/",
+ caps: new TorznabCapabilities(),
+ manager: i,
+ client: wc,
+ logger: l,
+ p: ps,
+ configData: new ConfigurationDataBlueTigers(@"BlueTigers can search for one or all languages.
+ If you select 2 languages below, results will contain all 3 languages.
+
For best results change the torrents per page setting to 50 in your BlueTigers profile."))
+ {
+ AddCategoryMapping(19, TorznabCatType.TV);
+ AddCategoryMapping(2, TorznabCatType.TV);
+ AddCategoryMapping(17, TorznabCatType.TV);
+
+ AddCategoryMapping(52, TorznabCatType.ConsoleXbox);
+
+ AddCategoryMapping(29, TorznabCatType.PCMac);
+ }
+
+ public async Task ApplyConfiguration(JToken configJson)
+ {
+ ConfigData.LoadValuesFromJson(configJson);
+
+ if (ConfigData.French.Value == false && ConfigData.English.Value == false && ConfigData.Spanish.Value == false)
+ throw new ExceptionWithConfigData("Please select at least one language.", ConfigData);
+
+ await RequestStringWithCookies(LoginUrl, string.Empty);
+ var pairs = new Dictionary {
+ { "username", ConfigData.Username.Value },
+ { "password", ConfigData.Password.Value },
+ { "take_login", "1" }
+ };
+
+ var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, IndexUrl, SiteLink);
+ Regex rgx = new Regex(@"uid=[0-9]{1,10}; pass=[a-z0-9]{1,40};");
+ await ConfigureIfOK(result.Cookies, rgx.IsMatch(result.Cookies), () =>
+ {
+ var errorMessage = "Error while trying to login.";
+ throw new ExceptionWithConfigData(errorMessage, ConfigData);
+ });
+
+ return IndexerConfigurationStatus.RequiresTesting;
+ }
+
+ public async Task> PerformQuery(TorznabQuery query)
+ {
+ List releases = new List();
+
+ NameValueCollection qParams = new NameValueCollection();
+ if (ConfigData.French.Value && !ConfigData.English.Value && !ConfigData.Spanish.Value)
+ {
+ qParams.Add("lang", "1");
+ }
+ else
+ {
+ if (!ConfigData.French.Value && ConfigData.English.Value && !ConfigData.Spanish.Value)
+ {
+ qParams.Add("lang", "2");
+ }
+ else
+ {
+ if (!ConfigData.French.Value && !ConfigData.English.Value && ConfigData.Spanish.Value)
+ {
+ qParams.Add("lang", "3");
+ }
+ else
+ {
+ qParams.Add("lang", "0");
+ }
+ }
+
+ }
+
+ List catList = MapTorznabCapsToTrackers(query);
+ foreach (string cat in catList)
+ {
+ qParams.Add("cat", cat);
+ }
+
+ if (!string.IsNullOrEmpty(query.SanitizedSearchTerm))
+ {
+ qParams.Add("search", query.GetQueryString());
+ }
+
+ string queryStr = qParams.GetQueryString();
+ string searchUrl = $"{TorrentSearchUrl}?incldead=0&freeleech=0&sort=id&order=ascdesc&{queryStr}";
+
+ List torrentRowList = new List();
+
+ var results = await RequestStringWithCookiesAndRetry(searchUrl);
+ try
+ {
+ CQ fDom = results.Content;
+ var firstPageRows = fDom["table[class='ttable_headinner'] > tbody > tr:not(:First-child)"];
+ torrentRowList.AddRange(firstPageRows.Select(fRow => fRow.Cq()));
+
+ //If a search term is used, follow upto the first 4 pages (initial plus 3 more)
+ if (!string.IsNullOrWhiteSpace(query.GetQueryString()) && fDom["a[class='boutons']"].Filter("a[href*=&page=]").Length > 0)
+ {
+ int pageLinkCount;
+ int.TryParse(fDom["a[class='boutons']"].Filter("a[href*=&page=]").Last().Attr("href").Split(new[] { "&page=" }, StringSplitOptions.None).LastOrDefault(), out pageLinkCount);
+ for (int i = 1; i < Math.Min(4, pageLinkCount + 1); i++)
+ {
+ var sResults = await RequestStringWithCookiesAndRetry($"{searchUrl}&page={i}");
+ CQ sDom = sResults.Content;
+ var additionalPageRows = sDom["table[class='ttable_headinner'] > tbody > tr:not(:First-child)"];
+ torrentRowList.AddRange(additionalPageRows.Select(sRow => sRow.Cq()));
+ }
+ }
+
+ foreach (CQ tRow in torrentRowList)
+ {
+ long torrentId = 0;
+ string idTarget = "bookmarks.php?torrent=";
+ string id = tRow.Find("a[href*=" + idTarget + "]").First().Attr("href").Trim();
+ if (!string.IsNullOrEmpty(id) && id.Contains(idTarget))
+ {
+ long.TryParse(id.Substring(id.LastIndexOf(idTarget, StringComparison.Ordinal) + idTarget.Length), out torrentId);
+ }
+
+ if (torrentId <= 0) continue;
+
+ long category = 0;
+ string catTarget = "torrents.php?cat=";
+ string cat = tRow.Find("a[href*=" + catTarget + "]").First().Attr("href").Trim();
+ if (!string.IsNullOrEmpty(cat) && cat.Contains(catTarget))
+ {
+ long.TryParse(cat.Substring(cat.LastIndexOf(catTarget, StringComparison.Ordinal) + catTarget.Length), out category);
+ }
+
+ Uri guid = new Uri($"{SiteLink}torrents-details.php?hit=1&id={torrentId}");
+ Uri link = new Uri($"{SiteLink}download.php?hit=1&id={torrentId}");
+ Uri comments = new Uri($"{SiteLink}comments.php?type=torrent&id={torrentId}");
+ string title = tRow.Find("a[href*=torrents-details.php?id=]").First().Text().Trim();
+ string stats = tRow.Find("div[id=kt" + torrentId.ToString() + "]").First().Text();
+ string sizeStr = new Regex("Taille:(.*)Vitesse:").Match(stats).Groups[1].ToString().Trim();
+ string pubDateStr = new Regex("Ajout.:(.*)Compl.t.s").Match(stats).Groups[1].ToString().Trim();
+ DateTime pubDate = DateTime.ParseExact(pubDateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal);
+
+ string statistics = tRow.Find("a[href*=torrents-details.php?id=]").First().RenderSelection().Trim();
+ string startTag = "
+
@@ -203,6 +204,7 @@
+
@@ -439,6 +441,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataBlueTigers.cs b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataBlueTigers.cs
new file mode 100644
index 000000000..05cb8a51f
--- /dev/null
+++ b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataBlueTigers.cs
@@ -0,0 +1,63 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Jackett.Models.IndexerConfig.Bespoke
+{
+ public class ConfigurationDataBlueTigers : ConfigurationData
+ {
+ public StringItem Username { get; private set; }
+ public StringItem Password { get; private set; }
+ public DisplayItem Instructions { get; set; }
+ public BoolItem French { get; set; }
+ public BoolItem English { get; set; }
+ public BoolItem Spanish { get; set; }
+
+ public ConfigurationDataBlueTigers(string displayInstructions)
+ {
+ Username = new StringItem { Name = "Username", Value = "" };
+ Password = new StringItem { Name = "Password", Value = "" };
+ Instructions = new DisplayItem(displayInstructions) { Name = "" };
+ French = new BoolItem { Name = "French", Value = true };
+ English = new BoolItem { Name = "English", Value = true };
+ Spanish = new BoolItem { Name = "Spanish", Value = true };
+ }
+
+ public ConfigurationDataBlueTigers(JToken json)
+ {
+ ConfigurationDataNCore configData = new ConfigurationDataNCore();
+
+ dynamic configArray = JsonConvert.DeserializeObject(json.ToString());
+ foreach (var config in configArray)
+ {
+ string propertyName = UppercaseFirst((string)config.id);
+ switch (propertyName)
+ {
+ case "Username":
+ Username = new StringItem { Name = propertyName, Value = config.value };
+ break;
+ case "Password":
+ Password = new StringItem { Name = propertyName, Value = config.value };
+ break;
+ case "French":
+ French = new BoolItem { Name = propertyName, Value = config.value };
+ break;
+ case "English":
+ English = new BoolItem { Name = propertyName, Value = config.value };
+ break;
+ case "Spanish":
+ Spanish = new BoolItem { Name = propertyName, Value = config.value };
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ static string UppercaseFirst(string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ return string.Empty;
+ return char.ToUpper(s[0]) + s.Substring(1);
+ }
+ }
+}
\ No newline at end of file