Jackett/src/Jackett.Common/Indexers/BaseIndexer.cs

778 lines
32 KiB
C#
Raw Normal View History

2020-02-09 18:08:34 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
2020-02-09 02:35:16 +00:00
using AutoMapper;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Jackett.Common.Utils.Clients;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
2018-06-26 15:47:19 +00:00
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
namespace Jackett.Common.Indexers
{
public abstract class BaseIndexer : IIndexer
{
public string Id { get; protected set; }
public string SiteLink { get; protected set; }
public virtual string[] LegacySiteLinks { get; protected set; }
public string DefaultSiteLink { get; protected set; }
2020-10-19 21:19:10 +00:00
public virtual string[] AlternativeSiteLinks { get; protected set; } = { };
public string DisplayDescription { get; protected set; }
public string DisplayName { get; protected set; }
public string Language { get; protected set; }
public string Type { get; protected set; }
2018-06-15 09:12:03 +00:00
[JsonConverter(typeof(EncodingJsonConverter))]
public Encoding Encoding { get; protected set; }
public virtual bool IsConfigured { get; protected set; }
protected Logger logger;
protected IIndexerConfigurationService configurationService;
protected IProtectionService protectionService;
protected ConfigurationData configData;
protected string CookieHeader
{
get => configData.CookieHeader.Value;
set => configData.CookieHeader.Value = value;
}
public string LastError
{
get => configData.LastError.Value;
set
{
var SaveNeeded = configData.LastError.Value != value && IsConfigured;
configData.LastError.Value = value;
if (SaveNeeded)
SaveConfig();
}
}
public abstract TorznabCapabilities TorznabCaps { get; protected set; }
// standard constructor used by most indexers
public BaseIndexer(string link, string id, string name, string description,
IIndexerConfigurationService configService, Logger logger, ConfigurationData configData,
IProtectionService p)
{
this.logger = logger;
configurationService = configService;
protectionService = p;
if (!link.EndsWith("/", StringComparison.Ordinal))
throw new Exception("Site link must end with a slash.");
Id = id;
DisplayName = name;
DisplayDescription = description;
SiteLink = link;
DefaultSiteLink = link;
this.configData = configData;
if (configData != null)
LoadValuesFromJson(null);
}
public virtual Task<ConfigurationData> GetConfigurationForSetup() => Task.FromResult<ConfigurationData>(configData);
public virtual void ResetBaseConfig()
{
CookieHeader = string.Empty;
IsConfigured = false;
}
public virtual void SaveConfig() => configurationService.Save(this as IIndexer, configData.ToJson(protectionService, forDisplay: false));
protected void LoadLegacyCookieConfig(JToken jsonConfig)
{
var legacyCookieHeader = (string)jsonConfig["cookie_header"];
if (!string.IsNullOrEmpty(legacyCookieHeader))
{
CookieHeader = legacyCookieHeader;
}
else
{
// Legacy cookie key
var jcookies = jsonConfig["cookies"];
if (jcookies is JArray)
{
var array = (JArray)jcookies;
legacyCookieHeader = string.Empty;
for (var i = 0; i < array.Count; i++)
{
if (i != 0)
legacyCookieHeader += "; ";
legacyCookieHeader += array[i];
}
CookieHeader = legacyCookieHeader;
}
else if (jcookies != null)
{
CookieHeader = (string)jcookies;
}
}
}
public virtual void LoadValuesFromJson(JToken jsonConfig, bool useProtectionService = false)
{
IProtectionService ps = null;
if (useProtectionService)
ps = protectionService;
configData.LoadValuesFromJson(jsonConfig, ps);
if (string.IsNullOrWhiteSpace(configData.SiteLink.Value))
{
configData.SiteLink.Value = DefaultSiteLink;
}
if (!configData.SiteLink.Value.EndsWith("/", StringComparison.Ordinal))
configData.SiteLink.Value += "/";
// reset site link to default if it's a legacy (defunc link)
if (LegacySiteLinks != null && LegacySiteLinks.Contains(configData.SiteLink.Value))
{
logger.Debug(string.Format("changing legacy site link from {0} to {1}", configData.SiteLink.Value, DefaultSiteLink));
configData.SiteLink.Value = DefaultSiteLink;
}
2017-07-19 17:24:03 +00:00
// check whether the site link is well-formatted
var siteUri = new Uri(configData.SiteLink.Value);
SiteLink = configData.SiteLink.Value;
}
public void LoadFromSavedConfiguration(JToken jsonConfig)
{
if (jsonConfig is JArray)
{
if (!MigratedFromDPAPI(jsonConfig))
{
LoadValuesFromJson(jsonConfig, true);
IsConfigured = true;
}
}
// read and upgrade old settings file format
else if (jsonConfig is object)
{
LoadLegacyCookieConfig(jsonConfig);
SaveConfig();
IsConfigured = true;
}
}
//TODO: Remove this section once users have moved off DPAPI
private bool MigratedFromDPAPI(JToken jsonConfig)
{
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
if (!isWindows && DotNetCoreUtil.IsRunningOnDotNetCore)
{
2018-06-10 02:51:34 +00:00
// User isn't running Windows, but is running on .NET Core framework, no access to the DPAPI, so don't bother trying to migrate
return false;
}
LoadValuesFromJson(jsonConfig, false);
2018-06-26 15:47:19 +00:00
StringItem passwordPropertyValue = null;
var passwordValue = "";
try
{
2018-06-26 15:47:19 +00:00
// try dynamic items first (e.g. all cardigann indexers)
passwordPropertyValue = (StringItem)configData.GetDynamicByName("password");
if (passwordPropertyValue == null) // if there's no dynamic password try the static property
{
passwordPropertyValue = (StringItem)configData.GetType().GetProperty("Password").GetValue(configData, null);
// protection is based on the item.Name value (property name might be different, example: Abnormal), so check the Name again
if (!string.Equals(passwordPropertyValue.Name, "password", StringComparison.InvariantCultureIgnoreCase))
{
logger.Debug($"Skipping non default password property (unencrpyted password) for [{Id}] while attempting migration");
2018-06-26 15:47:19 +00:00
return false;
}
}
passwordValue = passwordPropertyValue.Value;
}
catch (Exception)
{
logger.Debug($"Unable to source password for [{Id}] while attempting migration, likely a tracker without a password setting");
return false;
}
if (!string.IsNullOrEmpty(passwordValue))
{
try
{
protectionService.UnProtect(passwordValue);
//Password successfully unprotected using Microsoft.AspNetCore.DataProtection, no further action needed as we've already converted the password previously
return false;
}
catch (Exception ex)
{
2018-06-10 02:51:34 +00:00
if (ex.Message != "The provided payload cannot be decrypted because it was not protected with this protection provider.")
{
logger.Info($"Password could not be unprotected using Microsoft.AspNetCore.DataProtection - {Id} : " + ex);
2018-06-10 02:51:34 +00:00
}
logger.Info($"Attempting legacy Unprotect - {Id} : ");
try
{
var unprotectedPassword = protectionService.LegacyUnProtect(passwordValue);
//Password successfully unprotected using Windows/Mono DPAPI
2018-06-26 15:47:19 +00:00
passwordPropertyValue.Value = unprotectedPassword;
SaveConfig();
IsConfigured = true;
logger.Info($"Password successfully migrated for {Id}");
2018-06-10 02:51:34 +00:00
return true;
}
catch (Exception exception)
{
logger.Info($"Password could not be unprotected using legacy DPAPI - {Id} : " + exception);
}
}
}
return false;
}
protected async Task ConfigureIfOK(string cookies, bool isLoggedin, Func<Task> onError)
{
if (isLoggedin)
{
CookieHeader = cookies;
IsConfigured = true;
SaveConfig();
}
else
{
await onError();
}
}
protected virtual IEnumerable<ReleaseInfo> FilterResults(TorznabQuery query, IEnumerable<ReleaseInfo> results)
{
if (query.Categories.Length == 0)
return results;
var filteredResults = results.Where(
result => result.Category?.Any() != true || query.Categories.Intersect(result.Category).Any() ||
TorznabCatType.QueryContainsParentCategory(query.Categories, result.Category));
return filteredResults;
}
2018-04-06 15:43:18 +00:00
public virtual bool CanHandleQuery(TorznabQuery query)
{
if (query == null)
return false;
2017-08-11 22:30:43 +00:00
if (query.QueryType == "caps")
return true;
var caps = TorznabCaps;
if (query.HasSpecifiedCategories)
if (!caps.SupportsCategories(query.Categories))
return false;
if (caps.TvSearchImdbAvailable && query.IsImdbQuery && query.IsTVSearch)
return true;
if (caps.MovieSearchImdbAvailable && query.IsImdbQuery && query.IsMovieSearch)
return true;
else if (!caps.MovieSearchImdbAvailable && query.IsImdbQuery && query.QueryType != "TorrentPotato") // potato query should always contain imdb+search term
return false;
2017-08-11 16:13:22 +00:00
if (caps.SearchAvailable && query.IsSearch)
return true;
if (caps.TvSearchAvailable && query.IsTVSearch)
2017-08-11 16:13:22 +00:00
return true;
if (caps.MovieSearchAvailable && query.IsMovieSearch)
return true;
2017-10-18 16:30:41 +00:00
if (caps.MusicSearchAvailable && query.IsMusicSearch)
return true;
2020-08-16 21:44:12 +00:00
if (caps.BookSearchAvailable && query.IsBookSearch)
return true;
if (caps.TvSearchTvRageAvailable && query.IsTVRageSearch)
2017-08-11 16:13:22 +00:00
return true;
if (caps.TvSearchTvdbAvailable && query.IsTvdbSearch)
return true;
if (caps.MovieSearchImdbAvailable && query.IsImdbQuery)
2017-08-11 16:13:22 +00:00
return true;
if (caps.MovieSearchTmdbAvailable && query.IsTmdbQuery)
return true;
2017-08-11 16:13:22 +00:00
return false;
}
public void Unconfigure()
{
IsConfigured = false;
2017-11-13 11:55:54 +00:00
SiteLink = DefaultSiteLink;
2017-11-13 15:55:02 +00:00
CookieHeader = ""; // clear cookies
}
public abstract Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson);
public virtual async Task<IndexerResult> ResultsForQuery(TorznabQuery query)
{
try
{
if (!CanHandleQuery(query))
return new IndexerResult(this, new ReleaseInfo[0]);
var results = await PerformQuery(query);
results = FilterResults(query, results);
2018-05-14 14:48:57 +00:00
if (query.Limit > 0)
{
results = results.Take(query.Limit);
}
results = results.Select(r =>
{
r.Origin = this;
Feature/new api (#1584) * Introducing API v2 There were multiple inconsistencies in the old API and I have been toying with the idea to replace it. This will suck for everyone who was building on top of the Jackett API, however as it was probably too painful to do so I'd say no one really tried. Now API v2.0 should be much more constistent as it uses DTObjects, and instead of manually constructing a json response it is handled by the ASP.NET web api. It is much more RESTful than it was, proper GET endpoints are introduced, and updating resources are now done via POST - it might be improved by introducing other type of REST methods. I know this sucks as completely breaks backward compatibility, however it'll probably make it easier to maintain and build on top of in the long run. * Use DELETE method to unconfigure an indexer * Remove debugging format from NLog * Fixing an null exception * Properly implementing IExceptionFilter interface * Enable adding public indexers without configuration * Fix missing manual search results * Basic modularization of the JS API * Introduce API versioning * Fix redirects to the dashboard * Cleaning up a little bit * Revamping Torznab and Potato as well * Move preconditions to FilterAttributes and simplify logic * Remove legacy filtering... will move to IResultFilter * Minor adjustment on results interface * Use Interpolated strings in ResultsController * DTO-ify * Remove fallback logic from potato results * DTO everywhere!!! * DTO-ify everything! * I hope this is my last piece of modification to this PR * Remove test variables... * Left out a couple conflicts... It's late
2017-08-08 15:02:16 +00:00
// Some trackers do not keep their clocks up to date and can be ~20 minutes out!
if (r.PublishDate > DateTime.Now)
r.PublishDate = DateTime.Now;
return r;
});
return new IndexerResult(this, results);
}
catch (Exception ex)
{
throw new IndexerException(this, ex);
}
}
protected abstract Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query);
}
public abstract class BaseWebIndexer : BaseIndexer, IWebIndexer
{
protected BaseWebIndexer(string link, string id, string name, string description,
IIndexerConfigurationService configService, WebClient client, Logger logger,
ConfigurationData configData, IProtectionService p, TorznabCapabilities caps,
string downloadBase = null)
: base(link, id, name, description, configService, logger, configData, p)
{
webclient = client;
downloadUrlBase = downloadBase;
TorznabCaps = caps;
}
// minimal constructor used by e.g. cardigann generic indexer
protected BaseWebIndexer(IIndexerConfigurationService configService, WebClient client, Logger logger, IProtectionService p)
: base("/", "", "", "", configService, logger, null, p) => webclient = client;
public virtual async Task<byte[]> Download(Uri link)
{
var uncleanLink = UncleanLink(link);
return await Download(uncleanLink, RequestType.GET);
}
protected async Task<byte[]> Download(Uri link, RequestType method, string referer = null, Dictionary<string, string>headers = null)
{
// return magnet link
if (link.Scheme == "magnet")
return Encoding.UTF8.GetBytes(link.OriginalString);
// do some extra escaping, needed for HD-Torrents
var requestLink = link.ToString()
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("'", "%27");
var response = await RequestWithCookiesAndRetryAsync(requestLink, null, method, referer, null, headers);
if (response.IsRedirect)
{
await FollowIfRedirect(response);
}
if (response.Status != System.Net.HttpStatusCode.OK && response.Status != System.Net.HttpStatusCode.Continue && response.Status != System.Net.HttpStatusCode.PartialContent)
{
logger.Error("Failed download cookies: " + CookieHeader);
if (response.ContentBytes != null)
logger.Error("Failed download response:\n" + Encoding.UTF8.GetString(response.ContentBytes));
throw new Exception($"Remote server returned {response.Status.ToString()}" + (response.IsRedirect ? " => " + response.RedirectingTo : ""));
}
return response.ContentBytes;
}
protected async Task<WebResult> RequestWithCookiesAndRetryAsync(
string url, string cookieOverride = null, RequestType method = RequestType.GET,
string referer = null, IEnumerable<KeyValuePair<string, string>> data = null,
Dictionary<string, string> headers = null, string rawbody = null, bool? emulateBrowser = null)
{
Exception lastException = null;
for (var i = 0; i < 3; i++)
{
try
{
return await RequestWithCookiesAsync(
url, cookieOverride, method, referer, data, headers, rawbody, emulateBrowser);
}
catch (Exception e)
{
logger.Error(
e, string.Format("On attempt {0} downloading from {1}: {2}", (i + 1), DisplayName, e.Message));
lastException = e;
}
await Task.Delay(500);
}
throw lastException;
}
protected virtual async Task<WebResult> RequestWithCookiesAsync(
string url, string cookieOverride = null, RequestType method = RequestType.GET,
string referer = null, IEnumerable<KeyValuePair<string, string>> data = null,
Dictionary<string, string> headers = null, string rawbody = null, bool? emulateBrowser = null)
{
var request = new WebRequest
{
Url = url,
Type = method,
Cookies = cookieOverride ?? CookieHeader,
PostData = data,
Referer = referer,
Headers = headers,
RawBody = rawbody,
Encoding = Encoding
};
if (emulateBrowser.HasValue)
request.EmulateBrowser = emulateBrowser.Value;
var result = await webclient.GetResultAsync(request);
CheckSiteDown(result);
UpdateCookieHeader(result.Cookies, cookieOverride);
return result;
}
protected async Task<WebResult> RequestLoginAndFollowRedirect(string url, IEnumerable<KeyValuePair<string, string>> data, string cookies, bool returnCookiesFromFirstCall, string redirectUrlOverride = null, string referer = null, bool accumulateCookies = false)
{
2020-10-19 21:19:10 +00:00
var request = new WebRequest
{
Url = url,
Type = RequestType.POST,
Cookies = cookies,
Referer = referer,
PostData = data,
Encoding = Encoding
};
var response = await webclient.GetResultAsync(request);
CheckSiteDown(response);
if (accumulateCookies)
{
response.Cookies = ResolveCookies((request.Cookies == null ? "" : request.Cookies + " ") + response.Cookies);
}
var firstCallCookies = response.Cookies;
if (response.IsRedirect)
{
await FollowIfRedirect(response, request.Url, redirectUrlOverride, response.Cookies, accumulateCookies);
}
if (returnCookiesFromFirstCall)
{
response.Cookies = ResolveCookies(firstCallCookies + (accumulateCookies ? " " + response.Cookies : ""));
}
return response;
}
protected static void CheckSiteDown(WebResult response)
{
if (response.Status == System.Net.HttpStatusCode.BadGateway
|| response.Status == System.Net.HttpStatusCode.GatewayTimeout
|| (int)response.Status == 521 // used by cloudflare to signal the original webserver is refusing the connection
|| (int)response.Status == 522 // used by cloudflare to signal the original webserver is not reachable at all (timeout)
2018-04-01 13:40:43 +00:00
|| (int)response.Status == 523 // used by cloudflare to signal the original webserver is not reachable at all (Origin is unreachable)
)
{
throw new Exception("Request to " + response.Request.Url + " failed (Error " + response.Status + ") - The tracker seems to be down.");
}
if (response.Status == System.Net.HttpStatusCode.Forbidden && response.ContentString.Contains("<span data-translate=\"complete_sec_check\">Please complete the security check to access</span>"))
{
throw new Exception("Request to " + response.Request.Url + " failed (Error " + response.Status + ") - The page is protected by an Cloudflare reCaptcha. The page is in aggressive DDoS mitigation mode or your IP might be blacklisted (e.g. in case of shared VPN IPs). There's no easy way of making it usable with Jackett.");
}
}
protected async Task FollowIfRedirect(WebResult response, string referrer = null, string overrideRedirectUrl = null, string overrideCookies = null, bool accumulateCookies = false)
{
// Follow up to 5 redirects
for (var i = 0; i < 5; i++)
{
if (!response.IsRedirect)
break;
await DoFollowIfRedirect(response, referrer, overrideRedirectUrl, overrideCookies, accumulateCookies);
if (accumulateCookies)
{
CookieHeader = ResolveCookies((CookieHeader != null && CookieHeader != "" ? CookieHeader + " " : "") + (overrideCookies != null && overrideCookies != "" ? overrideCookies + " " : "") + response.Cookies);
overrideCookies = response.Cookies = CookieHeader;
}
if (overrideCookies != null && response.Cookies == null)
{
response.Cookies = overrideCookies;
}
}
}
private string ResolveCookies(string incomingCookies = "")
{
var redirRequestCookies = string.IsNullOrWhiteSpace(CookieHeader) ? incomingCookies : CookieHeader + " " + incomingCookies;
var cookieDictionary = CookieUtil.CookieHeaderToDictionary(redirRequestCookies);
// These cookies are causing BadGateway errors, so we drop them, see issue #2306
cookieDictionary.Remove("cf_use_ob");
cookieDictionary.Remove("cf_ob_info");
return CookieUtil.CookieDictionaryToHeader(cookieDictionary);
}
// Update CookieHeader with new cookies and save the config if something changed (e.g. a new CloudFlare clearance cookie was issued)
2017-10-03 12:23:31 +00:00
protected virtual void UpdateCookieHeader(string newCookies, string cookieOverride = null)
{
var newCookieHeader = ResolveCookies((cookieOverride != null && cookieOverride != "" ? cookieOverride + " " : "") + newCookies);
if (CookieHeader != newCookieHeader)
{
logger.Debug(string.Format("updating Cookies {0} => {1}", CookieHeader, newCookieHeader));
CookieHeader = newCookieHeader;
if (IsConfigured)
SaveConfig();
}
}
private async Task DoFollowIfRedirect(WebResult incomingResponse, string referrer = null, string overrideRedirectUrl = null, string overrideCookies = null, bool accumulateCookies = false)
{
if (incomingResponse.IsRedirect)
{
var redirRequestCookies = "";
if (accumulateCookies)
{
redirRequestCookies = ResolveCookies((CookieHeader != "" ? CookieHeader + " " : "") + (overrideCookies != null ? overrideCookies : ""));
}
else
{
redirRequestCookies = (overrideCookies != null ? overrideCookies : "");
}
// Do redirect
2020-10-19 21:19:10 +00:00
var redirectedResponse = await webclient.GetResultAsync(new WebRequest
{
Url = overrideRedirectUrl ?? incomingResponse.RedirectingTo,
Referer = referrer,
Cookies = redirRequestCookies,
Encoding = Encoding
});
Mapper.Map(redirectedResponse, incomingResponse);
}
}
protected List<string> GetAllTrackerCategories() => categoryMapping.Select(x => x.TrackerCategory).ToList();
protected void AddCategoryMapping(string trackerCategory, TorznabCategory newznabCategory, string trackerCategoryDesc = null)
{
categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, newznabCategory.ID));
if (!TorznabCaps.Categories.Contains(newznabCategory))
TorznabCaps.Categories.Add(newznabCategory);
// add 1:1 categories
if (trackerCategoryDesc != null && trackerCategory != null)
{
//TODO convert to int.TryParse() to avoid using throw as flow control
try
{
var trackerCategoryInt = int.Parse(trackerCategory);
var CustomCat = new TorznabCategory(trackerCategoryInt + 100000, trackerCategoryDesc);
if (!TorznabCaps.Categories.Contains(CustomCat))
TorznabCaps.Categories.Add(CustomCat);
}
catch (FormatException)
{
// trackerCategory is not an integer, continue
}
}
}
protected void AddCategoryMapping(int trackerCategory, TorznabCategory newznabCategory, string trackerCategoryDesc = null) => AddCategoryMapping(trackerCategory.ToString(), newznabCategory, trackerCategoryDesc);
protected void AddMultiCategoryMapping(TorznabCategory newznabCategory, params int[] trackerCategories)
{
foreach (var trackerCat in trackerCategories)
{
AddCategoryMapping(trackerCat, newznabCategory);
}
}
protected virtual List<string> MapTorznabCapsToTrackers(TorznabQuery query, bool mapChildrenCatsToParent = false)
{
var result = new List<string>();
foreach (var cat in query.Categories)
{
// use 1:1 mapping to tracker categories for newznab categories >= 100000
if (cat >= 100000)
{
result.Add((cat - 100000).ToString());
continue;
}
var queryCats = new List<int> { cat };
var newznabCat = TorznabCatType.AllCats.FirstOrDefault(c => c.ID == cat);
if (newznabCat != null)
{
queryCats.AddRange(newznabCat.SubCategories.Select(c => c.ID));
}
if (mapChildrenCatsToParent)
{
var parentNewznabCat = TorznabCatType.AllCats.FirstOrDefault(c => c.SubCategories.Contains(newznabCat));
if (parentNewznabCat != null)
{
queryCats.Add(parentNewznabCat.ID);
}
}
foreach (var mapping in categoryMapping.Where(c => queryCats.Contains(c.NewzNabCategory)))
{
result.Add(mapping.TrackerCategory);
}
}
return result.Distinct().ToList();
}
protected ICollection<int> MapTrackerCatToNewznab(string input)
{
2018-07-30 12:25:35 +00:00
if (input == null)
return new List<int>();
2018-07-30 12:25:35 +00:00
var cats = categoryMapping.Where(m => m.TrackerCategory != null && m.TrackerCategory.ToLowerInvariant() == input.ToLowerInvariant()).Select(c => c.NewzNabCategory).ToList();
// 1:1 category mapping
try
{
var trackerCategoryInt = int.Parse(input);
cats.Add(trackerCategoryInt + 100000);
}
catch (FormatException)
{
// input is not an integer, continue
}
2018-07-30 12:25:35 +00:00
return cats;
}
protected ICollection<int> MapTrackerCatDescToNewznab(string input)
{
var cats = new List<int>();
if (null != input)
{
var mapping = categoryMapping.Where(m => m.TrackerCategoryDesc != null && m.TrackerCategoryDesc.ToLowerInvariant() == input.ToLowerInvariant()).FirstOrDefault();
if (mapping != null)
{
cats.Add(mapping.NewzNabCategory);
if (mapping.TrackerCategory != null)
{
// 1:1 category mapping
try
{
var trackerCategoryInt = int.Parse(mapping.TrackerCategory);
cats.Add(trackerCategoryInt + 100000);
}
catch (FormatException)
{
// mapping.TrackerCategory is not an integer, continue
}
}
}
}
return cats;
}
private IEnumerable<ReleaseInfo> CleanLinks(IEnumerable<ReleaseInfo> releases)
{
if (string.IsNullOrEmpty(downloadUrlBase))
return releases;
foreach (var release in releases)
{
if (release.Link.ToString().StartsWith(downloadUrlBase, StringComparison.Ordinal))
{
release.Link = new Uri(release.Link.ToString().Substring(downloadUrlBase.Length), UriKind.Relative);
}
}
return releases;
}
public override async Task<IndexerResult> ResultsForQuery(TorznabQuery query)
{
var result = await base.ResultsForQuery(query);
result.Releases = CleanLinks(result.Releases);
return result;
}
protected virtual Uri UncleanLink(Uri link)
{
if (string.IsNullOrWhiteSpace(downloadUrlBase))
{
return link;
}
if (link.ToString().StartsWith(downloadUrlBase, StringComparison.Ordinal))
{
return link;
}
return new Uri(downloadUrlBase + link.ToString(), UriKind.RelativeOrAbsolute);
}
protected void OnParseError(string results, Exception ex)
{
var fileName = string.Format("Error on {0} for {1}.txt", DateTime.Now.ToString("yyyyMMddHHmmss"), DisplayName);
var spacing = string.Join("", Enumerable.Repeat(Environment.NewLine, 5));
var fileContents = string.Format("{0}{1}{2}", ex, spacing, results);
logger.Error(fileName + fileContents);
throw new Exception("Parse error", ex);
}
public override TorznabCapabilities TorznabCaps { get; protected set; }
private readonly List<CategoryMapping> categoryMapping = new List<CategoryMapping>();
protected WebClient webclient;
protected readonly string downloadUrlBase = "";
}
public abstract class BaseCachingWebIndexer : BaseWebIndexer
{
protected BaseCachingWebIndexer(string link,string id, string name, string description,
IIndexerConfigurationService configService, WebClient client, Logger logger,
ConfigurationData configData, IProtectionService p, TorznabCapabilities caps = null,
string downloadBase = null)
: base(link, id, name, description, configService, client, logger, configData, p, caps, downloadBase)
{
}
protected void CleanCache()
{
foreach (var expired in cache.Where(i => DateTime.Now - i.Created > cacheTime).ToList())
{
cache.Remove(expired);
}
}
protected static List<CachedQueryResult> cache = new List<CachedQueryResult>();
protected static readonly TimeSpan cacheTime = new TimeSpan(0, 9, 0);
}
}