diff --git a/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs b/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs index f0b245827..eac06866d 100644 --- a/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs +++ b/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs @@ -9,7 +9,7 @@ using System.Text; using System.Threading.Tasks; using AngleSharp.Html.Parser; 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; @@ -22,26 +22,24 @@ namespace Jackett.Common.Indexers.Abstract [ExcludeFromCodeCoverage] public abstract class GazelleTracker : BaseWebIndexer { - protected string LoginUrl => SiteLink + "login.php"; - protected string APIUrl => SiteLink + "ajax.php"; - protected string DownloadUrl => SiteLink + "torrents.php?action=download&usetoken=" + (useTokens ? "1" : "0") + "&id="; - protected string DetailsUrl => SiteLink + "torrents.php?torrentid="; - protected bool supportsFreeleechTokens; - protected bool imdbInTags; - protected bool supportsCategories = true; // set to false if the tracker doesn't include the categories in the API search results - protected bool useTokens = false; + protected virtual string LoginUrl => SiteLink + "login.php"; + protected virtual string APIUrl => SiteLink + "ajax.php"; + protected virtual string DownloadUrl => SiteLink + "torrents.php?action=download&usetoken=" + (useTokens ? "1" : "0") + "&id="; + protected virtual string DetailsUrl => SiteLink + "torrents.php?torrentid="; + + protected bool useTokens; protected string cookie = ""; - private new ConfigurationDataBasicLogin configData - { - get => (ConfigurationDataBasicLogin)base.configData; - set => base.configData = value; - } + private readonly bool imdbInTags; + private readonly bool useApiKey; + + private new ConfigurationDataGazelleTracker configData => (ConfigurationDataGazelleTracker)base.configData; protected GazelleTracker(string link, string id, string name, string description, IIndexerConfigurationService configService, WebClient client, Logger logger, IProtectionService p, TorznabCapabilities caps, bool supportsFreeleechTokens, - bool imdbInTags = false, bool has2Fa = false) + bool imdbInTags = false, bool has2Fa = false, bool useApiKey = false, + string instructionMessageOptional = null) : base(id: id, name: name, description: description, @@ -51,55 +49,57 @@ namespace Jackett.Common.Indexers.Abstract client: client, logger: logger, p: p, - configData: new ConfigurationDataBasicLogin()) + configData: new ConfigurationDataGazelleTracker( + has2Fa, supportsFreeleechTokens, useApiKey, instructionMessageOptional)) { Encoding = Encoding.UTF8; - this.supportsFreeleechTokens = supportsFreeleechTokens; + this.imdbInTags = imdbInTags; - - if (has2Fa) - { - var cookieHint = new ConfigurationData.DisplayItem( - "
  1. (use this only if 2FA is enabled for your account)
  2. Login to this tracker with your browser
  3. Open the DevTools panel by pressing F12
  4. Select the Network tab
  5. Click on the Doc button
  6. Refresh the page by pressing F5
  7. Select the Headers tab
  8. Find 'cookie:' in the Request Headers section
  9. Copy & paste the whole cookie string to here.
") - { - Name = "CookieHint" - }; - configData.AddDynamic("cookieHint", cookieHint); - var cookieItem = new ConfigurationData.StringItem { Value = "" }; - cookieItem.Name = "Cookie"; - configData.AddDynamic("cookie", cookieItem); - } - - if (supportsFreeleechTokens) - { - var useTokenItem = new ConfigurationData.BoolItem { Value = false }; - useTokenItem.Name = "Use Freeleech Tokens when available"; - configData.AddDynamic("usetoken", useTokenItem); - } + this.useApiKey = useApiKey; } public override void LoadValuesFromJson(JToken jsonConfig, bool useProtectionService = false) { base.LoadValuesFromJson(jsonConfig, useProtectionService); - var cookieItem = (ConfigurationData.StringItem)configData.GetDynamic("cookie"); + var cookieItem = configData.CookieItem; if (cookieItem != null) - { cookie = cookieItem.Value; - } - var useTokenItem = (ConfigurationData.BoolItem)configData.GetDynamic("usetoken"); + var useTokenItem = configData.UseTokenItem; if (useTokenItem != null) - { useTokens = useTokenItem.Value; - } - } public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); + if (useApiKey) + { + var apiKey = configData.ApiKey; + if (apiKey?.Value == null) + throw new Exception("Invalid API Key configured"); + if (apiKey.Value.Length != 41) + throw new Exception($"Invalid API Key configured: expected length: 41, got {apiKey.Value.Length}"); + + 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 API Key did not work: {e.Message}"); + } + } + var pairs = new Dictionary { { "username", configData.Username.Value }, { "password", configData.Password.Value }, @@ -114,9 +114,7 @@ namespace Jackett.Common.Indexers.Abstract { var results = await PerformQuery(new TorznabQuery()); if (!results.Any()) - { throw new Exception("Found 0 results in the tracker"); - } IsConfigured = true; SaveConfig(); @@ -125,7 +123,7 @@ namespace Jackett.Common.Indexers.Abstract catch (Exception e) { IsConfigured = false; - throw new Exception("Your cookie did not work: " + e.Message); + throw new Exception($"Your cookie did not work: {e.Message}"); } } @@ -162,7 +160,6 @@ namespace Jackett.Common.Indexers.Abstract { "order_way", "desc" } }; - if (!string.IsNullOrWhiteSpace(query.ImdbID)) { if (imdbInTags) @@ -171,9 +168,7 @@ namespace Jackett.Common.Indexers.Abstract queryCollection.Add("cataloguenumber", query.ImdbID); } else if (!string.IsNullOrWhiteSpace(searchString)) - { queryCollection.Add("searchstr", searchString); - } if (query.Artist != null) queryCollection.Add("artistname", query.Artist); @@ -187,18 +182,17 @@ namespace Jackett.Common.Indexers.Abstract if (query.Album != null) queryCollection.Add("groupname", query.Album); - if (supportsCategories) - { - foreach (var cat in MapTorznabCapsToTrackers(query)) - { - queryCollection.Add("filter_cat[" + cat + "]", "1"); - } - } + foreach (var cat in MapTorznabCapsToTrackers(query)) + queryCollection.Add("filter_cat[" + cat + "]", "1"); searchUrl += "?" + queryCollection.GetQueryString(); - var response = await RequestWithCookiesAndRetryAsync(searchUrl); - if (response.IsRedirect) + var apiKey = configData.ApiKey; + var headers = apiKey != null ? new Dictionary { ["Authorization"] = apiKey.Value } : null; + + var response = await RequestWithCookiesAndRetryAsync(searchUrl, headers: headers); + // we get a redirect in html pages and an error message in json response (api) + if (response.IsRedirect || (response.ContentString != null && response.ContentString.Contains("\"bad credentials\""))) { // re-login await ApplyConfiguration(null); @@ -241,14 +235,11 @@ namespace Jackett.Common.Indexers.Abstract if (imdbInTags) - { release.Imdb = tags .Select(tag => ParseUtil.GetImdbID((string)tag)) .Where(tag => tag != null).FirstIfSingleOrDefault(); - } if (r["torrents"] is JArray) - { foreach (JObject torrent in r["torrents"]) { var release2 = (ReleaseInfo)release.Clone(); @@ -256,7 +247,6 @@ namespace Jackett.Common.Indexers.Abstract if (ReleaseInfoPostParse(release2, torrent, r)) releases.Add(release2); } - } else { FillReleaseInfoFromJson(release, r); @@ -282,9 +272,7 @@ namespace Jackett.Common.Indexers.Abstract var time = (string)torrent["time"]; if (!string.IsNullOrEmpty(time)) - { release.PublishDate = DateTime.ParseExact(time + " +0000", "yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture); - } var flags = new List(); @@ -365,14 +353,10 @@ namespace Jackett.Common.Indexers.Abstract release.DownloadVolumeFactor = 1; release.UploadVolumeFactor = 1; if ((bool)torrent["isFreeleech"]) - { release.DownloadVolumeFactor = 0; - } var isPersonalFreeleech = (bool?)torrent["isPersonalFreeleech"]; if (isPersonalFreeleech != null && isPersonalFreeleech == true) - { release.DownloadVolumeFactor = 0; - } if ((bool)torrent["isNeutralLeech"]) { release.DownloadVolumeFactor = 0; @@ -382,7 +366,10 @@ namespace Jackett.Common.Indexers.Abstract public override async Task Download(Uri link) { - var content = await base.Download(link); + var apiKey = configData.ApiKey; + var headers = apiKey != null ? new Dictionary { ["Authorization"] = apiKey.Value } : null; + + var content = await base.Download(link, RequestType.GET, headers: headers); // Check if we're out of FL tokens/torrent is to large // most gazelle trackers will simply return the torrent anyway but e.g. redacted will return an error @@ -398,7 +385,7 @@ namespace Jackett.Common.Indexers.Abstract { // download again with usetoken=0 var requestLinkNew = requestLink.Replace("usetoken=1", "usetoken=0"); - content = await base.Download(new Uri(requestLinkNew)); + content = await base.Download(new Uri(requestLinkNew), RequestType.GET, headers: headers); } } diff --git a/src/Jackett.Common/Indexers/BaseIndexer.cs b/src/Jackett.Common/Indexers/BaseIndexer.cs index f39932254..bbeafcdba 100644 --- a/src/Jackett.Common/Indexers/BaseIndexer.cs +++ b/src/Jackett.Common/Indexers/BaseIndexer.cs @@ -381,7 +381,7 @@ namespace Jackett.Common.Indexers return await Download(uncleanLink, RequestType.GET); } - protected async Task Download(Uri link, RequestType method, string refererlink = null) + protected async Task Download(Uri link, RequestType method, string refererlink = null, Dictionaryheaders = null) { // return magnet link if (link.Scheme == "magnet") @@ -392,11 +392,8 @@ namespace Jackett.Common.Indexers .Replace("(", "%28") .Replace(")", "%29") .Replace("'", "%27"); - var response = await RequestWithCookiesAndRetryAsync(requestLink, null, method, requestLink); + var response = await RequestWithCookiesAndRetryAsync(requestLink, null, method, refererlink, null, headers); - // if referer link is provied it will be used - if (refererlink != null) - response = await RequestWithCookiesAndRetryAsync(requestLink, null, method, refererlink); if (response.IsRedirect) { await FollowIfRedirect(response); diff --git a/src/Jackett.Common/Indexers/Redacted.cs b/src/Jackett.Common/Indexers/Redacted.cs index b5f0b2ed0..824ba4aa6 100644 --- a/src/Jackett.Common/Indexers/Redacted.cs +++ b/src/Jackett.Common/Indexers/Redacted.cs @@ -5,14 +5,16 @@ using System.Threading.Tasks; using Jackett.Common.Indexers.Abstract; using Jackett.Common.Models; using Jackett.Common.Services.Interfaces; -using Jackett.Common.Utils.Clients; using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; namespace Jackett.Common.Indexers { [ExcludeFromCodeCoverage] public class Redacted : GazelleTracker { + protected override string DownloadUrl => SiteLink + "ajax.php?action=download&usetoken=" + (useTokens ? "1" : "0") + "&id="; + public Redacted(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) : base(id: "redacted", name: "Redacted", @@ -27,7 +29,10 @@ namespace Jackett.Common.Indexers logger: l, p: ps, supportsFreeleechTokens: true, - has2Fa: true) + has2Fa: false, + useApiKey: true, + instructionMessageOptional: "
  1. Go to Redacted's site and open your account settings.
  2. Go to Access Settings tab and copy the API Key.
  3. Ensure that you've checked Confirm API Key.
  4. Finally, click Save Profile.
" + ) { Language = "en-us"; Type = "private"; @@ -48,5 +53,6 @@ namespace Jackett.Common.Indexers results = results.Where(release => query.MatchQueryStringAND(release.Title)); return results; } + } } diff --git a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs new file mode 100644 index 000000000..99e7b8cba --- /dev/null +++ b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs @@ -0,0 +1,49 @@ +namespace Jackett.Common.Models.IndexerConfig.Bespoke +{ + public class ConfigurationDataGazelleTracker : ConfigurationData + { + public StringItem Username { get; private set; } + public StringItem Password { get; private set; } + public StringItem ApiKey { get; private set; } + public DisplayItem CookieHint { get; private set; } + public StringItem CookieItem { get; private set; } + public BoolItem UseTokenItem { get; private set; } + public DisplayItem Instructions { get; private set; } + + public ConfigurationDataGazelleTracker(bool has2Fa = false, bool supportsFreeleechToken = false, + bool useApiKey = false, string instructionMessageOptional = null) + { + if (useApiKey) + ApiKey = new StringItem { Name = "APIKey" }; + else + { + Username = new StringItem { Name = "Username" }; + Password = new StringItem { Name = "Password" }; + } + + if (has2Fa) + { + CookieHint = new DisplayItem( + @"Use the Cookie field only if 2FA is enabled for your account, let it empty otherwise. +
  1. Login to this tracker with your browser +
  2. Open the DevTools panel by pressing F12 +
  3. Select the Network tab +
  4. Click on the Doc button +
  5. Refresh the page by pressing F5 +
  6. Select the Headers tab +
  7. Find 'cookie:' in the Request Headers section +
  8. Copy & paste the whole cookie string to here.
") + { + Name = "CookieHint" + }; + CookieItem = new StringItem { Name = "Cookie", Value = "" }; + } + + if (supportsFreeleechToken) + UseTokenItem = new BoolItem { Name = "Use Freeleech Tokens when Available", Value = false }; + + Instructions = new DisplayItem(instructionMessageOptional) { Name = "" }; + } + + } +} diff --git a/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs b/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs index db6583c6c..d999fe30b 100644 --- a/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs +++ b/src/Jackett.Common/Models/IndexerConfig/ConfigurationData.cs @@ -169,6 +169,7 @@ namespace Jackett.Common.Models.IndexerConfig .GetProperties() .Where(p => p.CanRead) .Where(p => p.PropertyType.IsSubclassOf(typeof(Item))) + .Where(p => p.GetValue(this) != null) .Select(p => (Item)p.GetValue(this)).ToList(); // remove/insert Site Link manualy to make sure it shows up first