diff --git a/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs b/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs index 2b8ea89a9..ac76a6c38 100644 --- a/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs +++ b/src/Jackett.Common/Indexers/Abstract/GazelleTracker.cs @@ -8,7 +8,9 @@ using System.Net; using System.Text; using System.Threading.Tasks; using AngleSharp.Html.Parser; +using BencodeNET.Objects; using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; using Jackett.Common.Models.IndexerConfig.Bespoke; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; @@ -24,9 +26,10 @@ namespace Jackett.Common.Indexers.Abstract { 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") + (usePassKey ? "&torrent_pass=" + configData.PassKey.Value : "") + "&id="; + protected virtual string DownloadUrl => SiteLink + "torrents.php?action=download&usetoken=" + (useTokens ? "1" : "0") + (usePassKey ? "&torrent_pass=" + configData.PassKey.Value : "") + (useAuthKey ? "&authkey=" + configData.AuthKey.Value : "") + "&id="; protected virtual string DetailsUrl => SiteLink + "torrents.php?torrentid="; protected virtual string PosterUrl => SiteLink; + protected virtual string AuthorizationName => "Authorization"; protected virtual string AuthorizationFormat => "{0}"; protected virtual int ApiKeyLength => 41; protected virtual string FlipOptionalTokenString(string requestLink) => requestLink.Replace("usetoken=1", "usetoken=0"); @@ -34,17 +37,22 @@ namespace Jackett.Common.Indexers.Abstract protected bool useTokens; protected string cookie = ""; - private readonly bool imdbInTags; - private readonly bool useApiKey; - private readonly bool usePassKey; + protected readonly bool imdbInTags; + protected readonly bool useApiKey; + protected readonly bool usePassKey; + protected readonly bool useAuthKey; - private new ConfigurationDataGazelleTracker configData => (ConfigurationDataGazelleTracker)base.configData; + private new ConfigurationDataGazelleTracker configData + { + get => (ConfigurationDataGazelleTracker)base.configData; + set => base.configData = value; + } protected GazelleTracker(string link, string id, string name, string description, IIndexerConfigurationService configService, WebClient client, Logger logger, IProtectionService p, ICacheService cs, TorznabCapabilities caps, bool supportsFreeleechTokens, bool imdbInTags = false, bool has2Fa = false, - bool useApiKey = false, bool usePassKey = false, string instructionMessageOptional = null) + bool useApiKey = false, bool usePassKey = false, bool useAuthKey = false, string instructionMessageOptional = null) : base(id: id, name: name, description: description, @@ -56,13 +64,14 @@ namespace Jackett.Common.Indexers.Abstract p: p, cacheService: cs, configData: new ConfigurationDataGazelleTracker( - has2Fa, supportsFreeleechTokens, useApiKey, usePassKey, instructionMessageOptional)) + has2Fa, supportsFreeleechTokens, useApiKey, usePassKey, useAuthKey, instructionMessageOptional)) { Encoding = Encoding.UTF8; this.imdbInTags = imdbInTags; this.useApiKey = useApiKey; this.usePassKey = usePassKey; + this.useAuthKey = useAuthKey; } public override void LoadValuesFromJson(JToken jsonConfig, bool useProtectionService = false) @@ -199,7 +208,7 @@ namespace Jackett.Common.Indexers.Abstract searchUrl += "?" + queryCollection.GetQueryString().Replace(".", " "); var apiKey = configData.ApiKey; - var headers = apiKey != null ? new Dictionary { ["Authorization"] = String.Format(AuthorizationFormat, apiKey.Value) } : null; + var headers = apiKey != null ? new Dictionary { [AuthorizationName] = String.Format(AuthorizationFormat, 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) @@ -217,7 +226,6 @@ namespace Jackett.Common.Indexers.Abstract throw new Exception(errorReason); } - try { var json = JObject.Parse(response.ContentString); @@ -296,7 +304,7 @@ namespace Jackett.Common.Indexers.Abstract // hook to add/modify the parsed information, return false to exclude the torrent from the results protected virtual bool ReleaseInfoPostParse(ReleaseInfo release, JObject torrent, JObject result) => true; - private void FillReleaseInfoFromJson(ReleaseInfo release, JObject torrent) + protected void FillReleaseInfoFromJson(ReleaseInfo release, JObject torrent) { var torrentId = torrent["torrentId"]; @@ -397,7 +405,7 @@ namespace Jackett.Common.Indexers.Abstract public override async Task Download(Uri link) { var apiKey = configData.ApiKey; - var headers = apiKey != null ? new Dictionary { ["Authorization"] = String.Format(AuthorizationFormat, apiKey.Value) } : null; + var headers = apiKey != null ? new Dictionary { [AuthorizationName] = String.Format(AuthorizationFormat, apiKey.Value) } : null; var response = await base.RequestWithCookiesAsync(link.ToString(), null, RequestType.GET, headers: headers); var content = response.ContentBytes; diff --git a/src/Jackett.Common/Indexers/GazelleGamesAPI.cs b/src/Jackett.Common/Indexers/GazelleGamesAPI.cs new file mode 100644 index 000000000..5d8a488f1 --- /dev/null +++ b/src/Jackett.Common/Indexers/GazelleGamesAPI.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using FlareSolverrSharp.Types; +using Jackett.Common.Indexers.Abstract; +using Jackett.Common.Models; +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; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + public class GazelleGamesApi : GazelleTracker + { + // API Reference: https://gazellegames.net/wiki.php?action=article&id=401 + protected override string APIUrl => SiteLink + "api.php"; + protected override string AuthorizationName => "X-API-Key"; + protected override int ApiKeyLength => 64; + protected override string FlipOptionalTokenString(string requestLink) => requestLink.Replace("usetoken=1", ""); + public GazelleGamesApi(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, + ICacheService cs) + : base(id: "gazellegamesapi", + name: "GazelleGamesApi", + description: "A gaming tracker", + link: "https://gazellegames.net/", + caps: new TorznabCapabilities(), + configService: configService, + client: wc, + logger: l, + p: ps, + cs: cs, + supportsFreeleechTokens: true, + has2Fa: false, + useApiKey: true, + usePassKey: true, + useAuthKey: true, + instructionMessageOptional: "
  1. Go to GGn's site and open your account settings.
  2. Under Access Settings click on 'Create a new token'
  3. Give it a name you like and click Generate.
  4. Copy the generated API Key and paste it in the above text field.
") + { + Language = "en-US"; + Type = "private"; + + configData.AddDynamic("searchgroupnames", new BoolConfigurationItem("Search Group Names Only") { Value = false }); + + // Apple + AddCategoryMapping("Mac", TorznabCatType.ConsoleOther, "Mac"); + AddCategoryMapping("iOS", TorznabCatType.PCMobileiOS, "iOS"); + AddCategoryMapping("Apple Bandai Pippin", TorznabCatType.ConsoleOther, "Apple Bandai Pippin"); + + // Google + AddCategoryMapping("Android", TorznabCatType.PCMobileAndroid, "Android"); + + // Microsoft + AddCategoryMapping("DOS", TorznabCatType.PCGames, "DOS"); + AddCategoryMapping("Windows", TorznabCatType.PCGames, "Windows"); + AddCategoryMapping("Xbox", TorznabCatType.ConsoleXBox, "Xbox"); + AddCategoryMapping("Xbox 360", TorznabCatType.ConsoleXBox360, "Xbox 360"); + + // Nintendo + AddCategoryMapping("Game Boy", TorznabCatType.ConsoleOther, "Game Boy"); + AddCategoryMapping("Game Boy Advance", TorznabCatType.ConsoleOther, "Game Boy Advance"); + AddCategoryMapping("Game Boy Color", TorznabCatType.ConsoleOther, "Game Boy Color"); + AddCategoryMapping("NES", TorznabCatType.ConsoleOther, "NES"); + AddCategoryMapping("Nintendo 64", TorznabCatType.ConsoleOther, "Nintendo 64"); + AddCategoryMapping("Nintendo 3DS", TorznabCatType.ConsoleOther, "Nintendo 3DS"); + AddCategoryMapping("New Nintendo 3DS", TorznabCatType.ConsoleOther, "New Nintendo 3DS"); + AddCategoryMapping("Nintendo DS", TorznabCatType.ConsoleNDS, "Nintendo DS"); + AddCategoryMapping("Nintendo GameCube", TorznabCatType.ConsoleOther, "Nintendo GameCube"); + AddCategoryMapping("Pokemon Mini", TorznabCatType.ConsoleOther, "Pokemon Mini"); + AddCategoryMapping("SNES", TorznabCatType.ConsoleOther, "SNES"); + AddCategoryMapping("Virtual Boy", TorznabCatType.ConsoleOther, "Virtual Boy"); + AddCategoryMapping("Wii", TorznabCatType.ConsoleWii, "Wii"); + AddCategoryMapping("Wii U", TorznabCatType.ConsoleWiiU, "Wii U"); + + // Sony + AddCategoryMapping("PlayStation 1", TorznabCatType.ConsoleOther, "PlayStation 1"); + AddCategoryMapping("PlayStation 2", TorznabCatType.ConsoleOther, "PlayStation 2"); + AddCategoryMapping("PlayStation 3", TorznabCatType.ConsolePS3, "PlayStation 3"); + AddCategoryMapping("PlayStation 4", TorznabCatType.ConsolePS4, "PlayStation 4"); + AddCategoryMapping("PlayStation Portable", TorznabCatType.ConsolePSP, "PlayStation Portable"); + AddCategoryMapping("PlayStation Vita", TorznabCatType.ConsolePSVita, "PlayStation Vita"); + + // Sega + AddCategoryMapping("Dreamcast", TorznabCatType.ConsoleOther, "Dreamcast"); + AddCategoryMapping("Game Gear", TorznabCatType.ConsoleOther, "Game Gear"); + AddCategoryMapping("Master System", TorznabCatType.ConsoleOther, "Master System"); + AddCategoryMapping("Mega Drive", TorznabCatType.ConsoleOther, "Mega Drive"); + AddCategoryMapping("Pico", TorznabCatType.ConsoleOther, "Pico"); + AddCategoryMapping("Saturn", TorznabCatType.ConsoleOther, "Saturn"); + AddCategoryMapping("SG-1000", TorznabCatType.ConsoleOther, "SG-1000"); + + // Atari + AddCategoryMapping("Atari 2600", TorznabCatType.ConsoleOther, "Atari 2600"); + AddCategoryMapping("Atari 5200", TorznabCatType.ConsoleOther, "Atari 5200"); + AddCategoryMapping("Atari 7800", TorznabCatType.ConsoleOther, "Atari 7800"); + AddCategoryMapping("Atari Jaguar", TorznabCatType.ConsoleOther, "Atari Jaguar"); + AddCategoryMapping("Atari Lynx", TorznabCatType.ConsoleOther, "Atari Lynx"); + AddCategoryMapping("Atari ST", TorznabCatType.ConsoleOther, "Atari ST"); + + // Amstrad + AddCategoryMapping("Amstrad CPC", TorznabCatType.ConsoleOther, "Amstrad CPC"); + + // Sinclair + AddCategoryMapping("ZX Spectrum", TorznabCatType.ConsoleOther, "ZX Spectrum"); + + // Spectravideo + AddCategoryMapping("MSX", TorznabCatType.ConsoleOther, "MSX"); + AddCategoryMapping("MSX 2", TorznabCatType.ConsoleOther, "MSX 2"); + + // Tiger + AddCategoryMapping("Game.com", TorznabCatType.ConsoleOther, "Game.com"); + AddCategoryMapping("Gizmondo", TorznabCatType.ConsoleOther, "Gizmondo"); + + // VTech + AddCategoryMapping("V.Smile", TorznabCatType.ConsoleOther, "V.Smile"); + AddCategoryMapping("CreatiVision", TorznabCatType.ConsoleOther, "CreatiVision"); + + // Tabletop Games + AddCategoryMapping("Board Game", TorznabCatType.ConsoleOther, "Board Game"); + AddCategoryMapping("Card Game", TorznabCatType.ConsoleOther, "Card Game"); + AddCategoryMapping("Miniature Wargames", TorznabCatType.ConsoleOther, "Miniature Wargames"); + AddCategoryMapping("Pen and Paper RPG", TorznabCatType.ConsoleOther, "Pen and Paper RPG"); + + // Other + AddCategoryMapping("3DO", TorznabCatType.ConsoleOther, "3DO"); + AddCategoryMapping("Bandai WonderSwan", TorznabCatType.ConsoleOther, "Bandai WonderSwan"); + AddCategoryMapping("Bandai WonderSwan Color", TorznabCatType.ConsoleOther, "Bandai WonderSwan Color"); + AddCategoryMapping("Casio Loopy", TorznabCatType.ConsoleOther, "Casio Loopy"); + AddCategoryMapping("Casio PV-1000", TorznabCatType.ConsoleOther, "Casio PV-1000"); + AddCategoryMapping("Colecovision", TorznabCatType.ConsoleOther, "Colecovision"); + AddCategoryMapping("Commodore 64", TorznabCatType.ConsoleOther, "Commodore 64"); + AddCategoryMapping("Commodore 128", TorznabCatType.ConsoleOther, "Commodore 128"); + AddCategoryMapping("Commodore Amiga", TorznabCatType.ConsoleOther, "Commodore Amiga"); + AddCategoryMapping("Commodore Plus-4", TorznabCatType.ConsoleOther, "Commodore Plus-4"); + AddCategoryMapping("Commodore VIC-20", TorznabCatType.ConsoleOther, "Commodore VIC-20"); + AddCategoryMapping("Emerson Arcadia 2001", TorznabCatType.ConsoleOther, "Emerson Arcadia 2001"); + AddCategoryMapping("Entex Adventure Vision", TorznabCatType.ConsoleOther, "Entex Adventure Vision"); + AddCategoryMapping("Epoch Super Casette Vision", TorznabCatType.ConsoleOther, "Epoch Super Casette Vision"); + AddCategoryMapping("Fairchild Channel F", TorznabCatType.ConsoleOther, "Fairchild Channel F"); + AddCategoryMapping("Funtech Super Acan", TorznabCatType.ConsoleOther, "Funtech Super Acan"); + AddCategoryMapping("GamePark GP32", TorznabCatType.ConsoleOther, "GamePark GP32"); + AddCategoryMapping("General Computer Vectrex", TorznabCatType.ConsoleOther, "General Computer Vectrex"); + AddCategoryMapping("Interactive DVD", TorznabCatType.ConsoleOther, "Interactive DVD"); + AddCategoryMapping("Linux", TorznabCatType.ConsoleOther, "Linux"); + AddCategoryMapping("Hartung Game Master", TorznabCatType.ConsoleOther, "Hartung Game Master"); + AddCategoryMapping("Magnavox-Phillips Odyssey", TorznabCatType.ConsoleOther, "Magnavox-Phillips Odyssey"); + AddCategoryMapping("Mattel Intellivision", TorznabCatType.ConsoleOther, "Mattel Intellivision"); + AddCategoryMapping("Memotech MTX", TorznabCatType.ConsoleOther, "Memotech MTX"); + AddCategoryMapping("Miles Gordon Sam Coupe", TorznabCatType.ConsoleOther, "Miles Gordon Sam Coupe"); + AddCategoryMapping("NEC PC-98", TorznabCatType.ConsoleOther, "NEC PC-98"); + AddCategoryMapping("NEC PC-FX", TorznabCatType.ConsoleOther, "NEC PC-FX"); + AddCategoryMapping("NEC SuperGrafx", TorznabCatType.ConsoleOther, "NEC SuperGrafx"); + AddCategoryMapping("NEC TurboGrafx-16", TorznabCatType.ConsoleOther, "NEC TurboGrafx-16"); + AddCategoryMapping("Nokia N-Gage", TorznabCatType.ConsoleOther, "Nokia N-Gage"); + AddCategoryMapping("Ouya", TorznabCatType.ConsoleOther, "Ouya"); + AddCategoryMapping("Philips Videopac+", TorznabCatType.ConsoleOther, "Philips Videopac+"); + AddCategoryMapping("Phone/PDA", TorznabCatType.ConsoleOther, "Phone/PDA"); + AddCategoryMapping("RCA Studio II", TorznabCatType.ConsoleOther, "RCA Studio II"); + AddCategoryMapping("Sharp X1", TorznabCatType.ConsoleOther, "Sharp X1"); + AddCategoryMapping("Sharp X68000", TorznabCatType.ConsoleOther, "Sharp X68000"); + AddCategoryMapping("SNK Neo Geo", TorznabCatType.ConsoleOther, "SNK Neo Geo"); + AddCategoryMapping("SNK Neo Geo Pocket", TorznabCatType.ConsoleOther, "SNK Neo Geo Pocket"); + AddCategoryMapping("Taito Type X", TorznabCatType.ConsoleOther, "Taito Type X"); + AddCategoryMapping("Tandy Color Computer", TorznabCatType.ConsoleOther, "Tandy Color Computer"); + AddCategoryMapping("Tangerine Oric", TorznabCatType.ConsoleOther, "Tangerine Oric"); + AddCategoryMapping("Thomson MO5", TorznabCatType.ConsoleOther, "Thomson MO5"); + AddCategoryMapping("Watara Supervision", TorznabCatType.ConsoleOther, "Watara Supervision"); + AddCategoryMapping("Retro - Other", TorznabCatType.ConsoleOther, "Retro - Other"); + + // special categories (real categories/not platforms) + AddCategoryMapping("OST", TorznabCatType.AudioOther, "OST"); + AddCategoryMapping("Applications", TorznabCatType.PC0day, "Applications"); + AddCategoryMapping("E-Books", TorznabCatType.BooksEBook, "E-Books"); + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + var searchString = GetSearchTerm(query); + + var searchUrl = APIUrl; + var queryCollection = new NameValueCollection + { + { "request", "search" }, + { "search_type", "torrents" }, + //{"group_results", "0"}, # results won't include all information + { "order_by", "time" }, + { "order_way", "desc" } + }; + + if (!string.IsNullOrWhiteSpace(searchString)) + queryCollection.Add("searchstr", searchString); + + var i = 0; + foreach (var cat in MapTorznabCapsToTrackers(query)) + queryCollection.Add($"artistcheck[{i++}]", cat); + + // remove . as not used in titles + searchUrl += "?" + queryCollection.GetQueryString().Replace(".", " "); + + var apiKey = ((ConfigurationDataGazelleTracker)configData).ApiKey; + var headers = apiKey != null ? new Dictionary { [AuthorizationName] = String.Format(AuthorizationFormat, 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 && !useApiKey) + { + // re-login only if API key is not in use. + await ApplyConfiguration(null); + response = await RequestWithCookiesAndRetryAsync(searchUrl); + } + else if (response.ContentString != null && response.ContentString.Contains("failure") && useApiKey) + { + // reason for failure should be explained. + var jsonError = JObject.Parse(response.ContentString); + var errorReason = (string)jsonError["error"]; + throw new Exception(errorReason); + } + + + try + { + var json = JObject.Parse(response.ContentString); + foreach (var gObj in JObject.FromObject(json["response"])) + { + var group = gObj.Value as JObject; + + foreach (var tObj in JObject.FromObject(group["Torrents"])) + { + var torrent = tObj.Value as JObject; + var torrentId = torrent["ID"].ToString(); + + var Category = "Windows"; + if (((JArray)group["Artists"]).Count > 0) + Category = group["Artists"][0]["name"].ToString(); + var GroupCategory = MapTrackerCatToNewznab(Category); + + var publishDate = DateTime.SpecifyKind( + DateTime.ParseExact(torrent["Time"].ToString(), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), + DateTimeKind.Unspecified).ToLocalTime(); + + var size = ParseUtil.CoerceLong(torrent["Size"].ToString()); + var details = new Uri(DetailsUrl + torrentId); + var link = new Uri(DownloadUrl + torrentId); + var grabs = ParseUtil.CoerceLong(torrent["Snatched"].ToString()); + var seeders = ParseUtil.CoerceLong(torrent["Seeders"].ToString()); + var leechers = ParseUtil.CoerceLong(torrent["Leechers"].ToString()); + var title = torrent["ReleaseTitle"].ToString(); + + List tags = new List(); + string[] tagNames = { "Format", "Encoding", "Region", "Language", "Scene", "Miscellaneous", "GameDOXType", "GameDOXVers" }; + foreach (var tag in tagNames) + { + string tagValue; + if (tag.Equals("Scene")) + tagValue = torrent[tag].ToString().Equals("1") ? "Scene" : ""; + else + tagValue = torrent[tag].ToString(); + + if (!string.IsNullOrEmpty(tagValue)) + tags.Add(tagValue); + } + + if (tags.Count > 0) + title += " [" + string.Join(", ", tags) + "]"; + + var freeTorrent = ParseUtil.CoerceInt(torrent["FreeTorrent"].ToString()); + var files = ParseUtil.CoerceInt(torrent["FileCount"].ToString()); + + var release = new ReleaseInfo + { + MinimumRatio = 1, + MinimumSeedTime = 288000, //80 hours + Category = GroupCategory, + PublishDate = publishDate, + Size = size, + Details = details, + Link = link, + Guid = link, + Grabs = grabs, + Seeders = seeders, + Peers = leechers + seeders, + Title = title, + Files = files, + UploadVolumeFactor = freeTorrent >= 2 ? 0 : 1, + DownloadVolumeFactor = freeTorrent >= 1 ? 0 : 1 + }; + releases.Add(release); + } + } + } + catch (Exception ex) + { + OnParseError(response.ContentString, ex); + } + + return releases; + } + } +} diff --git a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs index 5e0b6273b..6fe926b10 100644 --- a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs +++ b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataGazelleTracker.cs @@ -9,13 +9,14 @@ namespace Jackett.Common.Models.IndexerConfig.Bespoke public PasswordConfigurationItem Password { get; private set; } public StringConfigurationItem ApiKey { get; private set; } public StringConfigurationItem PassKey { get; private set; } + public StringConfigurationItem AuthKey { get; private set; } public DisplayInfoConfigurationItem CookieHint { get; private set; } public StringConfigurationItem CookieItem { get; private set; } public BoolConfigurationItem UseTokenItem { get; private set; } public DisplayInfoConfigurationItem Instructions { get; private set; } public ConfigurationDataGazelleTracker(bool has2Fa = false, bool supportsFreeleechToken = false, - bool useApiKey = false, bool usePassKey = false, + bool useApiKey = false, bool usePassKey = false, bool useAuthKey = false, string instructionMessageOptional = null) { if (useApiKey) @@ -44,6 +45,9 @@ namespace Jackett.Common.Models.IndexerConfig.Bespoke if (usePassKey) PassKey = new StringConfigurationItem("Passkey"); + if (useAuthKey) + AuthKey = new StringConfigurationItem("Authkey"); + if (supportsFreeleechToken) UseTokenItem = new BoolConfigurationItem("Use Freeleech Tokens when Available") { Value = false };