Add basic support for Cardigann definitions (#571)

* Add basic support for Cardigann definitions

* Add HDME definition

* Fix tests

* support split with negative indexes

* allow FromTimeAgo formats without spaces betwen value and unit

Example: 2h 3m

* Add basic support for Cardigann definitions

* Add HDME definition

* Fix tests

* support split with negative indexes

* allow FromTimeAgo formats without spaces betwen value and unit

Example: 2h 3m
This commit is contained in:
kaso17
2016-10-27 09:30:03 +02:00
committed by flightlevel
parent fa9bbaa18c
commit 21cffe2d35
13 changed files with 987 additions and 48 deletions

View File

@@ -42,5 +42,10 @@ namespace JackettTest
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void InitCardigannIndexers(string path)
{
throw new NotImplementedException();
}
} }
} }

View File

@@ -0,0 +1,101 @@
---
site: hdme
name: HDME
language: en-us
links:
- https://hdme.eu
caps:
categories:
24: TV/Anime # Anime
25: PC/0day # Appz
47: Movies/HD # AVCHD
26: Movies/BluRay # Bluray
54: Movies/HD # dbREMUX
41: Movies/HD # Documentaries
50: Movies/HD # FourGHD
44: Movies/HD # HDME
28: Audio/Lossless # HQ Music
48: Movies/HD # iCandy
45: Movies/HD # INtL
29: Other # Misc
49: PC/Phone-Other # Mobile
30: Movies/HD # Movie 1080i
31: Movies/HD # Movie 1080p
32: Movies/HD # Movie 720p
33: Audio/Video # Music Videos
34: TV # Packs
53: Movies/HD # Remux
56: Movies/HD # RUXi
55: Movies/HD # SiNiSteR
36: TV/Sport # Sports
37: TV/HD # TV Series 1080i
38: TV/HD # TV Series 1080p
39: TV/HD # TV Series 720p
57: Movies # UHD 2160p
40: XXX # XXX
modes:
search: [q]
tv-search: [q, season, ep]
login:
path: /takelogin.php
method: post
form: form
inputs:
username: "{{ .Config.username }}"
password: "{{ .Config.password }}"
error:
- selector: td.embedded
message:
selector: td.text
test:
path: /my.php
ratio:
path: /my.php
selector: span.smallfont > font
filters:
- name: regexp
args: "Ratio:(.+?)Uploaded"
- name: replace
args: [",", ""]
search:
path: /browse.php
inputs:
$raw: "{{range .Categories}}c{{.}}=1&{{end}}"
search: "{{ .Keywords }}"
incldead: "1"
blah: "0"
rows:
selector: table[width="100%"] > tbody > tr:has(td.bottom[background="_images/bg_torrent.jpg"])
fields:
category:
selector: td:nth-child(2) a
attribute: href
filters:
- name: querystring
args: cat
title:
selector: td:nth-child(3) > a
attribute: title
comments:
selector: td:nth-child(3) > a
attribute: href
download:
selector: td:nth-child(11) > a
attribute: href
size:
selector: td:nth-child(6)
remove: br
date:
selector: td:nth-child(3)
filters:
- name: regexp
args: "Added: (.+?)\n"
seeders:
selector: td:nth-child(8)
leechers:
selector: td:nth-child(9)

View File

@@ -17,13 +17,13 @@ namespace Jackett.Indexers
{ {
public abstract class BaseIndexer public abstract class BaseIndexer
{ {
public string SiteLink { get; private set; } public string SiteLink { get; protected set; }
public string DisplayDescription { get; private set; } public string DisplayDescription { get; protected set; }
public string DisplayName { get; private set; } public string DisplayName { get; protected set; }
public string ID { get { return GetIndexerID(GetType()); } } public string ID { get { return GetIndexerID(GetType()); } }
public bool IsConfigured { get; protected set; } public bool IsConfigured { get; protected set; }
public TorznabCapabilities TorznabCaps { get; private set; } public TorznabCapabilities TorznabCaps { get; protected set; }
protected Logger logger; protected Logger logger;
protected IIndexerManagerService indexerService; protected IIndexerManagerService indexerService;
protected static List<CachedQueryResult> cache = new List<CachedQueryResult>(); protected static List<CachedQueryResult> cache = new List<CachedQueryResult>();
@@ -44,7 +44,9 @@ namespace Jackett.Indexers
private List<CategoryMapping> categoryMapping = new List<CategoryMapping>(); private List<CategoryMapping> categoryMapping = new List<CategoryMapping>();
// standard constructor used by most indexers
public BaseIndexer(string name, string link, string description, IIndexerManagerService manager, IWebClient client, Logger logger, ConfigurationData configData, IProtectionService p, TorznabCapabilities caps = null, string downloadBase = null) public BaseIndexer(string name, string link, string description, IIndexerManagerService manager, IWebClient client, Logger logger, ConfigurationData configData, IProtectionService p, TorznabCapabilities caps = null, string downloadBase = null)
: this(manager, client, logger, p)
{ {
if (!link.EndsWith("/")) if (!link.EndsWith("/"))
throw new Exception("Site link must end with a slash."); throw new Exception("Site link must end with a slash.");
@@ -52,12 +54,7 @@ namespace Jackett.Indexers
DisplayName = name; DisplayName = name;
DisplayDescription = description; DisplayDescription = description;
SiteLink = link; SiteLink = link;
this.logger = logger;
indexerService = manager;
webclient = client;
protectionService = p;
this.downloadUrlBase = downloadBase; this.downloadUrlBase = downloadBase;
this.configData = configData; this.configData = configData;
if (caps == null) if (caps == null)
@@ -66,6 +63,15 @@ namespace Jackett.Indexers
} }
// minimal constructor used by e.g. cardigann generic indexer
public BaseIndexer(IIndexerManagerService manager, IWebClient client, Logger logger, IProtectionService p)
{
this.logger = logger;
indexerService = manager;
webclient = client;
protectionService = p;
}
public IEnumerable<ReleaseInfo> CleanLinks(IEnumerable<ReleaseInfo> releases) public IEnumerable<ReleaseInfo> CleanLinks(IEnumerable<ReleaseInfo> releases)
{ {
if (string.IsNullOrEmpty(downloadUrlBase)) if (string.IsNullOrEmpty(downloadUrlBase))

View File

@@ -0,0 +1,636 @@
using Jackett.Utils.Clients;
using NLog;
using Jackett.Services;
using Jackett.Utils;
using Jackett.Models;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System;
using Jackett.Models.IndexerConfig;
using System.Collections.Specialized;
using System.Text;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using static Jackett.Models.IndexerConfig.ConfigurationData;
using AngleSharp.Parser.Html;
using System.Text.RegularExpressions;
using System.Web;
namespace Jackett.Indexers
{
public class CardigannIndexer : BaseIndexer, IIndexer
{
protected IndexerDefinition Definition;
public new string ID { get { return (Definition != null ? Definition.Site : GetIndexerID(GetType())); } }
new ConfigurationData configData
{
get { return (ConfigurationData)base.configData; }
set { base.configData = value; }
}
// Cardigann yaml classes
public class IndexerDefinition {
public string Site { get; set; }
public List<settingsField> Settings { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Language { get; set; }
public List<string> Links { get; set; }
public capabilitiesBlock Caps { get; set; }
public loginBlock Login { get; set; }
public ratioBlock Ratio { get; set; }
public searchBlock Search { get; set; }
// IndexerDefinitionStats not needed/implemented
}
public class settingsField
{
public string Name { get; set; }
public string Type { get; set; }
public string Label { get; set; }
}
public class capabilitiesBlock
{
public Dictionary<string, string> Categories { get; set; }
public Dictionary<string, List<string>> Modes { get; set; }
}
public class loginBlock
{
public string Path { get; set; }
public string Method { get; set; }
public string Form { get; set; }
public Dictionary<string, string> Inputs { get; set; }
public List<errorBlock> Error { get; set; }
public pageTestBlock Test { get; set; }
}
public class errorBlock
{
public string Path { get; set; }
public string Selector { get; set; }
public selectorBlock Message { get; set; }
}
public class selectorBlock
{
public string Selector { get; set; }
public string Text { get; set; }
public string Attribute { get; set; }
public string Remove { get; set; }
public List<filterBlock> Filters { get; set; }
public Dictionary<string, string> Case { get; set; }
}
public class filterBlock
{
public string Name { get; set; }
public dynamic Args { get; set; }
}
public class pageTestBlock
{
public string Path { get; set; }
public string Selector { get; set; }
}
public class ratioBlock : selectorBlock
{
public string Path { get; set; }
}
public class searchBlock
{
public string Path { get; set; }
public Dictionary<string, string> Inputs { get; set; }
public rowsBlock Rows { get; set; }
public Dictionary<string, selectorBlock> Fields { get; set; }
}
public class rowsBlock : selectorBlock
{
public int After { get; set; }
//public string Remove { get; set; } // already inherited
public string Dateheaders { get; set; }
}
public CardigannIndexer(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps)
: base(manager: i,
client: wc,
logger: l,
p: ps)
{
}
public CardigannIndexer(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps, string DefinitionString)
: base(manager: i,
client: wc,
logger: l,
p: ps)
{
Init(DefinitionString);
}
protected void Init(string DefinitionString)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new CamelCaseNamingConvention())
.IgnoreUnmatchedProperties()
.Build();
Definition = deserializer.Deserialize<IndexerDefinition>(DefinitionString);
// Add default data if necessary
if (Definition.Settings == null)
Definition.Settings = new List<settingsField>();
if (Definition.Settings.Count == 0)
{
Definition.Settings.Add(new settingsField { Name = "username", Label = "Username", Type = "text" });
Definition.Settings.Add(new settingsField { Name = "password", Label = "Password", Type = "password" });
}
// init missing mandatory attributes
DisplayName = Definition.Name;
DisplayDescription = Definition.Description;
SiteLink = Definition.Links[0]; // TODO: implement alternative links
if (!SiteLink.EndsWith("/"))
SiteLink += "/";
TorznabCaps = TorznabUtil.CreateDefaultTorznabTVCaps(); // TODO implement caps
// init config Data
configData = new ConfigurationData();
foreach (var Setting in Definition.Settings)
{
configData.AddDynamic(Setting.Name, new StringItem { Name = Setting.Label });
}
foreach (var Category in Definition.Caps.Categories)
{
var cat = TorznabCatType.GetCatByName(Category.Value);
if (cat == null)
{
logger.Error(string.Format("CardigannIndexer ({0}): Can't find a category for {1}", ID, Category.Value));
continue;
}
AddCategoryMapping(Category.Key, TorznabCatType.GetCatByName(Category.Value));
}
}
protected Dictionary<string, object> getTemplateVariablesFromConfigData()
{
Dictionary<string, object> variables = new Dictionary<string, object>();
foreach (settingsField Setting in Definition.Settings)
{
variables[".Config."+Setting.Name] = ((StringItem)configData.GetDynamic(Setting.Name)).Value;
}
return variables;
}
// A very bad implementation of the golang template/text templating engine.
// But it should work for most basic constucts used by Cardigann definitions.
protected string applyGoTemplateText(string template, Dictionary<string, object> variables = null)
{
if (variables == null)
{
variables = getTemplateVariablesFromConfigData();
}
// handle if ... else ... expression
Regex IfElseRegex = new Regex(@"{{if\s*(.+?)\s*}}(.*?){{\s*else\s*}}(.*?){{\s*end\s*}}");
var IfElseRegexMatches = IfElseRegex.Match(template);
while (IfElseRegexMatches.Success)
{
string conditionResult = null;
string all = IfElseRegexMatches.Groups[0].Value;
string condition = IfElseRegexMatches.Groups[1].Value;
string onTrue = IfElseRegexMatches.Groups[2].Value;
string onFalse = IfElseRegexMatches.Groups[3].Value;
if (condition.StartsWith("."))
{
string value = (string)variables[condition];
if (!string.IsNullOrWhiteSpace(value))
{
conditionResult = onTrue;
}
else
{
conditionResult = onFalse;
}
}
else
{
throw new NotImplementedException("CardigannIndexer: Condition operation '" + condition + "' not implemented");
}
template = template.Replace(all, conditionResult);
IfElseRegexMatches = IfElseRegexMatches.NextMatch();
}
// handle range expression
Regex RangeRegex = new Regex(@"{{\s*range\s*(.+?)\s*}}(.*?){{\.}}(.*?){{end}}");
var RangeRegexMatches = RangeRegex.Match(template);
while (RangeRegexMatches.Success)
{
string expanded = string.Empty;
string all = RangeRegexMatches.Groups[0].Value;
string variable = RangeRegexMatches.Groups[1].Value;
string prefix = RangeRegexMatches.Groups[2].Value;
string postfix = RangeRegexMatches.Groups[3].Value;
foreach (string value in (List<string>)variables[variable])
{
expanded += prefix + value + postfix;
}
template = template.Replace(all, expanded);
RangeRegexMatches = RangeRegexMatches.NextMatch();
}
// handle simple variables
Regex VariablesRegEx = new Regex(@"{{\s*(\..+?)\s*}}");
var VariablesRegExMatches = VariablesRegEx.Match(template);
while (VariablesRegExMatches.Success)
{
string expanded = string.Empty;
string all = VariablesRegExMatches.Groups[0].Value;
string variable = VariablesRegExMatches.Groups[1].Value;
string value = (string)variables[variable];
template = template.Replace(all, value);
VariablesRegExMatches = VariablesRegExMatches.NextMatch();
}
return template;
}
protected async Task<bool> DoLogin()
{
var Login = Definition.Login;
if (Login == null)
return false;
if (Login.Method == "post")
{
var pairs = new Dictionary<string, string>();
foreach (var Input in Definition.Login.Inputs)
{
var value = applyGoTemplateText(Input.Value);
pairs.Add(Input.Key, value);
}
foreach (var x in pairs)
{
logger.Error(x.Key + ": " + x.Value);
}
var LoginUrl = SiteLink + Login.Path;
configData.CookieHeader.Value = null;
var loginResult = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, SiteLink, true);
configData.CookieHeader.Value = loginResult.Cookies;
if (Login.Error != null)
{
var loginResultParser = new HtmlParser();
var loginResultDocument = loginResultParser.Parse(loginResult.Content);
foreach (errorBlock error in Login.Error)
{
var selection = loginResultDocument.QuerySelector(error.Selector);
if (selection != null)
{
string errorMessage = selection.TextContent;
if (error.Message != null)
{
var errorSubMessage = loginResultDocument.QuerySelector(error.Message.Selector);
errorMessage = errorSubMessage.TextContent;
}
throw new ExceptionWithConfigData(string.Format("Login failed: {0}", errorMessage.Trim()), configData);
}
}
}
}
else if (Login.Method == "cookie")
{
configData.CookieHeader.Value = ((StringItem)configData.GetDynamic("cookie")).Value;
}
else
{
throw new NotImplementedException("Login method " + Definition.Login.Method + " not implemented");
}
return true;
}
protected async Task<bool> TestLogin()
{
var Login = Definition.Login;
if (Login == null || Login.Test == null)
return false;
// test if login was successful
var LoginTestUrl = SiteLink + Login.Test.Path;
var testResult = await RequestStringWithCookies(LoginTestUrl);
if (testResult.IsRedirect)
{
throw new ExceptionWithConfigData("Login Failed, got redirected", configData);
}
if (Login.Test.Selector != null)
{
var testResultParser = new HtmlParser();
var testResultDocument = testResultParser.Parse(testResult.Content);
var selection = testResultDocument.QuerySelectorAll(Login.Test.Selector);
if (selection.Length == 0)
{
throw new ExceptionWithConfigData(string.Format("Login failed: Selector \"{0}\" didn't match", Login.Test.Selector), configData);
}
}
return true;
}
public async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
{
configData.LoadValuesFromJson(configJson);
await DoLogin();
await TestLogin();
SaveConfig();
IsConfigured = true;
return IndexerConfigurationStatus.Completed;
}
protected string applyFilters(string Data, List<filterBlock> Filters)
{
if (Filters == null)
return Data;
foreach(filterBlock Filter in Filters)
{
switch (Filter.Name)
{
case "querystring":
var param = (string)Filter.Args;
var qsStr = Data.Split(new char[] { '?' }, 2)[1];
qsStr = Data.Split(new char[] { '#' }, 2)[0];
var qs = HttpUtility.ParseQueryString(qsStr);
Data = qs.Get(param);
break;
case "timeparse":
case "dateparse":
throw new NotImplementedException("Filter " + Filter.Name + " not implemented");
/*
TODO: implement golang time format conversion, see http://fuckinggodateformat.com/
if args == nil {
return filterDateParse(nil, value)
}
if layout, ok := args.(string); ok {
return filterDateParse([]string{layout}, value)
}
return "", fmt.Errorf("Filter argument type %T was invalid", args)
*/
break;
case "regexp":
var pattern = (string)Filter.Args;
var Regexp = new Regex(pattern);
var Match = Regexp.Match(Data);
Data = Match.Groups[1].Value;
break;
case "split":
var sep = (string)Filter.Args[0];
var pos = (string)Filter.Args[1];
var posInt = int.Parse(pos);
var strParts = Data.Split(sep[0]);
if (posInt < 0)
{
posInt += strParts.Length;
}
Data = strParts[posInt];
break;
case "replace":
var from = (string)Filter.Args[0];
var to = (string)Filter.Args[1];
Data = Data.Replace(from, to);
break;
case "trim":
var cutset = (string)Filter.Args;
Data = Data.Trim(cutset[0]);
break;
case "append":
var str = (string)Filter.Args;
Data += str;
break;
case "timeago":
case "fuzzytime":
case "reltime":
var timestr = (string)Filter.Args;
Data = DateTimeUtil.FromUnknown(timestr).ToString(DateTimeUtil.RFC1123ZPattern);
break;
default:
break;
}
}
return Data;
}
protected string handleSelector(selectorBlock Selector, AngleSharp.Dom.IElement Dom)
{
if (Selector.Text != null)
{
return applyFilters(Selector.Text, Selector.Filters);
}
string value = null;
if (Selector.Selector != null)
{
AngleSharp.Dom.IElement selection = Dom.QuerySelector(Selector.Selector);
if (selection == null)
{
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", Selector.Selector, Dom.OuterHtml));
}
if (Selector.Remove != null)
{
foreach(var i in selection.QuerySelectorAll(Selector.Remove))
{
i.Remove();
}
}
if (Selector.Attribute != null)
{
value = selection.GetAttribute(Selector.Attribute);
}
else
{
value = selection.TextContent;
}
}
return applyFilters(value, Selector.Filters); ;
}
protected Uri resolvePath(string path)
{
return new Uri(SiteLink + path);
}
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var releases = new List<ReleaseInfo>();
searchBlock Search = Definition.Search;
// init template context
var variables = getTemplateVariablesFromConfigData();
variables[".Query.Type"] = query.QueryType;
variables[".Query.Q"] = query.SearchTerm;
variables[".Query.Series"] = null;
variables[".Query.Ep"] = query.Episode;
variables[".Query.Season"] = query.Season;
variables[".Query.Movie"] = null;
variables[".Query.Year"] = null;
variables[".Query.Limit"] = query.Limit;
variables[".Query.Offset"] = query.Offset;
variables[".Query.Extended"] = query.Extended;
variables[".Query.Categories"] = query.Categories;
variables[".Query.APIKey"] = query.ApiKey;
variables[".Query.TVDBID"] = null;
variables[".Query.TVRageID"] = query.RageID;
variables[".Query.IMDBID"] = query.ImdbID;
variables[".Query.TVMazeID"] = null;
variables[".Query.TraktID"] = null;
variables[".Query.Episode"] = query.GetEpisodeSearchString();
variables[".Categories"] = MapTorznabCapsToTrackers(query);
var KeywordTokens = new List<string>();
var KeywordTokenKeys = new List<string> { "Q", "Series", "Movie", "Year" };
foreach (var key in KeywordTokenKeys)
{
var Value = (string)variables[".Query." + key];
if (!string.IsNullOrWhiteSpace(Value))
KeywordTokens.Add(Value);
}
if (!string.IsNullOrWhiteSpace((string)variables[".Query.Episode"]))
KeywordTokens.Add((string)variables[".Query.Episode"]);
variables[".Query.Keywords"] = string.Join(" ", KeywordTokens);
variables[".Keywords"] = variables[".Query.Keywords"];
// build search URL
var searchUrl = SiteLink + applyGoTemplateText(Search.Path, variables) + "?";
var queryCollection = new NameValueCollection();
if (Search.Inputs != null)
{
foreach (var Input in Search.Inputs)
{
var value = applyGoTemplateText(Input.Value, variables);
if (Input.Key == "$raw")
searchUrl += value;
else
queryCollection.Add(Input.Key, value);
}
}
searchUrl += "&" + queryCollection.GetQueryString();
// send HTTP request
var response = await RequestBytesWithCookies(searchUrl);
var results = Encoding.GetEncoding("iso-8859-1").GetString(response.Content);
try
{
var SearchResultParser = new HtmlParser();
var SearchResultDocument = SearchResultParser.Parse(results);
var Rows = SearchResultDocument.QuerySelectorAll(Search.Rows.Selector);
foreach (var Row in Rows)
{
try
{
var release = new ReleaseInfo();
release.MinimumRatio = 1;
release.MinimumSeedTime = 48 * 60 * 60;
// Parse fields
foreach (var Field in Search.Fields)
{
string value = handleSelector(Field.Value, Row);
try
{
switch (Field.Key)
{
case "download":
release.Link = resolvePath(value);
break;
case "details":
var url = resolvePath(value);
release.Guid = url;
if (release.Comments == null)
release.Comments = url;
break;
case "comments":
release.Comments = resolvePath(value);
break;
case "title":
release.Title = value;
break;
case "description":
release.Description = value;
break;
case "category":
release.Category = MapTrackerCatToNewznab(value);
break;
case "size":
release.Size = ReleaseInfo.GetBytes(value);
break;
case "leechers":
if (release.Peers == null)
release.Peers = ParseUtil.CoerceInt(value);
else
release.Peers += ParseUtil.CoerceInt(value);
break;
case "seeders":
release.Seeders = ParseUtil.CoerceInt(value);
if (release.Peers == null)
release.Peers = release.Seeders;
else
release.Peers += release.Seeders;
break;
case "date":
release.PublishDate = DateTimeUtil.FromUnknown(value);
break;
default:
break;
}
}
catch (Exception ex)
{
throw new Exception(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", Field.Key, Field.Value.Selector, value, ex.Message));
}
}
releases.Add(release);
}
catch (Exception ex)
{
logger.Error(string.Format("CardigannIndexer ({0}): Error while parsing row '{1}': {2}", ID, Row.OuterHtml, ex.Message));
}
}
}
catch (Exception ex)
{
OnParseError(results, ex);
}
return releases;
}
}
}

View File

@@ -54,6 +54,10 @@
<HintPath>..\packages\CloudFlareUtilities.0.3.2-alpha\lib\portable45-net45+win8+wpa81\CloudFlareUtilities.dll</HintPath> <HintPath>..\packages\CloudFlareUtilities.0.3.2-alpha\lib\portable45-net45+win8+wpa81\CloudFlareUtilities.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="DateTimeRoutines, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DateTimeRoutines.1.0.16\lib\net40\DateTimeRoutines.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration.Install" /> <Reference Include="System.Configuration.Install" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@@ -147,6 +151,10 @@
<Reference Include="System.Web.Http.Tracing, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <Reference Include="System.Web.Http.Tracing, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<HintPath>..\packages\Microsoft.AspNet.WebApi.Tracing.5.2.3\lib\net45\System.Web.Http.Tracing.dll</HintPath> <HintPath>..\packages\Microsoft.AspNet.WebApi.Tracing.5.2.3\lib\net45\System.Web.Http.Tracing.dll</HintPath>
</Reference> </Reference>
<Reference Include="YamlDotNet, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.4.0.1-pre288\lib\net35\YamlDotNet.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AuthenticationException.cs" /> <Compile Include="AuthenticationException.cs" />
@@ -156,6 +164,7 @@
<Compile Include="Controllers\TorznabController.cs" /> <Compile Include="Controllers\TorznabController.cs" />
<Compile Include="Controllers\DownloadController.cs" /> <Compile Include="Controllers\DownloadController.cs" />
<Compile Include="Engine.cs" /> <Compile Include="Engine.cs" />
<Compile Include="Indexers\CardigannIndexer.cs" />
<Compile Include="Indexers\myAmity.cs" /> <Compile Include="Indexers\myAmity.cs" />
<Compile Include="Indexers\TorrentNetwork.cs" /> <Compile Include="Indexers\TorrentNetwork.cs" />
<Compile Include="Indexers\Andraste.cs" /> <Compile Include="Indexers\Andraste.cs" />
@@ -361,6 +370,9 @@
<None Include="CurlSharp.dll.config"> <None Include="CurlSharp.dll.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Include="Definitions\hdme.yml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config"> <None Include="packages.config">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</None> </None>
@@ -687,6 +699,7 @@
<ItemGroup> <ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" /> <Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -10,9 +10,10 @@ using System.Threading.Tasks;
namespace Jackett.Models.IndexerConfig namespace Jackett.Models.IndexerConfig
{ {
public abstract class ConfigurationData public class ConfigurationData
{ {
const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||"; const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||";
protected Dictionary<string, Item> dynamics = new Dictionary<string, Item>(); // list for dynamic items
public enum ItemType public enum ItemType
{ {
@@ -132,6 +133,8 @@ namespace Jackett.Models.IndexerConfig
.Where(p => p.PropertyType.IsSubclassOf(typeof(Item))) .Where(p => p.PropertyType.IsSubclassOf(typeof(Item)))
.Select(p => (Item)p.GetValue(this)); .Select(p => (Item)p.GetValue(this));
properties = properties.Concat(dynamics.Values).ToArray();
if (!forDisplay) if (!forDisplay)
{ {
properties = properties properties = properties
@@ -142,6 +145,16 @@ namespace Jackett.Models.IndexerConfig
return properties.ToArray(); return properties.ToArray();
} }
public void AddDynamic(string ID, Item item)
{
dynamics.Add(ID, item);
}
public Item GetDynamic(string ID)
{
return dynamics[ID];
}
public class Item public class Item
{ {
public ItemType ItemType { get; set; } public ItemType ItemType { get; set; }

View File

@@ -27,5 +27,21 @@ namespace Jackett.Models
return string.Empty; return string.Empty;
} }
public static string NormalizeCatName(string name)
{
return name.Replace(" ", "").ToLower();
}
public static TorznabCategory GetCatByName(string name)
{
var cat = AllCats.FirstOrDefault(c => NormalizeCatName(c.Name) == NormalizeCatName(name));
if (cat != null)
{
return cat;
}
return null;
}
} }
} }

View File

@@ -25,6 +25,7 @@ namespace Jackett.Services
T GetConfig<T>(); T GetConfig<T>();
void SaveConfig<T>(T config); void SaveConfig<T>(T config);
string ApplicationFolder(); string ApplicationFolder();
string GetCardigannDefinitionsFolder();
void CreateOrMigrateSettings(); void CreateOrMigrateSettings();
void PerformMigration(); void PerformMigration();
} }
@@ -197,6 +198,22 @@ namespace Jackett.Services
return dir; return dir;
} }
public string GetCardigannDefinitionsFolder()
{
// If we are debugging we can use the non copied definitions.
string dir = Path.Combine(ApplicationFolder(), "Definitions"); ;
#if DEBUG
// When we are running in debug use the source files
var sourcePath = Path.GetFullPath(Path.Combine(ApplicationFolder(), "..\\..\\..\\Jackett\\Definitions"));
if (Directory.Exists(sourcePath))
{
dir = sourcePath;
}
#endif
return dir;
}
public string GetVersion() public string GetVersion()
{ {
return Assembly.GetExecutingAssembly().GetName().Version.ToString(); return Assembly.GetExecutingAssembly().GetName().Version.ToString();

View File

@@ -22,6 +22,7 @@ namespace Jackett.Services
IEnumerable<IIndexer> GetAllIndexers(); IEnumerable<IIndexer> GetAllIndexers();
void SaveConfig(IIndexer indexer, JToken obj); void SaveConfig(IIndexer indexer, JToken obj);
void InitIndexers(); void InitIndexers();
void InitCardigannIndexers(string path);
} }
public class IndexerManagerService : IIndexerManagerService public class IndexerManagerService : IIndexerManagerService
@@ -40,26 +41,53 @@ namespace Jackett.Services
cacheService = cache; cacheService = cache;
} }
protected void LoadIndexerConfig(IIndexer idx)
{
var configFilePath = GetIndexerConfigFilePath(idx);
if (File.Exists(configFilePath))
{
var fileStr = File.ReadAllText(configFilePath);
var jsonString = JToken.Parse(fileStr);
try
{
idx.LoadFromSavedConfiguration(jsonString);
}
catch (Exception ex)
{
logger.Error(ex, "Failed loading configuration for {0}, you must reconfigure this indexer", idx.DisplayName);
}
}
}
public void InitIndexers() public void InitIndexers()
{ {
logger.Info("Using HTTP Client: " + container.Resolve<IWebClient>().GetType().Name); logger.Info("Using HTTP Client: " + container.Resolve<IWebClient>().GetType().Name);
foreach (var idx in container.Resolve<IEnumerable<IIndexer>>().OrderBy(_ => _.DisplayName)) foreach (var idx in container.Resolve<IEnumerable<IIndexer>>().Where(p => p.ID != "cardigannindexer").OrderBy(_ => _.DisplayName))
{ {
indexers.Add(idx.ID, idx); indexers.Add(idx.ID, idx);
var configFilePath = GetIndexerConfigFilePath(idx); LoadIndexerConfig(idx);
if (File.Exists(configFilePath)) }
}
public void InitCardigannIndexers(string path)
{
logger.Info("Loading Cardigann definitions from: " + path);
DirectoryInfo d = new DirectoryInfo(path);
foreach (var file in d.GetFiles("*.yml"))
{
string DefinitionString = File.ReadAllText(file.FullName);
CardigannIndexer idx = new CardigannIndexer(this, container.Resolve<IWebClient>(), logger, container.Resolve<IProtectionService>(), DefinitionString);
if (indexers.ContainsKey(idx.ID))
{ {
var fileStr = File.ReadAllText(configFilePath); logger.Debug(string.Format("Ignoring definition ID={0}, file={1}: Indexer already exists", idx.ID, file.FullName));
var jsonString = JToken.Parse(fileStr); }
try else
{ {
idx.LoadFromSavedConfiguration(jsonString); indexers.Add(idx.ID, idx);
} LoadIndexerConfig(idx);
catch (Exception ex)
{
logger.Error(ex, "Failed loading configuration for {0}, you must reconfigure this indexer", idx.DisplayName);
}
} }
} }
} }

View File

@@ -150,6 +150,7 @@ namespace Jackett.Services
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
// Load indexers // Load indexers
indexerService.InitIndexers(); indexerService.InitIndexers();
indexerService.InitCardigannIndexers(configService.GetCardigannDefinitionsFolder());
client.Init(); client.Init();
} }

View File

@@ -1,13 +1,17 @@
using System; using Cliver;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Jackett.Utils namespace Jackett.Utils
{ {
public static class DateTimeUtil public static class DateTimeUtil
{ {
public static string RFC1123ZPattern = "ddd, dd MMM yyyy HH':'mm':'ss z";
public static DateTime UnixTimestampToDateTime(double unixTime) public static DateTime UnixTimestampToDateTime(double unixTime)
{ {
DateTime unixStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); DateTime unixStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
@@ -32,33 +36,117 @@ namespace Jackett.Utils
return DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Local); return DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Local);
} }
var dateParts = str.Split(new char[0], StringSplitOptions.RemoveEmptyEntries); str = str.Replace(",", "");
str = str.Replace("ago", "");
str = str.Replace("and", "");
TimeSpan timeAgo = TimeSpan.Zero; TimeSpan timeAgo = TimeSpan.Zero;
for (var i = 0; i < dateParts.Length / 2; i++) Regex TimeagoRegex = new Regex(@"\s*?([\d\.]+)\s*?([^\d\s\.]+)\s*?");
var TimeagoMatches = TimeagoRegex.Match(str);
while (TimeagoMatches.Success)
{ {
var val = ParseUtil.CoerceFloat(dateParts[i * 2]); string expanded = string.Empty;
var unit = dateParts[i * 2 + 1];
if (unit.Contains("sec")) var val = ParseUtil.CoerceFloat(TimeagoMatches.Groups[1].Value);
var unit = TimeagoMatches.Groups[2].Value;
TimeagoMatches = TimeagoMatches.NextMatch();
if (unit.Contains("sec") || unit == "s")
timeAgo += TimeSpan.FromSeconds(val); timeAgo += TimeSpan.FromSeconds(val);
else if (unit.Contains("min")) else if (unit.Contains("min") || unit == "m")
timeAgo += TimeSpan.FromMinutes(val); timeAgo += TimeSpan.FromMinutes(val);
else if (unit.Contains("hour") || unit.Contains("hr")) else if (unit.Contains("hour") || unit.Contains("hr") || unit == "h")
timeAgo += TimeSpan.FromHours(val); timeAgo += TimeSpan.FromHours(val);
else if (unit.Contains("day")) else if (unit.Contains("day") ||unit == "d")
timeAgo += TimeSpan.FromDays(val); timeAgo += TimeSpan.FromDays(val);
else if (unit.Contains("week") || unit.Contains("wk")) else if (unit.Contains("week") || unit.Contains("wk") || unit == "w")
timeAgo += TimeSpan.FromDays(val * 7); timeAgo += TimeSpan.FromDays(val * 7);
else if (unit.Contains("month")) else if (unit.Contains("month") || unit == "mo")
timeAgo += TimeSpan.FromDays(val * 30); timeAgo += TimeSpan.FromDays(val * 30);
else if (unit.Contains("year")) else if (unit.Contains("year") || unit == "y")
timeAgo += TimeSpan.FromDays(val * 365); timeAgo += TimeSpan.FromDays(val * 365);
else else
{ {
throw new Exception("TimeAgo parsing failed"); throw new Exception("TimeAgo parsing failed, unknown unit: "+unit);
} }
} }
return DateTime.SpecifyKind(DateTime.Now - timeAgo, DateTimeKind.Local); return DateTime.SpecifyKind(DateTime.Now - timeAgo, DateTimeKind.Local);
} }
// Uses the DateTimeRoutines library to parse the date
// http://www.codeproject.com/Articles/33298/C-Date-Time-Parser
public static DateTime FromFuzzyTime(string str, DateTimeRoutines.DateTimeFormat format = DateTimeRoutines.DateTimeFormat.USA_DATE)
{
DateTimeRoutines.ParsedDateTime dt;
if (DateTimeRoutines.TryParseDateOrTime(str, format, out dt))
{
return dt.DateTime;
}
throw new Exception("FromFuzzyTime parsing failed");
}
public static Regex timeAgoRegexp = new Regex(@"(?i)\bago", RegexOptions.Compiled);
public static Regex todayRegexp = new Regex(@"(?i)\btoday([\s,]+|$)", RegexOptions.Compiled);
public static Regex tomorrowRegexp = new Regex(@"(?i)\btomorrow([\s,]+|$)", RegexOptions.Compiled);
public static Regex yesterdayRegexp = new Regex(@"(?i)\byesterday([\s,]+|$)", RegexOptions.Compiled);
public static Regex missingYearRegexp = new Regex(@"^\d{1,2}-\d{1,2}\b", RegexOptions.Compiled);
public static DateTime FromUnknown(string str)
{
str = ParseUtil.NormalizeSpace(str);
Match match;
// ... ago
match = timeAgoRegexp.Match(str);
if (match.Success)
{
var timeago = str;
return FromTimeAgo(timeago);
}
// Today ...
match = todayRegexp.Match(str);
if (match.Success)
{
var time = str.Replace(match.Groups[0].Value, "");
DateTime dt = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Unspecified);
dt += TimeSpan.Parse(time);
return dt;
}
// Yesterday ...
match = yesterdayRegexp.Match(str);
if (match.Success)
{
var time = str.Replace(match.Groups[0].Value, "");
DateTime dt = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Unspecified);
dt += TimeSpan.Parse(time);
dt -= TimeSpan.FromDays(1);
return dt;
}
// Tomorrow ...
match = tomorrowRegexp.Match(str);
if (match.Success)
{
var time = str.Replace(match.Groups[0].Value, "");
DateTime dt = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Unspecified);
dt += TimeSpan.Parse(time);
dt += TimeSpan.FromDays(1);
return dt;
}
// add missing year
match = missingYearRegexp.Match(str);
if (match.Success)
{
var date = match.Groups[0].Value;
string newDate = date+"-"+DateTime.Now.Year.ToString();
str = str.Replace(date, newDate);
}
return FromFuzzyTime(str);
}
} }
} }

View File

@@ -9,45 +9,58 @@ namespace Jackett.Utils
{ {
public static class ParseUtil public static class ParseUtil
{ {
public static string NormalizeSpace(string s)
{
return s.Trim();
}
public static string NormalizeNumber(string s)
{
string normalized = NormalizeSpace(s);
normalized = normalized.Replace("-", "0");
normalized = normalized.Replace(",", "");
return normalized;
}
public static double CoerceDouble(string str) public static double CoerceDouble(string str)
{ {
return double.Parse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture); return double.Parse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture);
} }
public static float CoerceFloat(string str) public static float CoerceFloat(string str)
{ {
return float.Parse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture); return float.Parse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture);
} }
public static int CoerceInt(string str) public static int CoerceInt(string str)
{ {
return int.Parse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture); return int.Parse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture);
} }
public static long CoerceLong(string str) public static long CoerceLong(string str)
{ {
return long.Parse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture); return long.Parse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture);
} }
public static bool TryCoerceDouble(string str, out double result) public static bool TryCoerceDouble(string str, out double result)
{ {
return double.TryParse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out result); return double.TryParse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
} }
public static bool TryCoerceFloat(string str, out float result) public static bool TryCoerceFloat(string str, out float result)
{ {
return float.TryParse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out result); return float.TryParse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
} }
public static bool TryCoerceInt(string str, out int result) public static bool TryCoerceInt(string str, out int result)
{ {
return int.TryParse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out result); return int.TryParse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
} }
public static bool TryCoerceLong(string str, out long result) public static bool TryCoerceLong(string str, out long result)
{ {
return long.TryParse(str.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out result); return long.TryParse(NormalizeNumber(str), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
} }
} }

View File

@@ -9,6 +9,7 @@
<package id="AutoMapper" version="4.1.1" targetFramework="net45" /> <package id="AutoMapper" version="4.1.1" targetFramework="net45" />
<package id="CloudFlareUtilities" version="0.3.2-alpha" targetFramework="net45" /> <package id="CloudFlareUtilities" version="0.3.2-alpha" targetFramework="net45" />
<package id="CsQuery" version="1.3.4" targetFramework="net45" /> <package id="CsQuery" version="1.3.4" targetFramework="net45" />
<package id="DateTimeRoutines" version="1.0.16" targetFramework="net45" />
<package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net45" /> <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net45" />
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net45" />
@@ -30,4 +31,5 @@
<package id="NLog.Windows.Forms" version="4.2.3" targetFramework="net45" /> <package id="NLog.Windows.Forms" version="4.2.3" targetFramework="net45" />
<package id="Owin" version="1.0" targetFramework="net45" /> <package id="Owin" version="1.0" targetFramework="net45" />
<package id="SharpZipLib" version="0.86.0" targetFramework="net45" /> <package id="SharpZipLib" version="0.86.0" targetFramework="net45" />
<package id="YamlDotNet" version="4.0.1-pre288" targetFramework="net45" />
</packages> </packages>