mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-04-24 13:57:11 -04:00
New: Support JSON parsing in Cardigann
Co-Authored-By: mikeoscar2006 <89641725+mikeoscar2006@users.noreply.github.com>
This commit is contained in:
parent
93deb56e8e
commit
2c0c6aa158
6 changed files with 531 additions and 351 deletions
|
@ -26,7 +26,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||||
{
|
{
|
||||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||||
|
|
||||||
private const int DEFINITION_VERSION = 2;
|
private const int DEFINITION_VERSION = 3;
|
||||||
private readonly List<string> _defintionBlocklist = new List<string>()
|
private readonly List<string> _defintionBlocklist = new List<string>()
|
||||||
{
|
{
|
||||||
"aither",
|
"aither",
|
||||||
|
|
|
@ -141,7 +141,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
return element.QuerySelector(selector);
|
return element.QuerySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string HandleSelector(SelectorBlock selector, IElement dom, Dictionary<string, object> variables = null)
|
protected string HandleSelector(SelectorBlock selector, IElement dom, Dictionary<string, object> variables = null, bool required = true)
|
||||||
{
|
{
|
||||||
if (selector.Text != null)
|
if (selector.Text != null)
|
||||||
{
|
{
|
||||||
|
@ -164,7 +164,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
|
|
||||||
if (selection == null)
|
if (selection == null)
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector.Selector, dom.ToHtmlPretty()));
|
if (required)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector.Selector, dom.ToHtmlPretty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +194,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
|
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("None of the case selectors \"{0}\" matched {1}", string.Join(",", selector.Case), selection.ToHtmlPretty()));
|
if (required)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("None of the case selectors \"{0}\" matched {1}", string.Join(",", selector.Case), selection.ToHtmlPretty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (selector.Attribute != null)
|
else if (selector.Attribute != null)
|
||||||
|
@ -197,7 +207,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
value = selection.GetAttribute(selector.Attribute);
|
value = selection.GetAttribute(selector.Attribute);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("Attribute \"{0}\" is not set for element {1}", selector.Attribute, selection.ToHtmlPretty()));
|
if (required)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("Attribute \"{0}\" is not set for element {1}", selector.Attribute, selection.ToHtmlPretty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -208,6 +223,57 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
return ApplyFilters(ParseUtil.NormalizeSpace(value), selector.Filters, variables);
|
return ApplyFilters(ParseUtil.NormalizeSpace(value), selector.Filters, variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected string HandleJsonSelector(SelectorBlock selector, JToken parentObj, Dictionary<string, object> variables = null, bool required = true)
|
||||||
|
{
|
||||||
|
if (selector.Text != null)
|
||||||
|
{
|
||||||
|
return ApplyFilters(ApplyGoTemplateText(selector.Text, variables), selector.Filters, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
string value = null;
|
||||||
|
|
||||||
|
if (selector.Selector != null)
|
||||||
|
{
|
||||||
|
var selector_Selector = ApplyGoTemplateText(selector.Selector.TrimStart('.'), variables);
|
||||||
|
var selection = parentObj.SelectToken(selector_Selector);
|
||||||
|
if (selection == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector_Selector, parentObj.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = selection.Value<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector.Case != null)
|
||||||
|
{
|
||||||
|
foreach (var jcase in selector.Case)
|
||||||
|
{
|
||||||
|
if (value.Equals(jcase.Key) || jcase.Key.Equals("*"))
|
||||||
|
{
|
||||||
|
value = jcase.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("None of the case selectors \"{0}\" matched {1}", string.Join(",", selector.Case), parentObj.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApplyFilters(ParseUtil.NormalizeSpace(value), selector.Filters, variables);
|
||||||
|
}
|
||||||
|
|
||||||
protected Dictionary<string, object> GetBaseTemplateVariables()
|
protected Dictionary<string, object> GetBaseTemplateVariables()
|
||||||
{
|
{
|
||||||
var indexerLogging = _configService.LogIndexerResponse;
|
var indexerLogging = _configService.LogIndexerResponse;
|
||||||
|
|
|
@ -150,13 +150,15 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
{
|
{
|
||||||
public int After { get; set; }
|
public int After { get; set; }
|
||||||
public SelectorBlock Dateheaders { get; set; }
|
public SelectorBlock Dateheaders { get; set; }
|
||||||
|
public SelectorBlock Count { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchPathBlock : RequestBlock
|
public class SearchPathBlock : RequestBlock
|
||||||
{
|
{
|
||||||
public List<string> Categories { get; set; }
|
public List<string> Categories { get; set; }
|
||||||
public bool Inheritinputs { get; set; } = true;
|
public bool Inheritinputs { get; set; } = true;
|
||||||
public bool Followredirect { get; set; } = false;
|
public bool Followredirect { get; set; }
|
||||||
|
public ResponseBlock Response { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RequestBlock
|
public class RequestBlock
|
||||||
|
@ -194,4 +196,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
{
|
{
|
||||||
public SelectorField Pathselector { get; set; }
|
public SelectorField Pathselector { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ResponseBlock
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Attribute { get; set; }
|
||||||
|
public bool Multiple { get; set; }
|
||||||
|
public string NoResultsMessage { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Net;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using AngleSharp.Html.Parser;
|
using AngleSharp.Html.Parser;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
@ -55,59 +56,46 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
|
|
||||||
var searchUrlUri = new Uri(request.Url.FullUri);
|
var searchUrlUri = new Uri(request.Url.FullUri);
|
||||||
|
|
||||||
try
|
if (request.SearchPath.Response != null && request.SearchPath.Response.Type.Equals("json"))
|
||||||
{
|
{
|
||||||
var searchResultParser = new HtmlParser();
|
if (request.SearchPath.Response != null && request.SearchPath.Response.NoResultsMessage != null && (request.SearchPath.Response.NoResultsMessage.Equals(results) || (request.SearchPath.Response.NoResultsMessage == string.Empty && results == string.Empty)))
|
||||||
var searchResultDocument = searchResultParser.ParseDocument(results);
|
|
||||||
|
|
||||||
/* checkForError(response, Definition.Search.Error); */
|
|
||||||
|
|
||||||
if (search.Preprocessingfilters != null)
|
|
||||||
{
|
{
|
||||||
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
return releases;
|
||||||
searchResultDocument = searchResultParser.ParseDocument(results);
|
|
||||||
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
var parsedJson = JToken.Parse(results);
|
||||||
var rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
if (parsedJson == null)
|
||||||
var rows = new List<IElement>();
|
|
||||||
foreach (var rowDom in rowsDom)
|
|
||||||
{
|
{
|
||||||
rows.Add(rowDom);
|
throw new Exception("Error Parsing Json Response");
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge following rows for After selector
|
if (search.Rows.Count != null)
|
||||||
var after = search.Rows.After;
|
|
||||||
if (after > 0)
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < rows.Count; i += 1)
|
var countVal = HandleJsonSelector(search.Rows.Count, parsedJson, variables);
|
||||||
|
if (int.TryParse(countVal, out var count))
|
||||||
{
|
{
|
||||||
var currentRow = rows[i];
|
if (count < 1)
|
||||||
for (var j = 0; j < after; j += 1)
|
|
||||||
{
|
{
|
||||||
var mergeRowIndex = i + j + 1;
|
return releases;
|
||||||
var mergeRow = rows[mergeRowIndex];
|
|
||||||
var mergeNodes = new List<INode>();
|
|
||||||
foreach (var node in mergeRow.ChildNodes)
|
|
||||||
{
|
|
||||||
mergeNodes.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRow.Append(mergeNodes.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.RemoveRange(i + 1, after);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var row in rows)
|
var rowsObj = parsedJson.SelectToken(search.Rows.Selector);
|
||||||
|
if (rowsObj == null)
|
||||||
{
|
{
|
||||||
try
|
throw new Exception("Error Parsing Rows Selector");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var row in rowsObj.Value<JArray>())
|
||||||
|
{
|
||||||
|
var selObj = request.SearchPath.Response.Attribute != null ? row.SelectToken(request.SearchPath.Response.Attribute).Value<JToken>() : row;
|
||||||
|
var mulRows = request.SearchPath.Response.Multiple == true ? selObj.Values<JObject>() : new List<JObject> { selObj.Value<JObject>() };
|
||||||
|
|
||||||
|
foreach (var mulRow in mulRows)
|
||||||
{
|
{
|
||||||
var release = new TorrentInfo();
|
var release = new TorrentInfo();
|
||||||
|
|
||||||
// Parse fields
|
|
||||||
foreach (var field in search.Fields)
|
foreach (var field in search.Fields)
|
||||||
{
|
{
|
||||||
var fieldParts = field.Key.Split('|');
|
var fieldParts = field.Key.Split('|');
|
||||||
|
@ -120,200 +108,23 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
|
|
||||||
string value = null;
|
string value = null;
|
||||||
var variablesKey = ".Result." + fieldName;
|
var variablesKey = ".Result." + fieldName;
|
||||||
|
var isOptional = OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
value = HandleSelector(field.Value, row, variables);
|
var parentObj = mulRow;
|
||||||
switch (fieldName)
|
if (field.Value.Selector != null && field.Value.Selector.StartsWith(".."))
|
||||||
{
|
{
|
||||||
case "download":
|
parentObj = row.Value<JObject>();
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
value = null;
|
|
||||||
release.DownloadUrl = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.StartsWith("magnet:"))
|
|
||||||
{
|
|
||||||
release.MagnetUrl = value;
|
|
||||||
value = release.MagnetUrl;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.DownloadUrl = ResolvePath(value, searchUrlUri).AbsoluteUri;
|
|
||||||
value = release.DownloadUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "magnet":
|
|
||||||
var magnetUri = value;
|
|
||||||
release.MagnetUrl = magnetUri;
|
|
||||||
value = magnetUri.ToString();
|
|
||||||
break;
|
|
||||||
case "infohash":
|
|
||||||
release.InfoHash = value;
|
|
||||||
break;
|
|
||||||
case "details":
|
|
||||||
var url = ResolvePath(value, searchUrlUri)?.AbsoluteUri;
|
|
||||||
release.InfoUrl = url;
|
|
||||||
release.Guid = url;
|
|
||||||
value = url.ToString();
|
|
||||||
break;
|
|
||||||
case "comments":
|
|
||||||
var commentsUrl = ResolvePath(value, searchUrlUri);
|
|
||||||
if (release.CommentUrl == null)
|
|
||||||
{
|
|
||||||
release.CommentUrl = commentsUrl.AbsoluteUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = commentsUrl.ToString();
|
|
||||||
break;
|
|
||||||
case "title":
|
|
||||||
if (fieldModifiers.Contains("append"))
|
|
||||||
{
|
|
||||||
release.Title += value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.Title = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = release.Title;
|
|
||||||
break;
|
|
||||||
case "description":
|
|
||||||
if (fieldModifiers.Contains("append"))
|
|
||||||
{
|
|
||||||
release.Description += value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.Description = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = release.Description;
|
|
||||||
break;
|
|
||||||
case "category":
|
|
||||||
var cats = MapTrackerCatToNewznab(value);
|
|
||||||
if (cats.Any())
|
|
||||||
{
|
|
||||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
|
||||||
{
|
|
||||||
release.Categories = cats;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.Categories = release.Categories.Union(cats).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = release.Categories.ToString();
|
|
||||||
break;
|
|
||||||
case "size":
|
|
||||||
release.Size = ParseUtil.GetBytes(value);
|
|
||||||
value = release.Size.ToString();
|
|
||||||
break;
|
|
||||||
case "leechers":
|
|
||||||
var leechers = ParseUtil.CoerceLong(value);
|
|
||||||
leechers = leechers < 5000000L ? leechers : 0; // to fix #6558
|
|
||||||
if (release.Peers == null)
|
|
||||||
{
|
|
||||||
release.Peers = (int)leechers;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.Peers += (int)leechers;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = leechers.ToString();
|
|
||||||
break;
|
|
||||||
case "seeders":
|
|
||||||
release.Seeders = ParseUtil.CoerceInt(value);
|
|
||||||
release.Seeders = release.Seeders < 5000000L ? release.Seeders : 0; // to fix #6558
|
|
||||||
if (release.Peers == null)
|
|
||||||
{
|
|
||||||
release.Peers = release.Seeders;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
release.Peers += release.Seeders;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = release.Seeders.ToString();
|
|
||||||
break;
|
|
||||||
case "date":
|
|
||||||
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
|
||||||
value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern);
|
|
||||||
break;
|
|
||||||
case "files":
|
|
||||||
release.Files = ParseUtil.CoerceInt(value);
|
|
||||||
value = release.Files.ToString();
|
|
||||||
break;
|
|
||||||
case "grabs":
|
|
||||||
release.Grabs = ParseUtil.CoerceInt(value);
|
|
||||||
value = release.Grabs.ToString();
|
|
||||||
break;
|
|
||||||
case "downloadvolumefactor":
|
|
||||||
release.DownloadVolumeFactor = ParseUtil.CoerceDouble(value);
|
|
||||||
value = release.DownloadVolumeFactor.ToString();
|
|
||||||
break;
|
|
||||||
case "uploadvolumefactor":
|
|
||||||
release.UploadVolumeFactor = ParseUtil.CoerceDouble(value);
|
|
||||||
value = release.UploadVolumeFactor.ToString();
|
|
||||||
break;
|
|
||||||
case "minimumratio":
|
|
||||||
release.MinimumRatio = ParseUtil.CoerceDouble(value);
|
|
||||||
value = release.MinimumRatio.ToString();
|
|
||||||
break;
|
|
||||||
case "minimumseedtime":
|
|
||||||
release.MinimumSeedTime = ParseUtil.CoerceLong(value);
|
|
||||||
value = release.MinimumSeedTime.ToString();
|
|
||||||
break;
|
|
||||||
case "imdb":
|
|
||||||
release.ImdbId = (int)ParseUtil.GetLongFromString(value);
|
|
||||||
value = release.ImdbId.ToString();
|
|
||||||
break;
|
|
||||||
case "tmdbid":
|
|
||||||
var tmdbIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
|
||||||
var tmdbIDMatch = tmdbIDRegEx.Match(value);
|
|
||||||
var tmdbID = tmdbIDMatch.Groups[1].Value;
|
|
||||||
release.TmdbId = (int)ParseUtil.CoerceLong(tmdbID);
|
|
||||||
value = release.TmdbId.ToString();
|
|
||||||
break;
|
|
||||||
case "rageid":
|
|
||||||
var rageIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
|
||||||
var rageIDMatch = rageIDRegEx.Match(value);
|
|
||||||
var rageID = rageIDMatch.Groups[1].Value;
|
|
||||||
release.TvRageId = (int)ParseUtil.CoerceLong(rageID);
|
|
||||||
value = release.TvRageId.ToString();
|
|
||||||
break;
|
|
||||||
case "tvdbid":
|
|
||||||
var tvdbIdRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
|
||||||
var tvdbIdMatch = tvdbIdRegEx.Match(value);
|
|
||||||
var tvdbId = tvdbIdMatch.Groups[1].Value;
|
|
||||||
release.TvdbId = (int)ParseUtil.CoerceLong(tvdbId);
|
|
||||||
value = release.TvdbId.ToString();
|
|
||||||
break;
|
|
||||||
case "poster":
|
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
var poster = ResolvePath(value, searchUrlUri);
|
|
||||||
release.PosterUrl = poster.AbsoluteUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = release.PosterUrl;
|
|
||||||
break;
|
|
||||||
|
|
||||||
//case "author":
|
|
||||||
// release.Author = value;
|
|
||||||
// break;
|
|
||||||
//case "booktitle":
|
|
||||||
// release.BookTitle = value;
|
|
||||||
// break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variables[variablesKey] = value;
|
value = HandleJsonSelector(field.Value, parentObj, variables, !isOptional);
|
||||||
|
if (isOptional && string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
variables[variablesKey] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
variables[variablesKey] = ParseFields(value, fieldName, release, fieldModifiers, searchUrlUri);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -322,141 +133,202 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
variables[variablesKey] = null;
|
variables[variablesKey] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional)
|
if (isOptional)
|
||||||
{
|
{
|
||||||
variables[variablesKey] = null;
|
variables[variablesKey] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexerLogging)
|
throw new Exception(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value ?? "<null>", ex.Message));
|
||||||
{
|
|
||||||
_logger.Trace("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters = search.Rows.Filters;
|
|
||||||
var skipRelease = false;
|
|
||||||
if (filters != null)
|
|
||||||
{
|
|
||||||
foreach (var filter in filters)
|
|
||||||
{
|
|
||||||
switch (filter.Name)
|
|
||||||
{
|
|
||||||
case "andmatch":
|
|
||||||
var characterLimit = -1;
|
|
||||||
if (filter.Args != null)
|
|
||||||
{
|
|
||||||
characterLimit = int.Parse(filter.Args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (query.ImdbID != null && TorznabCaps.SupportsImdbMovieSearch)
|
|
||||||
{
|
|
||||||
break; // skip andmatch filter for imdb searches
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.TmdbID != null && TorznabCaps.SupportsTmdbMovieSearch)
|
|
||||||
{
|
|
||||||
break; // skip andmatch filter for tmdb searches
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.TvdbID != null && TorznabCaps.SupportsTvdbSearch)
|
|
||||||
{
|
|
||||||
break; // skip andmatch filter for tvdb searches
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryKeywords = variables[".Keywords"] as string;
|
|
||||||
|
|
||||||
if (!query.MatchQueryStringAND(release.Title, characterLimit, queryKeywords))
|
|
||||||
{
|
|
||||||
_logger.Debug(string.Format("CardigannIndexer ({0}): skipping {1} (andmatch filter)", _definition.Id, release.Title));
|
|
||||||
skipRelease = true;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "strdump":
|
|
||||||
// for debugging
|
|
||||||
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToHtmlPretty()));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipRelease)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if DateHeaders is set go through the previous rows and look for the header selector
|
|
||||||
var dateHeaders = _definition.Search.Rows.Dateheaders;
|
|
||||||
if (release.PublishDate == DateTime.MinValue && dateHeaders != null)
|
|
||||||
{
|
|
||||||
var prevRow = row.PreviousElementSibling;
|
|
||||||
string value = null;
|
|
||||||
if (prevRow == null)
|
|
||||||
{
|
|
||||||
// continue with parent
|
|
||||||
var parent = row.ParentElement;
|
|
||||||
if (parent != null)
|
|
||||||
{
|
|
||||||
prevRow = parent.PreviousElementSibling;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (prevRow != null)
|
var filters = search.Rows.Filters;
|
||||||
{
|
var skipRelease = ParseRowFilters(filters, release, variables, row);
|
||||||
var curRow = prevRow;
|
|
||||||
_logger.Debug(prevRow.OuterHtml);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
value = HandleSelector(dateHeaders, curRow);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
prevRow = curRow.PreviousElementSibling;
|
if (skipRelease)
|
||||||
if (prevRow == null)
|
|
||||||
{
|
|
||||||
// continue with parent
|
|
||||||
var parent = curRow.ParentElement;
|
|
||||||
if (parent != null)
|
|
||||||
{
|
|
||||||
prevRow = parent.PreviousElementSibling;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null && dateHeaders.Optional == false)
|
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("No date header row found for {0}", release.ToString()));
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
releases.Add(release);
|
releases.Add(release);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error(ex, "CardigannIndexer ({0}): Error while parsing row '{1}':\n\n{2}", _definition.Id, row.ToHtmlPretty());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else
|
||||||
{
|
{
|
||||||
// OnParseError(results, ex);
|
try
|
||||||
throw;
|
{
|
||||||
|
var searchResultParser = new HtmlParser();
|
||||||
|
var searchResultDocument = searchResultParser.ParseDocument(results);
|
||||||
|
|
||||||
|
/* checkForError(response, Definition.Search.Error); */
|
||||||
|
|
||||||
|
if (search.Preprocessingfilters != null)
|
||||||
|
{
|
||||||
|
results = ApplyFilters(results, search.Preprocessingfilters, variables);
|
||||||
|
searchResultDocument = searchResultParser.ParseDocument(results);
|
||||||
|
_logger.Trace(string.Format("CardigannIndexer ({0}): result after preprocessingfilters: {1}", _definition.Id, results));
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowsSelector = ApplyGoTemplateText(search.Rows.Selector, variables);
|
||||||
|
var rowsDom = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||||
|
var rows = new List<IElement>();
|
||||||
|
foreach (var rowDom in rowsDom)
|
||||||
|
{
|
||||||
|
rows.Add(rowDom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge following rows for After selector
|
||||||
|
var after = search.Rows.After;
|
||||||
|
if (after > 0)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < rows.Count; i += 1)
|
||||||
|
{
|
||||||
|
var currentRow = rows[i];
|
||||||
|
for (var j = 0; j < after; j += 1)
|
||||||
|
{
|
||||||
|
var mergeRowIndex = i + j + 1;
|
||||||
|
var mergeRow = rows[mergeRowIndex];
|
||||||
|
var mergeNodes = new List<INode>();
|
||||||
|
foreach (var node in mergeRow.ChildNodes)
|
||||||
|
{
|
||||||
|
mergeNodes.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRow.Append(mergeNodes.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.RemoveRange(i + 1, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var release = new TorrentInfo();
|
||||||
|
|
||||||
|
// Parse fields
|
||||||
|
foreach (var field in search.Fields)
|
||||||
|
{
|
||||||
|
var fieldParts = field.Key.Split('|');
|
||||||
|
var fieldName = fieldParts[0];
|
||||||
|
var fieldModifiers = new List<string>();
|
||||||
|
for (var i = 1; i < fieldParts.Length; i++)
|
||||||
|
{
|
||||||
|
fieldModifiers.Add(fieldParts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
string value = null;
|
||||||
|
var variablesKey = ".Result." + fieldName;
|
||||||
|
var isOptional = OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = HandleSelector(field.Value, row, variables, !isOptional);
|
||||||
|
|
||||||
|
if (isOptional && string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
variables[variablesKey] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
variables[variablesKey] = ParseFields(value, fieldName, release, fieldModifiers, searchUrlUri);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!variables.ContainsKey(variablesKey))
|
||||||
|
{
|
||||||
|
variables[variablesKey] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OptionalFields.Contains(field.Key) || fieldModifiers.Contains("optional") || field.Value.Optional)
|
||||||
|
{
|
||||||
|
variables[variablesKey] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexerLogging)
|
||||||
|
{
|
||||||
|
_logger.Trace("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters = search.Rows.Filters;
|
||||||
|
var skipRelease = ParseRowFilters(filters, release, variables, row);
|
||||||
|
|
||||||
|
if (skipRelease)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if DateHeaders is set go through the previous rows and look for the header selector
|
||||||
|
var dateHeaders = _definition.Search.Rows.Dateheaders;
|
||||||
|
if (release.PublishDate == DateTime.MinValue && dateHeaders != null)
|
||||||
|
{
|
||||||
|
var prevRow = row.PreviousElementSibling;
|
||||||
|
string value = null;
|
||||||
|
if (prevRow == null)
|
||||||
|
{
|
||||||
|
// continue with parent
|
||||||
|
var parent = row.ParentElement;
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
prevRow = parent.PreviousElementSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (prevRow != null)
|
||||||
|
{
|
||||||
|
var curRow = prevRow;
|
||||||
|
_logger.Debug(prevRow.OuterHtml);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = HandleSelector(dateHeaders, curRow);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
prevRow = curRow.PreviousElementSibling;
|
||||||
|
if (prevRow == null)
|
||||||
|
{
|
||||||
|
// continue with parent
|
||||||
|
var parent = curRow.ParentElement;
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
prevRow = parent.PreviousElementSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null && dateHeaders.Optional == false)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("No date header row found for {0}", release.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releases.Add(release);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "CardigannIndexer ({0}): Error while parsing row '{1}':\n\n{2}", _definition.Id, row.ToHtmlPretty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// OnParseError(results, ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -469,5 +341,234 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
|
|
||||||
return releases;
|
return releases;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ParseFields(string value, string fieldName, TorrentInfo release, List<string> fieldModifiers, Uri searchUrlUri)
|
||||||
|
{
|
||||||
|
switch (fieldName)
|
||||||
|
{
|
||||||
|
case "download":
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
release.DownloadUrl = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.StartsWith("magnet:"))
|
||||||
|
{
|
||||||
|
release.MagnetUrl = value;
|
||||||
|
value = release.MagnetUrl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.DownloadUrl = ResolvePath(value, searchUrlUri).AbsoluteUri;
|
||||||
|
value = release.DownloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "magnet":
|
||||||
|
var magnetUri = value;
|
||||||
|
release.MagnetUrl = magnetUri;
|
||||||
|
value = magnetUri.ToString();
|
||||||
|
break;
|
||||||
|
case "infohash":
|
||||||
|
release.InfoHash = value;
|
||||||
|
break;
|
||||||
|
case "details":
|
||||||
|
var url = ResolvePath(value, searchUrlUri)?.AbsoluteUri;
|
||||||
|
release.InfoUrl = url;
|
||||||
|
release.Guid = url;
|
||||||
|
value = url.ToString();
|
||||||
|
break;
|
||||||
|
case "comments":
|
||||||
|
var commentsUrl = ResolvePath(value, searchUrlUri);
|
||||||
|
if (release.CommentUrl == null)
|
||||||
|
{
|
||||||
|
release.CommentUrl = commentsUrl.AbsoluteUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = commentsUrl.ToString();
|
||||||
|
break;
|
||||||
|
case "title":
|
||||||
|
if (fieldModifiers.Contains("append"))
|
||||||
|
{
|
||||||
|
release.Title += value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.Title = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = release.Title;
|
||||||
|
break;
|
||||||
|
case "description":
|
||||||
|
if (fieldModifiers.Contains("append"))
|
||||||
|
{
|
||||||
|
release.Description += value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.Description = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = release.Description;
|
||||||
|
break;
|
||||||
|
case "category":
|
||||||
|
var cats = MapTrackerCatToNewznab(value);
|
||||||
|
if (cats.Any())
|
||||||
|
{
|
||||||
|
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||||
|
{
|
||||||
|
release.Categories = cats;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.Categories = release.Categories.Union(cats).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = release.Categories.ToString();
|
||||||
|
break;
|
||||||
|
case "size":
|
||||||
|
release.Size = ParseUtil.GetBytes(value);
|
||||||
|
value = release.Size.ToString();
|
||||||
|
break;
|
||||||
|
case "leechers":
|
||||||
|
var leechers = ParseUtil.CoerceLong(value);
|
||||||
|
leechers = leechers < 5000000L ? leechers : 0; // to fix #6558
|
||||||
|
if (release.Peers == null)
|
||||||
|
{
|
||||||
|
release.Peers = (int)leechers;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.Peers += (int)leechers;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = leechers.ToString();
|
||||||
|
break;
|
||||||
|
case "seeders":
|
||||||
|
release.Seeders = ParseUtil.CoerceInt(value);
|
||||||
|
release.Seeders = release.Seeders < 5000000L ? release.Seeders : 0; // to fix #6558
|
||||||
|
if (release.Peers == null)
|
||||||
|
{
|
||||||
|
release.Peers = release.Seeders;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
release.Peers += release.Seeders;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = release.Seeders.ToString();
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
release.PublishDate = DateTimeUtil.FromUnknown(value);
|
||||||
|
value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern);
|
||||||
|
break;
|
||||||
|
case "files":
|
||||||
|
release.Files = ParseUtil.CoerceInt(value);
|
||||||
|
value = release.Files.ToString();
|
||||||
|
break;
|
||||||
|
case "grabs":
|
||||||
|
release.Grabs = ParseUtil.CoerceInt(value);
|
||||||
|
value = release.Grabs.ToString();
|
||||||
|
break;
|
||||||
|
case "downloadvolumefactor":
|
||||||
|
release.DownloadVolumeFactor = ParseUtil.CoerceDouble(value);
|
||||||
|
value = release.DownloadVolumeFactor.ToString();
|
||||||
|
break;
|
||||||
|
case "uploadvolumefactor":
|
||||||
|
release.UploadVolumeFactor = ParseUtil.CoerceDouble(value);
|
||||||
|
value = release.UploadVolumeFactor.ToString();
|
||||||
|
break;
|
||||||
|
case "minimumratio":
|
||||||
|
release.MinimumRatio = ParseUtil.CoerceDouble(value);
|
||||||
|
value = release.MinimumRatio.ToString();
|
||||||
|
break;
|
||||||
|
case "minimumseedtime":
|
||||||
|
release.MinimumSeedTime = ParseUtil.CoerceLong(value);
|
||||||
|
value = release.MinimumSeedTime.ToString();
|
||||||
|
break;
|
||||||
|
case "imdb":
|
||||||
|
release.ImdbId = (int)ParseUtil.GetLongFromString(value);
|
||||||
|
value = release.ImdbId.ToString();
|
||||||
|
break;
|
||||||
|
case "tmdbid":
|
||||||
|
var tmdbIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||||
|
var tmdbIDMatch = tmdbIDRegEx.Match(value);
|
||||||
|
var tmdbID = tmdbIDMatch.Groups[1].Value;
|
||||||
|
release.TmdbId = (int)ParseUtil.CoerceLong(tmdbID);
|
||||||
|
value = release.TmdbId.ToString();
|
||||||
|
break;
|
||||||
|
case "rageid":
|
||||||
|
var rageIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||||
|
var rageIDMatch = rageIDRegEx.Match(value);
|
||||||
|
var rageID = rageIDMatch.Groups[1].Value;
|
||||||
|
release.TvRageId = (int)ParseUtil.CoerceLong(rageID);
|
||||||
|
value = release.TvRageId.ToString();
|
||||||
|
break;
|
||||||
|
case "tvdbid":
|
||||||
|
var tvdbIdRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
|
||||||
|
var tvdbIdMatch = tvdbIdRegEx.Match(value);
|
||||||
|
var tvdbId = tvdbIdMatch.Groups[1].Value;
|
||||||
|
release.TvdbId = (int)ParseUtil.CoerceLong(tvdbId);
|
||||||
|
value = release.TvdbId.ToString();
|
||||||
|
break;
|
||||||
|
case "poster":
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
var poster = ResolvePath(value, searchUrlUri);
|
||||||
|
release.PosterUrl = poster.AbsoluteUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = release.PosterUrl;
|
||||||
|
break;
|
||||||
|
|
||||||
|
//case "author":
|
||||||
|
// release.Author = value;
|
||||||
|
// break;
|
||||||
|
//case "booktitle":
|
||||||
|
// release.BookTitle = value;
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ParseRowFilters(List<FilterBlock> filters, ReleaseInfo release, Dictionary<string, object> variables, object row)
|
||||||
|
{
|
||||||
|
var skipRelease = false;
|
||||||
|
|
||||||
|
if (filters != null)
|
||||||
|
{
|
||||||
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
switch (filter.Name)
|
||||||
|
{
|
||||||
|
case "andmatch":
|
||||||
|
var characterLimit = -1;
|
||||||
|
if (filter.Args != null)
|
||||||
|
{
|
||||||
|
characterLimit = int.Parse(filter.Args);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryKeywords = variables[".Keywords"] as string;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "strdump":
|
||||||
|
// for debugging
|
||||||
|
_logger.Debug(string.Format("CardigannIndexer ({0}): row strdump: {1}", _definition.Id, row.ToString()));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.Error(string.Format("CardigannIndexer ({0}): Unsupported rows filter: {1}", _definition.Id, filter.Name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skipRelease;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,20 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
public class CardigannRequest : IndexerRequest
|
public class CardigannRequest : IndexerRequest
|
||||||
{
|
{
|
||||||
public Dictionary<string, object> Variables { get; private set; }
|
public Dictionary<string, object> Variables { get; private set; }
|
||||||
|
public SearchPathBlock SearchPath { get; private set; }
|
||||||
|
|
||||||
public CardigannRequest(string url, HttpAccept httpAccept, Dictionary<string, object> variables)
|
public CardigannRequest(string url, HttpAccept httpAccept, Dictionary<string, object> variables, SearchPathBlock searchPath)
|
||||||
: base(url, httpAccept)
|
: base(url, httpAccept)
|
||||||
{
|
{
|
||||||
Variables = variables;
|
Variables = variables;
|
||||||
|
SearchPath = searchPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CardigannRequest(HttpRequest httpRequest, Dictionary<string, object> variables)
|
public CardigannRequest(HttpRequest httpRequest, Dictionary<string, object> variables, SearchPathBlock searchPath)
|
||||||
: base(httpRequest)
|
: base(httpRequest)
|
||||||
{
|
{
|
||||||
Variables = variables;
|
Variables = variables;
|
||||||
|
SearchPath = searchPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1067,7 +1067,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = new CardigannRequest(requestbuilder.Build(), variables);
|
var request = new CardigannRequest(requestbuilder.Build(), variables, searchPath);
|
||||||
|
|
||||||
// send HTTP request
|
// send HTTP request
|
||||||
if (search.Headers != null)
|
if (search.Headers != null)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue