Compare commits

...

18 Commits

Author SHA1 Message Date
Garfield69
01ff410e62 dark-shadow: fix 8a51a26 2021-05-10 16:47:23 +12:00
Garfield69
25942ab7f5 crazyspirits: new layout 2021-05-10 16:30:55 +12:00
Garfield69
f8ff98ed4c torrentslocal: certificate expired 2021-05-10 15:28:19 +12:00
Garfield69
4761718ce3 byrutor: drop grabs 2021-05-10 15:10:11 +12:00
Garfield69
b80d7c97e6 torrentwhiz: new domain *25.me 2021-05-10 15:03:32 +12:00
Garfield69
f0eb037dc7 torrentview: new domain *.34.com 2021-05-10 15:01:00 +12:00
Garfield69
f86acd2721 hdhouse: drop *.su domain 2021-05-10 14:57:42 +12:00
Garfield69
56a7e432d4 lesaloon: new login form. #11698 2021-05-10 13:14:34 +12:00
Garfield69
fbad508789 Merge branch 'master' of https://github.com/Jackett/Jackett 2021-05-09 12:19:09 +12:00
Garfield69
e1511de04b turktorrent: site has enabled hcaptcha. switch to cookie method. 2021-05-09 12:19:06 +12:00
Alessio Gogna
0a5582a323 [enhancement] Filter Indexers documentation (#11697) 2021-05-09 09:22:21 +12:00
Alessio Gogna
66bec102db [Feature] Filter Meta Indexer by tag and by language (#11662). resolves #8884 resolves #7170 resolves #4787 resolves #2185
* bump to 0.18.*

Also partially addresses https://github.com/Jackett/Jackett/issues/661 (if user adds `enabled` and `disabled` tags).

Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2021-05-08 21:24:18 +01:00
JigSaw
b07543bff6 xthor: misc enhancements (#11695)
* xthor: added categories dropping options (enabled, only for TMDB queries, disabled) - For TMDB queries by default
* xthor: removed search parameter when no search term submitted.
2021-05-08 21:52:52 +02:00
Garfield69
83c7028a95 cinematik: fix login. resolves #11470 2021-05-09 07:49:26 +12:00
Garfield69
1a89baa9a1 torrentqq: new domain 2021-05-08 21:17:10 +12:00
Garfield69
75e2f22eb7 vsttorrents: new domain 2021-05-08 21:15:37 +12:00
Garfield69
67ab3b8a10 erairaws: add nocensor.space alternate site link 2021-05-08 19:38:50 +12:00
Garfield69
2e7813ecfa mejortorrent: add nocensor.space alternate site link 2021-05-08 19:34:38 +12:00
46 changed files with 1239 additions and 268 deletions

View File

@@ -584,6 +584,27 @@ Using the all indexer has no advantages (besides reduced management overhead), o
To get all Jackett indexers including their capabilities you can use `t=indexers` on the all indexer. To get only configured/unconfigured indexers you can also add `configured=true/false` as a query parameter.
### Filter indexers
Another special "filter" indexer is avaible at <code>/api/v2.0/indexers/<i><b>filter</b></i>/results/torznab</code>
It will query the configured indexers that match the <i><b>filter</b></i> expression criterias and return the combined results as "all".
Supported filters
Filter | Condition
-|-
<code>type:<i><b>type</b></i></code> | where the indexer type is equal to <i><b>type</b></i>
<code>tag:<i><b>tag</b></i></code> | where the indexer tags contains <i><b>tag</b></i>
<code>lang:<i><b>tag</b></i></code> | where the indexer language start with <i><b>lang</b></i>
Supported operators
Operator | Condition
-|-
<code>!<i><b>filter</b></i></code> | where not <i><b>filter</b></i>
<code><i><b>filter1</b></i>+<i><b>filter2</b></i>+...</code> | where <i><b>filter1</b></i> and <i><b>filter2</b> and ...</
<code><i><b>filter1</b></i>,<i><b>filter2</b></i>,...</code> | where <i><b>filter1</b></i> or <i><b>filter2</b> or ...</
Example:
The "filter" indexer at <code>/api/v2.0/indexers/<b>tag:group1,!type:private+lang:en</b>/results/torznab</code> will query all the configured indexers tagged with `group1` or all the indexers not private and with `en` language (`en-en`,`en-us`,...)
## Installation on Windows
We recommend you install Jackett as a Windows service using the supplied installer. You may also download the zipped version if you would like to configure everything manually.

View File

@@ -2,7 +2,7 @@
name: $(majorVersion).$(minorVersion).$(patchVersion)
variables:
majorVersion: 0
minorVersion: 17
minorVersion: 18
patchVersion: $[counter(variables['minorVersion'], 1)] # this will reset when we bump minor
jackettVersion: $(majorVersion).$(minorVersion).$(patchVersion)
buildConfiguration: Release

File diff suppressed because one or more lines are too long

View File

@@ -76,6 +76,10 @@ body {
max-width: 255px;
}
.setup-item-inputtags {
max-width: 255px;
}
[data-type=hiddendata]{
display: none;
}
@@ -328,3 +332,21 @@ input#searchquery {
#proxy-warning {
color: red;
}
.label-tag {
text-transform: lowercase;
background-color: #777;
}
.tagify {
height: auto;
}
.tagify .tagify__input {
min-width: 0;
text-transform: lowercase;
}
.tagify .tagify__tag-text {
text-transform: lowercase;
}

View File

@@ -3,6 +3,8 @@ var basePath = '';
var indexers = [];
var configuredIndexers = [];
var unconfiguredIndexers = [];
var configuredTags = [];
var availableFilters = [];
$.fn.inView = function () {
if (!this.length) return false;
@@ -58,7 +60,7 @@ function openSearchIfNecessary() {
decodeURIComponent(item.split('=')[1].replace(/\+/g, '%20')))
}, prev), {});
if ("search" in hashArgs) {
showSearch(hashArgs.tracker, hashArgs.search, hashArgs.category);
showSearch(hashArgs.filter, hashArgs.tracker, hashArgs.search, hashArgs.category);
}
}
@@ -67,6 +69,14 @@ function insertWordWrap(str) {
return str.replace(/([\.\-_\/\\])/g, "$1\u200B");
}
function type_filter(indexer) {
return indexer.type == this.value;
}
function tag_filter(indexer) {
return indexer.tags.map(t => t.toLowerCase()).indexOf(this.value.toLowerCase()) > -1;
}
function getJackettConfig(callback) {
api.getServerConfig(callback).fail(function () {
doNotify("Error loading Jackett settings, request to Jackett server failed, is server running ?", "danger", "glyphicon glyphicon-alert");
@@ -131,11 +141,14 @@ function loadJackettSettings() {
}
function reloadIndexers() {
$('#filters').hide();
$('#indexers').hide();
api.getAllIndexers(function (data) {
indexers = data;
configuredIndexers = [];
unconfiguredIndexers = [];
configuredTags = [];
availableFilters = [];
for (var i = 0; i < data.length; i++) {
var item = data[i];
item.rss_host = resolveUrl(basePath + "/api/v2.0/indexers/" + item.id + "/results/torznab/api?apikey=" + api.key + "&t=search&cat=&q=");
@@ -169,7 +182,13 @@ function reloadIndexers() {
else
unconfiguredIndexers.push(item);
}
configuredTags = configuredIndexers.map(i => i.tags).reduce((a, g) => a.concat(g), []).filter((v, i, a) => a.indexOf(v) === i);
configureFilters(configuredIndexers);
displayConfiguredIndexersList(configuredIndexers);
$('#indexers div.dataTables_filter input').focusWithoutScrolling();
openSearchIfNecessary();
}).fail(function () {
@@ -177,6 +196,23 @@ function reloadIndexers() {
});
}
function configureFilters(indexers) {
function add(f) {
if (availableFilters.find(x => x.id == f.id))
return;
if (!indexers.every(f.apply, f) && indexers.some(f.apply, f))
availableFilters.push(f);
}
["public", "private", "semi-private"]
.map(t => { return { id: "type:" + t, apply: type_filter, value: t } })
.forEach(add);
configuredTags.sort()
.map(t => { return { id: "tag:" + t.toLowerCase(), apply: tag_filter, value: t }})
.forEach(add);
}
function displayConfiguredIndexersList(indexers) {
var indexersTemplate = Handlebars.compile($("#configured-indexer-table").html());
var indexersTable = $(indexersTemplate({
@@ -484,17 +520,20 @@ function prepareSearchButtons(element) {
var id = $btn.data("id");
$btn.click(function () {
window.location.hash = "search&tracker=" + id;
showSearch(id);
showSearch(null, id);
});
});
}
function prepareSetupButtons(element) {
element.find('.indexer-setup').each(function (i, btn) {
var indexer = configuredIndexers[i];
$(btn).click(function () {
displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks, indexer.description);
});
var $btn = $(btn);
var id = $btn.data("id");
var indexer = configuredIndexers.find(i => i.id === id);
if (indexer)
$btn.click(function () {
displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks, indexer.description);
});
});
}
@@ -610,11 +649,32 @@ function populateConfigItems(configForm, config) {
var item = config[i];
var setupValueTemplate = Handlebars.compile($("#setup-item-" + item.type).html());
item.value_element = setupValueTemplate(item);
var template = setupItemTemplate(item);
var template = $(setupItemTemplate(item));
$formItemContainer.append(template);
setupConfigItem(template, item);
}
}
function setupConfigItem(configItem, item) {
switch (item.type) {
case "inputtags": {
configItem.find("input").tagify({
dropdown: {
enabled: 0,
position: "text"
},
separator: item.separator || ",",
whitelist: item.whitelist || [],
blacklist: item.blacklist || [],
pattern: item.pattern || null,
delimiters: item.delimiters || item.separator || ",",
originalInputValueFormat: function (values) { return values.map(item => item.value.toLowerCase()).join(this.separator); }
});
}
break;
}
}
function newConfigModal(title, config, caps, link, alternativesitelinks, description) {
var configTemplate = Handlebars.compile($("#jackett-config-setup-modal").html());
var configForm = $(configTemplate({
@@ -638,6 +698,8 @@ function newConfigModal(title, config, caps, link, alternativesitelinks, descrip
});
}
$("div[data-id='tags'] input", configForm).data("tagify").settings.whitelist = configuredTags;
return configForm;
}
@@ -668,9 +730,13 @@ function getConfigModalJson(configForm) {
$el.find(".setup-item-inputcheckbox input:checked").each(function () {
itemEntry.values.push($(this).val());
});
break;
case "inputselect":
itemEntry.value = $el.find(".setup-item-inputselect select").val();
break;
case "inputtags":
itemEntry.value = $el.find(".setup-item-inputtags input").val();
break;
}
configJson.push(itemEntry)
});
@@ -802,14 +868,15 @@ function updateReleasesRow(row) {
}
}
function showSearch(selectedIndexer, query, category) {
function showSearch(selectedFilter, selectedIndexer, query, category) {
var selectedIndexers = [];
if (selectedIndexer)
selectedIndexers = selectedIndexer.split(",");
selectedIndexers = selectedIndexer.split(",");
$('#select-indexer-modal').remove();
var releaseTemplate = Handlebars.compile($("#jackett-search").html());
var releaseDialog = $(releaseTemplate({
indexers: configuredIndexers
filters: availableFilters,
active: selectedFilter
}));
$("#modals").append(releaseDialog);
@@ -823,6 +890,29 @@ function showSearch(selectedIndexer, query, category) {
window.location.hash = '';
});
var setTrackers = function (filterId, trackers) {
var select = $('#searchTracker');
var selected = select.val();
var filter = availableFilters.find(f => f.id == filterId);
if (filter)
trackers = trackers.filter(filter.apply,filter);
var options = trackers.map(t => {
return {
label: t.name,
value: t.id
}
});
select.multiselect('dataprovider', options);
select.val(selected).multiselect("refresh");
};
$('#searchFilter').change(jQuery.proxy(function () {
var filterId = $('#searchFilter').val();
setTrackers(filterId, this.items);
}, {
items: configuredIndexers
}));
var setCategories = function (trackers, items) {
var cats = {};
for (var i = 0; i < items.length; i++) {
@@ -869,6 +959,7 @@ function showSearch(selectedIndexer, query, category) {
return;
}
var searchString = releaseDialog.find('#searchquery').val();
var filterId = releaseDialog.find('#searchFilter').val();
var queryObj = {
Query: searchString,
Category: releaseDialog.find('#searchCategory').val(),
@@ -878,14 +969,15 @@ function showSearch(selectedIndexer, query, category) {
window.location.hash = Object.entries({
search: encodeURIComponent(queryObj.Query).replace(/%20/g, '+'),
tracker: queryObj.Tracker.join(","),
category: queryObj.Category.join(",")
}).map(([k, v], i) => k + '=' + v).join('&');
category: queryObj.Category.join(","),
filter: filterId ? encodeURIComponent(filterId) : ""
}).filter(([k, v]) => v).map(([k, v], i) => k + '=' + v).join('&');
$('#jackett-search-perform').html($('#spinner').html());
$('#searchResults div.dataTables_filter input').val("");
clearSearchResultTable($('#searchResults'));
var trackerId = "all";
var trackerId = filterId || "all";
api.resultsForIndexer(trackerId, queryObj, function (data) {
for (var i = 0; i < data.Results.length; i++) {
var item = data.Results[i];
@@ -906,16 +998,14 @@ function showSearch(selectedIndexer, query, category) {
var searchTracker = releaseDialog.find("#searchTracker");
var searchCategory = releaseDialog.find('#searchCategory');
searchCategory.multiselect({
var searchFilter = releaseDialog.find('#searchFilter');
searchFilter.multiselect({
maxHeight: 400,
enableFiltering: true,
includeSelectAllOption: true,
enableCaseInsensitiveFiltering: true,
nonSelectedText: 'Any'
nonSelectedText: 'All'
});
if (selectedIndexers)
searchTracker.val(selectedIndexers);
searchTracker.trigger("change");
updateSearchResultTable($('#searchResults'), []);
clearSearchResultTable($('#searchResults'));
@@ -928,6 +1018,29 @@ function showSearch(selectedIndexer, query, category) {
nonSelectedText: 'All'
});
searchCategory.multiselect({
maxHeight: 400,
enableFiltering: true,
includeSelectAllOption: true,
enableCaseInsensitiveFiltering: true,
nonSelectedText: 'Any'
});
if (availableFilters.length > 0) {
if (selectedFilter) {
searchFilter.val(selectedFilter);
searchFilter.multiselect("refresh");
}
searchFilter.trigger("change");
}
else
setTrackers(selectedFilter, configuredIndexers);
if (selectedIndexers) {
searchTracker.val(selectedIndexers);
searchTracker.multiselect("refresh");
}
searchTracker.trigger("change");
if (category !== undefined) {
searchCategory.val(category.split(","));
@@ -1231,7 +1344,7 @@ function bindUIButtons() {
});
$("#jackett-show-search").click(function () {
showSearch(null);
showSearch();
window.location.hash = "search";
});
@@ -1348,4 +1461,4 @@ function proxyWarning(input) {
} else {
$('#proxy-warning').hide();
}
}
}

View File

@@ -330,3 +330,21 @@ input#searchquery {
#proxy-warning {
color: red;
}
.label-tag {
text-transform: lowercase;
background-color: #777;
}
.tagify {
height: auto;
}
.tagify .tagify__input {
min-width: 0;
text-transform: lowercase;
}
.tagify .tagify__tag-text {
text-transform: lowercase;
}

View File

@@ -22,11 +22,14 @@
<script type="text/javascript" src="../bootstrap/bootstrap.min.js?changed=2017083001"></script>
<script type="text/javascript" src="../libs/bootstrap-notify.js?changed=2017083001"></script>
<script type="text/javascript" src="../libs/bootstrap-multiselect.js?changed=2017083001"></script>
<script type="text/javascript" src="../libs/tagify.min.js?changed=11662"></script>
<script type="text/javascript" src="../libs/jQuery.tagify.min.js?changed=11662"></script>
<link rel="stylesheet" type="text/css" href="../bootstrap/bootstrap.min.css?changed=2017083001">
<link rel="stylesheet" type="text/css" href="../animate.css?changed=2017083001">
<link rel="stylesheet" type="text/css" href="../custom.css?changed=20201208" media="only screen and (min-device-width: 480px)">
<link rel="stylesheet" type="text/css" href="../custom_mobile.css?changed=20201208" media="only screen and (max-device-width: 480px)">
<link rel="stylesheet" type="text/css" href="../css/tagify.css?changed=11662">
<link rel="stylesheet" type="text/css" href="../custom.css?changed=11662" media="only screen and (min-device-width: 480px)">
<link rel="stylesheet" type="text/css" href="../custom_mobile.css?changed=11662" media="only screen and (max-device-width: 480px)">
<link rel="stylesheet" type="text/css" href="../css/jquery.dataTables.min.css?changed=2017083001">
<link rel="stylesheet" type="text/css" href="../css/bootstrap-multiselect.css?changed=2017083001" />
<link rel="stylesheet" type="text/css" href="../css/font-awesome.min.css?changed=2017083001">
@@ -65,7 +68,7 @@
</button>
</div>
<h3>Configured Indexers</h3>
<div id="indexers"> </div>
<div id="indexers"></div>
<hr />
<div class="input-area">
@@ -213,7 +216,7 @@
<div id="modals"></div>
<script id="setup-item" type="text/x-handlebars-template">
<div class="setup-item form-group" data-id="{{id}}" data-value="{{value}}" data-type="{{type}}">
<div class="setup-item form-filter" data-id="{{id}}" data-value="{{value}}" data-type="{{type}}">
<div class="setup-item-label">{{name}}</div>
<div class="setup-item-value">{{{value_element}}}</div>
</div>
@@ -289,10 +292,14 @@
Click on an URL to copy it to the Site Link field.
</div>
</script>
<script id="setup-item-inputtags" type="text/x-handlebars-template">
<div class="setup-item-inputtags">
<input class="form-control input-sm" type="text" value="{{{value}}}" {{#if pattern}} pattern="{{pattern}}"{{/if}}/>
</div>
</script>
<script id="configured-indexer-table" type="text/x-handlebars-template">
<div class="configured-indexer-div">
<div class="tab-content configured-indexer-div">
<table id="configured-indexer-datatable" class="indexer-table dataTable compact cell-border hover stripe table table-responsive">
<thead>
<tr>
@@ -303,7 +310,7 @@
<tbody>
{{#each indexers}}
<tr class="configured-indexer-row">
<td><a target="_blank" href="{{site_link}}" title="{{description}}">{{name}}</a> <span title="{{type}}" class="label label-{{type_label}}" style="text-transform: capitalize;">{{type}}</span></td>
<td><a target="_blank" href="{{site_link}}" title="{{description}}">{{name}}</a> <span title="{{type}}" class="label label-{{type_label}}" style="text-transform: capitalize;">{{type}}</span>{{#each tags}} <span title="{{this}}" class="label label-tag">{{this}}</span>{{/each}}</td>
<td class="fit">
<div class="indexer-buttons">
<a href="{{rss_host}}" title="{{rss_host}}" role="button" class="indexer-button-copy btn btn-xs btn-info">Copy RSS Feed</i></a>
@@ -492,12 +499,17 @@
<p>You can search all configured indexers from this screen.</p>
<label for="text">Query</label>
<input class="form-control" type="text" name="query" id="searchquery" />
<label for="tracker">Tracker</label>
<select name="tracker" id="searchTracker" multiple="multiple">
{{#each indexers}}
<option value="{{id}}" selected>{{name}}</option>
{{/each}}
{{#if filters}}
<label for="filter">Filter</label>
<select name="filter" id="searchFilter">
<option value="all">all</option>
{{#each filters}}
<option value="{{id}}">{{id}}</option>
{{/each}}
</select>
{{/if}}
<label for="tracker">Tracker</label>
<select name="tracker" id="searchTracker" multiple="multiple"></select>
<label for="category">Category</label>
<select name="category" id="searchCategory" multiple="multiple"></select>
<button id="jackett-search-perform" class="btn btn-success btn-sm"><span class="fa fa-search"></span></button>
@@ -698,6 +710,6 @@
</script>
<script type="text/javascript" src="../libs/api.js?changed=2017083001"></script>
<script type="text/javascript" src="../custom.js?changed=20210424"></script>
<script type="text/javascript" src="../custom.js?changed=11662"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -317,8 +317,6 @@ search:
text: 1
leechers:
text: 1
grabs:
selector: li:has(img[alt="Скачиваний"])
size:
selector: div.shor_subtitles span:nth-child(2)
filters:

View File

@@ -1,39 +1,13 @@
---
# Update by LA5T based on the orignial 'cinematik.yml'
# 29.07.2018 22:53 UTC+2
#
id: cinematik
name: Cinematik
description: "Tracker for non-hollywood movies."
description: "A tracker for full BD and DVD discs of non-mainstream movies, niche cinema and arthouse."
language: en-us
type: private
encoding: UTF-8
links:
- https://www.cinematik.net/
settings:
- name: username
type: text
label: Username
- name: password
type: password
label: Password
- name: incldead
type: select
label: Status
default: 1
options:
0: Active
1: "Active and Inactive"
2: Inactive
- name: srchdtls
type: checkbox
label: "Detailed search"
- name: info_results
type: info
label: "Search results"
default: "You can increase the number of search results in your profile.<br />Default is 15."
caps:
categorymappings:
- {id: 1, cat: Movies, desc: "Comedy"}
@@ -59,9 +33,34 @@ caps:
search: [q]
movie-search: [q]
settings:
- name: username
type: text
label: Username
- name: password
type: password
label: Password
- name: incldead
type: select
label: Status
default: 1
options:
0: Active
1: "Active and Inactive"
2: Inactive
- name: srchdtls
type: checkbox
label: "Detailed search"
- name: info_results
type: info
label: "Search results"
default: "You can increase the number of search results in your profile.<br>Default is 15."
login:
path: takelogin.php
method: post
method: form
path: login.php
submitpath: takelogin.php
form: form[action="takelogin.php"]
inputs:
username: "{{ .Config.username }}"
password: "{{ .Config.password }}"
@@ -75,34 +74,36 @@ search:
- path: browse.php
inputs:
$raw: "{{ range .Categories }}c{{.}}=1&{{end}}"
search: "{{ .Query.Keywords }}"
search: "{{ .Keywords }}"
incldead: "{{ .Config.incldead }}"
srchdtls: "{{ if .Config.srchdtls }}1{{ else }}0{{ end }}"
rows:
selector: table[border="1"] tr:not(:first-child)
fields:
category:
text: 1
title:
selector: td:nth-child(2) a
details:
selector: a[href^="details.php?id="]
attribute: href
download:
selector: a[href^="details.php?id="]
attribute: href
filters:
- name: replace
args: ["details.php?id=", "download.php?id="]
details:
selector: a[href^="details.php?id="]
attribute: href
files:
selector: td:nth-child(5)
size:
selector: td:nth-child(7)
grabs:
selector: td:nth-child(8)
filters:
- name: regexp
args: ([\d,]+)
files:
selector: td:nth-child(5)
size:
selector: td:nth-child(7)
seeders:
selector: td:nth-child(9)
leechers:
@@ -111,11 +112,12 @@ search:
selector: td:nth-child(11) div.addedtor
downloadvolumefactor:
case:
"img[title=\"Golden Torrent: No Download Stats are Recorded\"]": 0
"img[title=\"Silver Torrent: Download Stats are 25% Recorded\"]": 0.25
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": 0
img[title^="Golden Torrent"]: 0
img[title^="Silver Torrent"]: 0.25
img[title^="Platinum Torrent"]: 0
"*": 1
uploadvolumefactor:
case:
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": 2
img[title^="Platinum Torrent"]: 2
"*": 1
# Engine n/a

View File

@@ -242,11 +242,23 @@ search:
- name: regexp
args: "src=(.*?)><"
grabs:
selector: td:nth-last-child(4)
selector: a[onmouseover][href^="torrents-details.php?id="]
attribute: onmouseover
filters:
- name: regexp
args: "Completé : </b>(\\d+)<"
seeders:
selector: td:nth-last-child(3)
selector: a[onmouseover][href^="torrents-details.php?id="]
attribute: onmouseover
filters:
- name: regexp
args: "=green>(\\d+)<"
leechers:
selector: td:nth-last-child(2)
selector: a[onmouseover][href^="torrents-details.php?id="]
attribute: onmouseover
filters:
- name: regexp
args: "=red>(\\d+)<"
size:
selector: a[onmouseover][href^="torrents-details.php?id="]
attribute: onmouseover

View File

@@ -136,9 +136,9 @@ search:
- path: selection.php
inputs:
$raw: "{{ range .Categories }}c{{.}}=1&{{end}}"
search: "{{ .Keywords }}}"
search: "{{ .Keywords }}"
# 0 name, 1 descr, 2 both
blah: "0"
blah: 0
orderby: "{{ .Config.sort }}"
sort: "{{ .Config.type }}"

View File

@@ -8,10 +8,10 @@ encoding: windows-1251
links:
- https://hdhouse.club/
- https://hdreactor.club/
- https://hdreactor.su/
legacylinks:
- https://hdreactor.guru/
- https://hdreactor.net/
- https://hdreactor.su/
caps:
categorymappings:

View File

@@ -122,15 +122,11 @@ login:
method: form
form: form[action^="login"]
inputs:
uid: "{{ .Config.username }}"
pwd: "{{ .Config.password }}"
rememberme: ""
captcha:
type: image
selector: img[src^="access_code/"]
input: private_key
username: "{{ .Config.username }}"
password: "{{ .Config.password }}"
loginButton: X
error:
- selector: tr td span[style="color:#FF0000;"]
- selector: "p[style=\"color: #B73C38\"]"
test:
path: index.php
selector: a[href="logout.php"]

View File

@@ -7,7 +7,7 @@ type: public
encoding: UTF-8
followredirect: true
links:
- https://torrentqq85.com/
- https://torrentqq86.com/
legacylinks:
- https://torrentqq76.com/
- https://torrentqq77.com/
@@ -18,6 +18,7 @@ legacylinks:
- https://torrentqq82.com/
- https://torrentqq83.com/
- https://torrentqq84.com/
- https://torrentqq85.com/
caps:
categorymappings:

View File

@@ -7,6 +7,8 @@ type: semi-private
encoding: UTF-8
links:
- https://torrents-local.xyz/
certificates:
- 85f31ff50a1ee0dbc738a12015efe9ef8708505b # expired 5th May 2021
caps:
categorymappings:

View File

@@ -7,7 +7,7 @@ type: public
encoding: UTF-8
followredirect: true
links:
- https://torrentview33.com/
- https://torrentview34.com/
legacylinks:
- https://torrentview.net/
- https://torrentview3.net/
@@ -40,6 +40,7 @@ legacylinks:
- https://torrentview30.com/
- https://torrentview31.com/
- https://torrentview32.com/
- https://torrentview33.com/
caps:
categorymappings:

View File

@@ -7,10 +7,11 @@ type: public
encoding: UTF-8
followredirect: true
links:
- https://torrentwiz24.me/
- https://torrentwiz25.me/
legacylinks:
- https://torrentwiz22.me/
- https://torrentwiz23.me/
- https://torrentwiz24.me/
caps:
categorymappings:

View File

@@ -65,13 +65,14 @@ caps:
book-search: [q]
settings:
- name: username
- name: cookie
type: text
label: Username
- name: password
type: password
label: Password
- name: info
label: Cookie
- name: info_cookie
type: info
label: How to get the Cookie
default: "<ol><li>Login to this tracker with your browser<li>Open the <b>DevTools</b> panel by pressing <b>F12</b><li>Select the <b>Network</b> tab<li>Click on the <b>Doc</b> button (Chrome Browser) or <b>HTML</b> button (FireFox)<li>Refresh the page by pressing <b>F5</b><li>Click on the first row entry<li>Select the <b>Headers</b> tab on the Right panel<li>Find <b>'cookie:'</b> in the <b>Request Headers</b> section<li><b>Select</b> and <b>Copy</b> the whole cookie string <i>(everything after 'cookie: ')</i> and <b>Paste</b> here.</ol>"
- name: info_profile
type: info
label: Layout
default: "<ol><li>Only the English Classic profile is supported.<li>Make sure to set the <b>Torrent Listing (Listeleme Biçimi)</b> option in your profile to <b>Classic (Klasik)</b><li>And set the <b>Language (Dil)</b> to <b>English</b><li>Using the <i>Modern</i> theme will prevent results, and using <i>Turkish</i> will prevent upload dates.</ol>"
@@ -96,24 +97,9 @@ settings:
asc: asc
login:
path: ?p=home&pid=1
method: form
form: form#loginbox_form
submitpath: ajax/login.php
method: cookie
inputs:
action: login
loginbox_membername: "{{ .Config.username }}"
loginbox_password: "{{ .Config.password }}"
loginbox_remember: 1
selectorinputs:
securitytoken:
selector: "script:contains(\"stKey: \")"
filters:
- name: regexp
args: "stKey: \"(.+?)\","
error:
- selector: div.error
- selector: :contains("-ERROR-")
cookie: "{{ .Config.cookie }}"
test:
path: ?p=home&pid=1
selector: div#member_info_bar

View File

@@ -6,6 +6,8 @@ language: en-us
type: public
encoding: UTF-8
links:
- https://looptorrent.net/
legacylinks:
- https://vsttorrents.net/
caps:

View File

@@ -34,6 +34,8 @@ namespace Jackett.Common.Indexers
public Encoding Encoding { get; protected set; }
public virtual bool IsConfigured { get; protected set; }
public virtual string[] Tags { get; protected set; }
protected Logger logger;
protected IIndexerConfigurationService configurationService;
protected IProtectionService protectionService;
@@ -148,6 +150,8 @@ namespace Jackett.Common.Indexers
// check whether the site link is well-formatted
var siteUri = new Uri(configData.SiteLink.Value);
SiteLink = configData.SiteLink.Value;
Tags = configData.Tags.Values.Select(t => t.ToLowerInvariant()).ToArray();
}
public void LoadFromSavedConfiguration(JToken jsonConfig)

View File

@@ -24,6 +24,11 @@ namespace Jackett.Common.Indexers
{ "540p", 350 }
};
public override string[] AlternativeSiteLinks { get; protected set; } = {
"https://www.erai-raws.info/",
"https://erairaws.nocensor.space/"
};
public EraiRaws(IIndexerConfigurationService configService, Utils.Clients.WebClient wc, Logger l,
IProtectionService ps, ICacheService cs)
: base(id: "erai-raws",
@@ -50,14 +55,14 @@ namespace Jackett.Common.Indexers
// Add note that download stats are not available
configData.AddDynamic(
"download-stats-unavailable",
"download-stats-unavailable",
new DisplayInfoConfigurationItem("", "<p>Please note that the following stats are not available for this indexer. Default values are used instead. </p><ul><li>Size</li><li>Seeders</li><li>Leechers</li><li>Download Factor</li><li>Upload Factor</li></ul>")
);
// Config item for title detail parsing
configData.AddDynamic("title-detail-parsing", new BoolConfigurationItem("Enable Title Detail Parsing"));
configData.AddDynamic(
"title-detail-parsing-help",
"title-detail-parsing-help",
new DisplayInfoConfigurationItem("", "Title Detail Parsing will attempt to determine the season and episode number from the release names and reformat them as a suffix in the format S1E1. If successful, this should provide better matching in applications such as Sonarr.")
);
@@ -87,7 +92,7 @@ namespace Jackett.Common.Indexers
return IndexerConfigurationStatus.Completed;
}
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var feedItems = await GetItemsFromFeed();
@@ -231,7 +236,7 @@ namespace Jackett.Common.Indexers
var title = rssItem.SelectSingleNode("title")?.InnerText;
var link = rssItem.SelectSingleNode("link")?.InnerText;
var publishDate = rssItem.SelectSingleNode("pubDate")?.InnerText;
if (string.IsNullOrWhiteSpace(title) ||
string.IsNullOrWhiteSpace(link) ||
string.IsNullOrWhiteSpace(publishDate))
@@ -322,7 +327,7 @@ namespace Jackett.Common.Indexers
{ "episode", DETAIL_SEARCH_EPISODE },
{ "season", DETAIL_SEARCH_SEASON }
});
var seasonEpisodeIdentifier = string.Concat(
PrefixOrDefault("S", results.details["season"]).Trim(),
PrefixOrDefault("E", results.details["episode"]).Trim()
@@ -340,7 +345,7 @@ namespace Jackett.Common.Indexers
results.strippedTitle.Substring(strangeHyphenPosition + 1).Trim()
).Trim();
}
return string.Concat(
results.strippedTitle.Trim(),
" ",

View File

@@ -40,6 +40,8 @@ namespace Jackett.Common.Indexers
// Whether this indexer has been configured, verified and saved in the past and has the settings required for functioning
bool IsConfigured { get; }
string[] Tags { get; }
// Retrieved for starting setup for the indexer via web API
Task<ConfigurationData> GetConfigurationForSetup();

View File

@@ -35,6 +35,11 @@ namespace Jackett.Common.Indexers
private const string NewTorrentsUrl = "secciones.php?sec=ultimos_torrents";
private const string SearchUrl = "secciones.php";
public override string[] AlternativeSiteLinks { get; protected set; } = {
"https://www.mejortorrento.com/",
"https://mejortorrent.nocensor.space/"
};
public override string[] LegacySiteLinks { get; protected set; } = {
"https://www.mejortorrentt.net/",
"http://www.mejortorrent.org/",

View File

@@ -67,7 +67,7 @@ namespace Jackett.Common.Indexers.Meta
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var indexers = validIndexers;
var indexers = ValidIndexers;
IEnumerable<Task<IndexerResult>> supportedTasks = indexers.Where(i => i.CanHandleQuery(query)).Select(i => i.ResultsForQuery(query, true)).ToList(); // explicit conversion to List to execute LINQ query
var fallbackStrategies = fallbackStrategyProvider.FallbackStrategiesForQuery(query);
@@ -109,11 +109,13 @@ namespace Jackett.Common.Indexers.Meta
return result;
}
public override TorznabCapabilities TorznabCaps => validIndexers.Select(i => i.TorznabCaps).Aggregate(new TorznabCapabilities(), TorznabCapabilities.Concat);
public override TorznabCapabilities TorznabCaps => ValidIndexers.Select(i => i.TorznabCaps).Aggregate(new TorznabCapabilities(), TorznabCapabilities.Concat);
public override bool IsConfigured => Indexers != null;
private IEnumerable<IIndexer> validIndexers => Indexers?.Where(i => i.IsConfigured && filterFunc(i));
public override string[] Tags => Array.Empty<string>();
public IEnumerable<IIndexer> ValidIndexers => Indexers?.Where(i => i.IsConfigured && filterFunc(i));
public IEnumerable<IIndexer> Indexers;

View File

@@ -1,7 +1,10 @@
using System;
using System.Linq;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using NLog;
namespace Jackett.Common.Indexers.Meta
@@ -37,4 +40,41 @@ namespace Jackett.Common.Indexers.Meta
}
}
}
public class FilterIndexer : BaseMetaIndexer
{
public FilterIndexer(string filter, IFallbackStrategyProvider fallbackStrategyProvider,
IResultFilterProvider resultFilterProvider, IIndexerConfigurationService configService,
WebClient client, Logger logger, IProtectionService ps, ICacheService cs, Func<IIndexer, bool> filterFunc)
: base(id: filter,
name: filter,
description: "This feed includes all configured trackers filter by "+filter,
configService: configService,
client: client,
logger: logger,
ps: ps,
cs: cs,
configData: new ConfigurationData(),
fallbackStrategyProvider: fallbackStrategyProvider,
resultFilterProvider: resultFilterProvider,
filter: filterFunc
)
{
}
public override TorznabCapabilities TorznabCaps
{
get
{
// increase the limits (workaround until proper paging is supported, issue #1661)
var caps = base.TorznabCaps;
caps.LimitsMax = caps.LimitsDefault = 1000;
return caps;
}
}
public override bool IsConfigured => base.IsConfigured && (ValidIndexers?.Any() ?? false);
public override void SaveConfig() { }
}
}

View File

@@ -29,6 +29,7 @@ namespace Jackett.Common.Indexers
private string WebRequestDelay => ((SingleSelectConfigurationItem)configData.GetDynamic("webRequestDelay")).Value;
private int MaxPages => Convert.ToInt32(((SingleSelectConfigurationItem)configData.GetDynamic("maxPages")).Value);
private bool MaxPagesBypassForTMDB => ((BoolConfigurationItem)configData.GetDynamic("maxPagesBypassForTMDB")).Value;
private int DropCategories => Convert.ToInt32(((SingleSelectConfigurationItem)configData.GetDynamic("dropCategories")).Value);
private string MultiReplacement => ((StringConfigurationItem)configData.GetDynamic("multiReplacement")).Value;
private bool SubReplacement => ((BoolConfigurationItem)configData.GetDynamic("subReplacement")).Value;
private bool EnhancedAnimeSearch => ((BoolConfigurationItem)configData.GetDynamic("enhancedAnimeSearch")).Value;
@@ -208,7 +209,7 @@ namespace Jackett.Common.Indexers
{ Value = "0" };
ConfigData.AddDynamic("specificLanguageAccent", ConfigSpecificLanguageAccent);
ConfigData.AddDynamic("advancedConfigurationWarning", new DisplayInfoConfigurationItem(string.Empty, "<center><b>Advanced Configuration</b></center>,<br /><br /> <center><b><u>WARNING !</u></b> <i>Be sure to read instructions before editing options bellow, you can <b>drastically reduce performance</b> of queries or have <b>non-accurate results</b>.</i></center><br/><br/><ul><li><b>Delay betwwen Requests</b>: (<i>not recommended</i>) you can increase delay to requests made to the tracker, but a minimum of 2.1s is enforced as there is an anti-spam protection.</li><br /><li><b>Max Pages</b>: (<i>not recommended</i>) you can increase max pages to follow when making a request. But be aware that others apps can consider this indexer not working if jackett take too many times to return results. Another thing is that API is very buggy on tracker side, most of time, results of next pages are same ... as the first page. Even if we deduplicate rows, you will loose performance for the same results. You can check logs to see if an higher pages following is not benefical, you will see an error percentage (duplicates) with recommandations.</li><br /><li><b>Bypass for TMDB</b>: (<i>recommended</i>) this indexer is compatible with TMDB queries (<i>for movies only</i>), so when requesting content with an TMDB ID, we will search directly ID on API instead of name. Results will be more accurate, so you can enable a max pages bypass for this query type. You will be at least limited by the hard limit of 4 pages.</li><br /><li><b>Enhanced Anime</b>: if you have \"Anime\", this will improve queries made to this tracker related to this type when making searches.</li><br /><li><b>Multi Replacement</b>: you can dynamically replace the word \"MULTI\" with another of your choice like \"MULTI.FRENCH\" for better analysis of 3rd party softwares.</li><li><b>Sub Replacement</b>: you can dynamically replace the word \"VOSTFR\" or \"SUBFRENCH\" with the word \"ENGLISH\" for better analysis of 3rd party softwares.</li></ul>"));
ConfigData.AddDynamic("advancedConfigurationWarning", new DisplayInfoConfigurationItem(string.Empty, "<center><b>Advanced Configuration</b></center>,<br /><br /> <center><b><u>WARNING !</u></b> <i>Be sure to read instructions before editing options bellow, you can <b>drastically reduce performance</b> of queries or have <b>non-accurate results</b>.</i></center><br/><br/><ul><li><b>Delay betwwen Requests</b>: (<i>not recommended</i>) you can increase delay to requests made to the tracker, but a minimum of 2.1s is enforced as there is an anti-spam protection.</li><br /><li><b>Max Pages</b>: (<i>not recommended</i>) you can increase max pages to follow when making a request. But be aware that others apps can consider this indexer not working if jackett take too many times to return results. Another thing is that API is very buggy on tracker side, most of time, results of next pages are same ... as the first page. Even if we deduplicate rows, you will loose performance for the same results. You can check logs to see if an higher pages following is not benefical, you will see an error percentage (duplicates) with recommandations.</li><br /><li><b>Bypass for TMDB</b>: (<i>recommended</i>) this indexer is compatible with TMDB queries (<i>for movies only</i>), so when requesting content with an TMDB ID, we will search directly ID on API instead of name. Results will be more accurate, so you can enable a max pages bypass for this query type. You will be at least limited by the hard limit of 4 pages.</li><br /><li><b>Drop categories</b>: (<i>recommended</i>) this indexer has some problems when too many categories are requested for filtering, so you will have better results by dropping categories from TMDB queries or selecting fewer categories in 3rd apps.</li><br /><li><b>Enhanced Anime</b>: if you have \"Anime\", this will improve queries made to this tracker related to this type when making searches.</li><br /><li><b>Multi Replacement</b>: you can dynamically replace the word \"MULTI\" with another of your choice like \"MULTI.FRENCH\" for better analysis of 3rd party softwares.</li><br /><li><b>Sub Replacement</b>: you can dynamically replace the word \"VOSTFR\" or \"SUBFRENCH\" with the word \"ENGLISH\" for better analysis of 3rd party softwares.</li></ul>"));
var ConfigWebRequestDelay = new SingleSelectConfigurationItem("Which delay do you want to apply between each requests made to tracker ?", new Dictionary<string, string>
{
@@ -235,6 +236,15 @@ namespace Jackett.Common.Indexers
var ConfigMaxPagesBypassForTMDB = new BoolConfigurationItem("Do you want to bypass max pages for TMDB searches ? (Radarr) - Hard limit of 4") { Value = true };
ConfigData.AddDynamic("maxPagesBypassForTMDB", ConfigMaxPagesBypassForTMDB);
var ConfigDropCategories = new SingleSelectConfigurationItem("Drop requested categories", new Dictionary<string, string>
{
{"0", "Disabled"},
{"1", "Yes, only for TMDB requests (default)"},
{"2", "Yes, for all requests"},
})
{ Value = "1" };
ConfigData.AddDynamic("dropCategories", ConfigDropCategories);
var ConfigEnhancedAnimeSearch = new BoolConfigurationItem("Do you want to use enhanced ANIME search ?") { Value = false };
ConfigData.AddDynamic("enhancedAnimeSearch", ConfigEnhancedAnimeSearch);
@@ -523,18 +533,26 @@ namespace Jackett.Common.Indexers
logger.Info("\nXthor - Search requested for movie with title \"" + term + "\"");
parameters.Add("search", WebUtility.UrlEncode(term));
}
else
{
logger.Info("\nXthor - Global search requested without term");
parameters.Add("search", string.Empty);
// Showing all torrents
}
}
// Loop on Categories needed
// Loop on categories needed
if (categoriesList.Count > 0)
{
parameters.Add("category", string.Join("+", categoriesList));
switch (DropCategories)
{
case 1:
// Drop categories for TMDB query only.
if (!query.IsTmdbQuery)
{ goto default; }
break;
case 2:
// Drop categories enabled for all requests
break;
default:
// Default or disabled state (0 value of config switch)
parameters.Add("category", string.Join("+", categoriesList));
break;
}
}
// If Only Freeleech Enabled

View File

@@ -58,6 +58,9 @@
<Content Include="Content\css\jquery.dataTables.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\css\tagify.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\custom.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -124,9 +127,15 @@
<Content Include="Content\libs\jquery.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\libs\jQuery.tagify.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\libs\moment.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\libs\tagify.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\login.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@@ -34,6 +34,8 @@ namespace Jackett.Common.Models.DTO
[DataMember]
public string language { get; private set; }
[DataMember]
public IEnumerable<string> tags { get; private set; }
[DataMember]
public string last_error { get; private set; }
[DataMember]
public bool potatoenabled { get; private set; }
@@ -55,6 +57,8 @@ namespace Jackett.Common.Models.DTO
alternativesitelinks = indexer.AlternativeSiteLinks;
tags = indexer.Tags;
caps = indexer.TorznabCaps.Categories.GetTorznabCategoryList(true)
.Select(c => new Capability
{

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Newtonsoft.Json.Linq;
namespace Jackett.Common.Models.IndexerConfig
@@ -12,9 +14,10 @@ namespace Jackett.Common.Models.IndexerConfig
private const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||";
protected Dictionary<string, ConfigurationItem> dynamics = new Dictionary<string, ConfigurationItem>(); // list for dynamic items
public HiddenStringConfigurationItem CookieHeader { get; private set; } = new HiddenStringConfigurationItem(name:"CookieHeader");
public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name:"LastError");
public StringConfigurationItem SiteLink { get; private set; } = new StringConfigurationItem(name:"Site Link");
public HiddenStringConfigurationItem CookieHeader { get; private set; } = new HiddenStringConfigurationItem(name: "CookieHeader");
public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name: "LastError");
public StringConfigurationItem SiteLink { get; private set; } = new StringConfigurationItem(name: "Site Link");
public TagsConfigurationItem Tags { get; private set; } = new TagsConfigurationItem(name: "Tags", charSet:"A-Za-z0-9\\-\\._~");
public ConfigurationData()
{
@@ -36,67 +39,10 @@ namespace Jackett.Common.Models.IndexerConfig
var jsonToken = jsonArray.FirstOrDefault(f => f.Value<string>("id") == item.ID);
if (jsonToken == null)
continue;
switch (item)
{
case StringConfigurationItem stringItem:
{
if (HasPasswordValue(item))
{
var pw = ReadValueAs<string>(jsonToken);
if (pw != PASSWORD_REPLACEMENT)
{
stringItem.Value = ps != null ? ps.UnProtect(pw) : pw;
}
}
else
{
stringItem.Value = ReadValueAs<string>(jsonToken);
}
break;
}
case HiddenStringConfigurationItem hiddenStringItem:
{
hiddenStringItem.Value = ReadValueAs<string>(jsonToken);
break;
}
case BoolConfigurationItem boolItem:
{
boolItem.Value = ReadValueAs<bool>(jsonToken);
break;
}
case SingleSelectConfigurationItem singleSelectItem:
{
singleSelectItem.Value = ReadValueAs<string>(jsonToken);
break;
}
case MultiSelectConfigurationItem multiSelectItem:
{
var values = jsonToken.Value<JArray>("values");
if (values != null)
{
multiSelectItem.Values = values.Values<string>().ToArray();
}
break;
}
case PasswordConfigurationItem passwordItem:
{
var pw = ReadValueAs<string>(jsonToken);
if (pw != PASSWORD_REPLACEMENT)
{
passwordItem.Value = ps != null ? ps.UnProtect(pw) : pw;
}
break;
}
}
item.FromJson(jsonToken, ps);
}
}
private T ReadValueAs<T>(JToken jToken) => jToken.Value<T>("value");
private bool HasPasswordValue(ConfigurationItem item)
=> string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase);
public JToken ToJson(IProtectionService ps, bool forDisplay = true)
{
var jArray = new JArray();
@@ -104,43 +50,7 @@ namespace Jackett.Common.Models.IndexerConfig
var configurationItems = GetConfigurationItems(forDisplay);
foreach (var configurationItem in configurationItems)
{
JObject jObject = null;
switch (configurationItem)
{
case ConfigurationItemMaybePassword maybePassword:
{
// Remove this code and give each derived ConfigurationItem class its own ToJson method
// as soon as everyone is using PasswordConfigurationItem for passwords.
jObject = maybePassword.ToJson(ps);
break;
}
case BoolConfigurationItem boolItem:
{
jObject = boolItem.ToJson();
break;
}
case SingleSelectConfigurationItem singleSelectItem:
{
jObject = singleSelectItem.ToJson();
break;
}
case MultiSelectConfigurationItem multiSelectItem:
{
jObject = multiSelectItem.ToJson();
break;
}
case DisplayImageConfigurationItem imageItem:
{
jObject = imageItem.ToJson();
break;
}
case PasswordConfigurationItem passwordItem:
{
jObject = passwordItem.ToJson(forDisplay, ps);
break;
}
}
var jObject = configurationItem.ToJson(ps, forDisplay);
if (jObject != null)
{
@@ -163,8 +73,13 @@ namespace Jackett.Common.Models.IndexerConfig
properties.Remove(SiteLink);
properties.Insert(0, SiteLink);
// remove/insert Tags manualy to make sure it shows up last
properties.Remove(Tags);
properties.AddRange(dynamics.Values);
properties.Add(Tags);
return properties;
}
@@ -204,6 +119,14 @@ namespace Jackett.Common.Models.IndexerConfig
["name"] = Name
};
}
protected static T ReadValueAs<T>(JToken jToken) => jToken.Value<T>("value");
protected static bool HasPasswordValue(ConfigurationItem item)
=> string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase);
public virtual JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true) => null;
public virtual void FromJson(JToken jsonToken, IProtectionService protectionService = null) { }
}
/// <summary>
@@ -218,7 +141,7 @@ namespace Jackett.Common.Models.IndexerConfig
{
}
public JObject ToJson(IProtectionService protectionService = null)
public override JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true)
{
var jObject = CreateJObject();
@@ -245,6 +168,22 @@ namespace Jackett.Common.Models.IndexerConfig
: base(name, itemType: "inputstring")
{
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
if (HasPasswordValue(this))
{
var pw = ReadValueAs<string>(jsonToken);
if (pw != PASSWORD_REPLACEMENT)
{
Value = ps != null ? ps.UnProtect(pw) : pw;
}
}
else
{
Value = ReadValueAs<string>(jsonToken);
}
}
}
public class HiddenStringConfigurationItem : ConfigurationItemMaybePassword
@@ -253,6 +192,11 @@ namespace Jackett.Common.Models.IndexerConfig
: base(name, itemType: "hiddendata", canBeShownToUser: false)
{
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
Value = ReadValueAs<string>(jsonToken);
}
}
public class DisplayInfoConfigurationItem : ConfigurationItemMaybePassword
@@ -273,12 +217,17 @@ namespace Jackett.Common.Models.IndexerConfig
{
}
public JObject ToJson()
public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true)
{
var jObject = CreateJObject();
jObject["value"] = Value;
return jObject;
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
Value = ReadValueAs<bool>(jsonToken);
}
}
public class DisplayImageConfigurationItem : ConfigurationItem
@@ -290,7 +239,7 @@ namespace Jackett.Common.Models.IndexerConfig
{
}
public JObject ToJson()
public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true)
{
var jObject = CreateJObject();
@@ -310,7 +259,7 @@ namespace Jackett.Common.Models.IndexerConfig
public SingleSelectConfigurationItem(string name, Dictionary<string, string> options)
: base(name, itemType: "inputselect") => Options = options;
public JObject ToJson()
public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true)
{
var jObject = CreateJObject();
@@ -323,6 +272,11 @@ namespace Jackett.Common.Models.IndexerConfig
return jObject;
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
Value = ReadValueAs<string>(jsonToken);
}
}
public class MultiSelectConfigurationItem : ConfigurationItem
@@ -334,7 +288,7 @@ namespace Jackett.Common.Models.IndexerConfig
public MultiSelectConfigurationItem(string name, Dictionary<string, string> options)
: base(name, itemType: "inputcheckbox") => Options = options;
public JObject ToJson()
public override JObject ToJson(IProtectionService ps, bool forDisplay)
{
var jObject = CreateJObject();
@@ -347,6 +301,15 @@ namespace Jackett.Common.Models.IndexerConfig
return jObject;
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
var values = jsonToken.Value<JArray>("values");
if (values != null)
{
Values = values.Values<string>().ToArray();
}
}
}
public class PasswordConfigurationItem : ConfigurationItem
@@ -358,7 +321,7 @@ namespace Jackett.Common.Models.IndexerConfig
{
}
public JObject ToJson(bool forDisplay, IProtectionService protectionService = null)
public override JObject ToJson(IProtectionService protectionService = null, bool forDisplay = true)
{
var jObject = CreateJObject();
@@ -373,6 +336,76 @@ namespace Jackett.Common.Models.IndexerConfig
return jObject;
}
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
{
var pw = ReadValueAs<string>(jsonToken);
if (pw != PASSWORD_REPLACEMENT)
{
Value = ps != null ? ps.UnProtect(pw) : pw;
}
}
}
public class TagsConfigurationItem : ConfigurationItem
{
public HashSet<string> Values { get; }
public string Pattern { get; set; }
public char Separator { get; set; }
public string Delimiters { get; set; }
public HashSet<string> Whitelist { get; }
public HashSet<string> Blacklist { get; }
public TagsConfigurationItem(string name, string charSet = null, char separator = ',')
: base(name, "inputtags")
{
Values = new HashSet<string>();
Whitelist = new HashSet<string>();
Blacklist = new HashSet<string>();
if (!string.IsNullOrWhiteSpace(charSet))
{
Pattern = $"^[{charSet}]+$";
Delimiters = $"[^{charSet}]+";
}
Separator = separator;
}
public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true)
{
var jObject = CreateJObject();
var separator = Separator.ToString();
jObject["value"] = string.Join(separator, Values);
if (forDisplay)
{
jObject["separator"] = separator;
if (!string.IsNullOrWhiteSpace(Delimiters))
jObject["delimiters"] = Delimiters;
if (!string.IsNullOrWhiteSpace(Pattern))
jObject["pattern"] = Pattern;
if (Whitelist.Count > 0)
jObject["whitelist"] = string.Join(separator, Whitelist);
if (Blacklist.Count > 0)
jObject["blacklist"] = string.Join(separator, Blacklist);
}
return jObject;
}
public override void FromJson(JToken jsonToken, IProtectionService ps)
{
var value = ReadValueAs<string>(jsonToken);
if (value == null)
return;
Values.Clear();
var tags = Regex.Split(value, !string.IsNullOrWhiteSpace(Delimiters) ? Delimiters : $"{Separator}+").Select(t => t.Trim().ToLowerInvariant());
if (!string.IsNullOrWhiteSpace(Pattern))
tags = tags.Where(t => Whitelist.Contains(t) || Regex.IsMatch(t, Pattern));
if (Blacklist.Count > 0)
tags = tags.Where(t => !Blacklist.Contains(t));
foreach (var tag in tags)
Values.Add(tag);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -8,10 +9,12 @@ using Jackett.Common.Indexers.Meta;
using Jackett.Common.Models;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Jackett.Common.Utils.Clients;
using NLog;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using FilterFunc = Jackett.Common.Utils.FilterFunc;
namespace Jackett.Common.Services
{
@@ -29,6 +32,7 @@ namespace Jackett.Common.Services
private readonly Dictionary<string, IIndexer> indexers = new Dictionary<string, IIndexer>();
private AggregateIndexer aggregateIndexer;
private ConcurrentDictionary<string, IWebIndexer> availableFilters = new ConcurrentDictionary<string, IWebIndexer>();
// this map is used to maintain backward compatibility when renaming the id of an indexer
// (the id is used in the torznab/download/search urls and in the indexer configuration file)
@@ -79,7 +83,7 @@ namespace Jackett.Common.Services
MigrateRenamedIndexers();
InitIndexers();
InitCardigannIndexers(path);
InitAggregateIndexer();
InitMetaIndexers();
RemoveLegacyConfigurations();
}
@@ -218,28 +222,25 @@ namespace Jackett.Common.Services
logger.Info($"Loaded {indexers.Count} indexers in total");
}
public void InitAggregateIndexer()
public void InitMetaIndexers()
{
var omdbApiKey = serverConfig.OmdbApiKey;
IFallbackStrategyProvider fallbackStrategyProvider;
IResultFilterProvider resultFilterProvider;
if (!string.IsNullOrWhiteSpace(omdbApiKey))
{
var imdbResolver = new OmdbResolver(webClient, omdbApiKey, serverConfig.OmdbApiUrl);
fallbackStrategyProvider = new ImdbFallbackStrategyProvider(imdbResolver);
resultFilterProvider = new ImdbTitleResultFilterProvider(imdbResolver);
}
else
{
fallbackStrategyProvider = new NoFallbackStrategyProvider();
resultFilterProvider = new NoResultFilterProvider();
}
var (fallbackStrategyProvider, resultFilterProvider) = GetStrategyProviders();
logger.Info("Adding aggregate indexer ('all' indexer) ...");
aggregateIndexer = new AggregateIndexer(fallbackStrategyProvider, resultFilterProvider, configService, webClient, logger, protectionService, cacheService)
{
Indexers = indexers.Values
};
var predefinedFilters =
new[] { "public", "private", "semi-public" }
.Select(type => (filter: FilterFunc.Type.ToFilter(type), func: FilterFunc.Type.ToFunc(type)))
.Concat(
indexers.Values.SelectMany(x => x.Tags).Distinct()
.Select(tag => (filter: FilterFunc.Tag.ToFilter(tag), func: FilterFunc.Tag.ToFunc(tag)))
).Select(x => new KeyValuePair<string, IWebIndexer>(x.filter, CreateFilterIndexer(x.filter, x.func)));
availableFilters = new ConcurrentDictionary<string, IWebIndexer>(predefinedFilters);
}
public void RemoveLegacyConfigurations()
@@ -271,16 +272,10 @@ namespace Jackett.Common.Services
This may stop working in the future.");
}
if (indexers.ContainsKey(realName))
return indexers[realName];
if (realName == "all")
return aggregateIndexer;
logger.Error($"Request for unknown indexer: {realName}");
throw new Exception($"Unknown indexer: {realName}");
return GetWebIndexer(realName);
}
public IWebIndexer GetWebIndexer(string name)
{
if (indexers.ContainsKey(name))
@@ -289,6 +284,12 @@ namespace Jackett.Common.Services
if (name == "all")
return aggregateIndexer;
if (availableFilters.TryGetValue(name, out var indexer))
return indexer;
if (FilterFunc.TryParse(name, out var filterFunc))
return availableFilters.GetOrAdd(name, x => CreateFilterIndexer(name, filterFunc));
logger.Error($"Request for unknown indexer: {name}");
throw new Exception($"Unknown indexer: {name}");
}
@@ -318,5 +319,47 @@ namespace Jackett.Common.Services
configService.Delete(indexer);
indexer.Unconfigure();
}
private IWebIndexer CreateFilterIndexer(string filter, Func<IIndexer, bool> filterFunc)
{
var (fallbackStrategyProvider, resultFilterProvider) = GetStrategyProviders();
logger.Info($"Adding filter indexer ('{filter}' indexer) ...");
return new FilterIndexer(
filter,
fallbackStrategyProvider,
resultFilterProvider,
configService,
webClient,
logger,
protectionService,
cacheService,
filterFunc
)
{
Indexers = indexers.Values
};
}
private (IFallbackStrategyProvider fallbackStrategyProvider, IResultFilterProvider resultFilterProvider)
GetStrategyProviders()
{
var omdbApiKey = serverConfig.OmdbApiKey;
IFallbackStrategyProvider fallbackStrategyProvider;
IResultFilterProvider resultFilterProvider;
if (!string.IsNullOrWhiteSpace(omdbApiKey))
{
var imdbResolver = new OmdbResolver(webClient, omdbApiKey, serverConfig.OmdbApiUrl);
fallbackStrategyProvider = new ImdbFallbackStrategyProvider(imdbResolver);
resultFilterProvider = new ImdbTitleResultFilterProvider(imdbResolver);
}
else
{
fallbackStrategyProvider = new NoFallbackStrategyProvider();
resultFilterProvider = new NoResultFilterProvider();
}
return (fallbackStrategyProvider, resultFilterProvider);
}
}
}

View File

@@ -13,6 +13,6 @@ namespace Jackett.Common.Services.Interfaces
IEnumerable<IIndexer> GetAllIndexers();
void InitIndexers(IEnumerable<string> path);
void InitAggregateIndexer();
void InitMetaIndexers();
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Linq;
using Jackett.Common.Indexers;
using Jackett.Common.Utils.FilterFuncs;
namespace Jackett.Common.Utils
{
public abstract class FilterFunc
{
public static readonly FilterFuncExpression Expression;
public static readonly FilterFuncComponent Tag = Component("tag", args =>
{
var tag = args.ToLowerInvariant();
return indexer => Array.IndexOf(indexer.Tags, tag) > -1;
});
public static readonly FilterFuncComponent Language = Component("lang", args => indexer => indexer.Language.StartsWith(args, StringComparison.InvariantCultureIgnoreCase));
public static readonly FilterFuncComponent Type = Component("type", args => indexer => string.Equals(indexer.Type, args, StringComparison.InvariantCultureIgnoreCase));
static FilterFunc()
{
Expression = new FilterFuncExpression(Tag, Language, Type);
}
public static bool TryParse(string source, out Func<IIndexer, bool> func)
{
func = Expression.FromFilter(source);
return func != null;
}
public abstract Func<IIndexer, bool> FromFilter(string source);
public static FilterFuncComponent Component(string id, Func<string, Func<IIndexer, bool>> builder)
{
return new LambdaFilterFuncComponent(id, builder);
}
private class LambdaFilterFuncComponent : FilterFuncComponent
{
private readonly Func<string, Func<IIndexer, bool>> builder;
internal LambdaFilterFuncComponent(string id, Func<string, Func<IIndexer, bool>> builder) : base(id)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
this.builder = builder;
}
public override Func<IIndexer, bool> ToFunc(string args)
{
var func = builder(args);
return indexer => indexer != null
? indexer.IsConfigured && func(indexer)
: throw new ArgumentNullException(nameof(indexer));
}
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Linq;
using Jackett.Common.Indexers;
namespace Jackett.Common.Utils.FilterFuncs
{
public abstract class FilterFuncComponent : FilterFunc
{
private static readonly char Separator = ':';
protected FilterFuncComponent(string id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("ID cannot be an empty string or whitespaces", nameof(id));
ID = id;
}
public string ID { get; }
public override Func<IIndexer, bool> FromFilter(string source)
{
if (string.IsNullOrWhiteSpace(source))
return null;
var parts = source.Split(new []{Separator}, 2);
if (parts.Length != 2)
return null;
if (!string.Equals(parts[0], ID, StringComparison.InvariantCultureIgnoreCase))
return null;
var args = parts[1];
if (string.IsNullOrWhiteSpace(args))
return null;
return ToFunc(args);
}
public abstract Func<IIndexer, bool> ToFunc(string args);
public string ToFilter(string args)
{
return $"{ID}{Separator}{args}";
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jackett.Common.Indexers;
namespace Jackett.Common.Utils.FilterFuncs
{
public class FilterFuncExpression : FilterFunc
{
private static readonly char Separator = ':';
private static readonly char NotOperator = '!';
private static readonly char OrOperator = ',';
private static readonly char AndOperator = '+';
private readonly IReadOnlyDictionary<string, Func<string, Func<IIndexer, bool>>> components;
public FilterFuncExpression(params FilterFuncComponent[] components)
{
if (components == null)
throw new ArgumentNullException(nameof(components));
if (components.Length == 0)
throw new ArgumentException("Filters cannot be an empty collection.", nameof(components));
if (components.Any(x => x == null))
throw new ArgumentException("Filters cannot contains null values.", nameof(components));
this.components = components.ToDictionary<FilterFuncComponent, string, Func<string, Func<IIndexer, bool>>>(x => x.ID, x => x.ToFunc, StringComparer.InvariantCultureIgnoreCase);
}
public override Func<IIndexer, bool> FromFilter(string source)
{
if (string.IsNullOrWhiteSpace(source))
return null;
if (source.Contains(OrOperator))
return source.Split(OrOperator).Select(FromFilter).Aggregate(Or);
if (source.Contains(AndOperator))
return source.Split(AndOperator).Select(FromFilter).Aggregate(And);
if (source[0] == NotOperator)
return Not(FromFilter(source.Substring(1)));
if (source.Contains(Separator))
{
var parts = source.Split(new[] {Separator}, 2);
if (parts.Length == 2 && components.TryGetValue(parts[0], out var toFunc))
return toFunc(parts[1]);
}
return null;
}
private static Func<IIndexer, bool> Not(Func<IIndexer, bool> u) => i => !u(i);
private static Func<IIndexer, bool> And(Func<IIndexer, bool> l, Func<IIndexer, bool> r) => i => l(i) && r(i);
private static Func<IIndexer, bool> Or(Func<IIndexer, bool> l, Func<IIndexer, bool> r) => i => l(i) || r(i);
}
}

View File

@@ -212,7 +212,7 @@ namespace Jackett.Server.Controllers
var manualResult = new ManualSearchResult();
var trackers = CurrentIndexer is BaseMetaIndexer
? (CurrentIndexer as BaseMetaIndexer).Indexers.Where(t => t.IsConfigured)
? (CurrentIndexer as BaseMetaIndexer).ValidIndexers
: (new[] { CurrentIndexer });
// Filter current trackers list on Tracker query parameter if available

View File

@@ -132,7 +132,7 @@ namespace Jackett.Server.Controllers
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
configService.SaveConfig(serverConfig);
// HACK
indexerService.InitAggregateIndexer();
indexerService.InitMetaIndexers();
}
if (config.proxy_type != serverConfig.ProxyType ||

View File

@@ -0,0 +1,98 @@
using System;
using Jackett.Common.Indexers;
using Jackett.Common.Utils.FilterFuncs;
using NUnit.Framework;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
[TestFixture]
public class FilterFuncComponentTests
{
private readonly FilterFuncComponent target = new FilterFuncComponentStub("filter");
private static readonly Func<IIndexer, bool> Func = _ => true;
private class FilterFuncComponentStub : FilterFuncComponent
{
public FilterFuncComponentStub(string id) : base(id)
{
}
public override Func<IIndexer, bool> ToFunc(string args) => Func;
}
[Test]
public void Ctor_NullID_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => new FilterFuncComponentStub(null));
}
[Test]
public void Ctor_EmptyID_ThrowsException()
{
Assert.Throws<ArgumentException>(() => new FilterFuncComponentStub(string.Empty));
}
[Test]
public void Ctor_WhitespaceID_ThrowsException()
{
Assert.Throws<ArgumentException>(() => new FilterFuncComponentStub(" "));
}
[Test]
public void FromFilter_NullSource_Null()
{
var actual = target.FromFilter(null);
Assert.IsNull(actual);
}
[Test]
public void FromFilter_EmptySource_Null()
{
var actual = target.FromFilter(string.Empty);
Assert.IsNull(actual);
}
[Test]
public void FromFilter_WhitespaceSource_Null()
{
var actual = target.FromFilter(" ");
Assert.IsNull(actual);
}
[Test]
public void FromFilter_WrongSource_Null()
{
var actual = target.FromFilter("wrong:args");
Assert.IsNull(actual);
}
[Test]
public void FromFilter_NoArgsSource_Null()
{
var actual = target.FromFilter(target.ID);
Assert.IsNull(actual);
}
[Test]
public void FromFilter_EmptyArgsSource_Null()
{
var actual = target.FromFilter($"{target.ID}:");
Assert.IsNull(actual);
}
[Test]
public void FromFilter_SourceWithArgs()
{
var actual = target.FromFilter($"{target.ID.ToUpper()}:args");
Assert.AreSame(Func, actual);
}
[Test]
public void FromFilter_CaseInsensitivePrefixSource()
{
var actual = target.FromFilter($"{target.ID.ToUpper()}:args");
Assert.AreSame(Func, actual);
}
}
}

View File

@@ -0,0 +1,128 @@
using System;
using Jackett.Common.Indexers;
using Jackett.Common.Utils.FilterFuncs;
using Jackett.Test.TestHelpers;
using NUnit.Framework;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
[TestFixture]
public class FilterFuncExpressionTests
{
private class FilterFuncComponentStub : FilterFuncComponent
{
private readonly Func<string, Func<IIndexer, bool>> builderFunc;
public FilterFuncComponentStub(string id, Func<string, Func<IIndexer, bool>> builderFunc) : base(id)
{
this.builderFunc = builderFunc;
}
public override Func<IIndexer, bool> ToFunc(string args) => builderFunc(args);
}
private static readonly FilterFuncComponentStub _BoolFilterFunc =
new FilterFuncComponentStub("bool",
args => bool.Parse(args) ? (Func<IIndexer, bool>)(_ => true) : _ => false
);
[Test]
public void Ctor_NoFilters_ThrowsException()
{
Assert.Throws<ArgumentException>(() => new FilterFuncExpression());
}
[Test]
public void Ctor_NullFilters_ThrowsException()
{
Assert.Throws<ArgumentNullException>(() => new FilterFuncExpression(null));
}
[Test]
public void Ctor_EmptyFilters_ThrowsException()
{
Assert.Throws<ArgumentException>(() =>
new FilterFuncExpression(Array.Empty<FilterFuncComponent>())
);
}
[Test]
public void Ctor_WithNullFilter_ThrowsException()
{
Assert.Throws<ArgumentException>(() =>
new FilterFuncExpression(default(FilterFuncComponent))
);
}
[Test]
public void Ctor_WithDuplicatedPrefixFilter_ThrowsException()
{
const string id = "f1";
Func<string, Func<IIndexer, bool>> func = _ => throw TestExceptions.UnexpectedInvocation;
Assert.Throws<ArgumentException>(() =>
{
new FilterFuncExpression(
new FilterFuncComponentStub(id, func),
new FilterFuncComponentStub(id, func));
});
}
[Test]
public void SingleSource()
{
Func<IIndexer, bool> expectedFunc1 = _ => throw TestExceptions.UnexpectedInvocation;
Func<IIndexer, bool> expectedFunc2 = _ => throw TestExceptions.UnexpectedInvocation;
var target = new FilterFuncExpression(
new FilterFuncComponentStub("f1", _ => expectedFunc1),
new FilterFuncComponentStub("f2", _ => expectedFunc2)
);
var actualFunc1 = target.FromFilter("f1:args");
Assert.AreSame(expectedFunc1, actualFunc1);
var actualFunc2 = target.FromFilter("f2:args");
Assert.AreSame(expectedFunc2, actualFunc2);
var actualFunc3 = target.FromFilter("f3:args");
Assert.IsNull(actualFunc3);
}
[Test]
public void SingleSource_NotOperator()
{
var target = new FilterFuncExpression(_BoolFilterFunc);
var filterFunc = target.FromFilter("!bool:true");
Assert.IsFalse(filterFunc(null));
}
[Test]
public void SingleSource_AndOperator()
{
var target = new FilterFuncExpression(_BoolFilterFunc);
var filterFunc = target.FromFilter("bool:true+bool:false");
Assert.IsFalse(filterFunc(null));
}
[Test]
public void SingleSource_OrOperator()
{
var target = new FilterFuncExpression(_BoolFilterFunc);
var filterFunc = target.FromFilter("bool:false,bool:true");
Assert.IsTrue(filterFunc(null));
}
[Test]
public void SingleSource_OperatorPrecedence()
{
var target = new FilterFuncExpression(_BoolFilterFunc);
var filterFunc1 = target.FromFilter("bool:false+bool:true,bool:true");
Assert.IsTrue(filterFunc1(null));
var filterFunc2 = target.FromFilter("bool:true,bool:true+bool:false");
Assert.IsTrue(filterFunc2(null));
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Text;
using System.Threading.Tasks;
using Jackett.Common.Indexers;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Test.TestHelpers;
using Newtonsoft.Json.Linq;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
public class IndexerStub : IIndexer
{
public virtual string SiteLink => throw TestExceptions.UnexpectedInvocation;
public virtual string[] AlternativeSiteLinks => throw TestExceptions.UnexpectedInvocation;
public virtual string DisplayName => throw TestExceptions.UnexpectedInvocation;
public virtual string DisplayDescription => throw TestExceptions.UnexpectedInvocation;
public virtual string Type => throw TestExceptions.UnexpectedInvocation;
public virtual string Language => throw TestExceptions.UnexpectedInvocation;
public virtual string LastError
{
get => throw TestExceptions.UnexpectedInvocation;
set => throw TestExceptions.UnexpectedInvocation;
}
public virtual string Id => throw TestExceptions.UnexpectedInvocation;
public virtual Encoding Encoding => throw TestExceptions.UnexpectedInvocation;
public virtual TorznabCapabilities TorznabCaps => throw TestExceptions.UnexpectedInvocation;
public virtual bool IsConfigured => throw TestExceptions.UnexpectedInvocation;
public virtual string[] Tags => throw TestExceptions.UnexpectedInvocation;
public virtual Task<ConfigurationData> GetConfigurationForSetup() => throw TestExceptions.UnexpectedInvocation;
public virtual Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson) => throw TestExceptions.UnexpectedInvocation;
public virtual void LoadFromSavedConfiguration(JToken jsonConfig) => throw TestExceptions.UnexpectedInvocation;
public virtual void SaveConfig() => throw TestExceptions.UnexpectedInvocation;
public virtual void Unconfigure() => throw TestExceptions.UnexpectedInvocation;
public virtual Task<IndexerResult> ResultsForQuery(TorznabQuery query, bool isMetaIndexer = false) => throw TestExceptions.UnexpectedInvocation;
public virtual bool CanHandleQuery(TorznabQuery query) => throw TestExceptions.UnexpectedInvocation;
}
}

View File

@@ -0,0 +1,55 @@
using NUnit.Framework;
using static Jackett.Common.Utils.FilterFunc;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
[TestFixture]
public class LanguageFuncTests
{
private class LanguageIndexerStub : IndexerStub
{
public LanguageIndexerStub(string language)
{
Language = language;
}
public override bool IsConfigured => true;
public override string Language { get; }
}
[Test]
public void CaseInsensitiveSource_CaseInsensitiveFilter()
{
var language = "en";
var region = "US";
var lrLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToLower()}");
var LRFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToUpper()}");
Assert.IsTrue(LRFilterFunc(lrLanguage));
var lRLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToUpper()}");
var LrFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToLower()}");
Assert.IsTrue(LrFilterFunc(lRLanguage));
var LrLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToLower()}");
var lRFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToUpper()}");
Assert.IsTrue(lRFilterFunc(LrLanguage));
var LRLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToUpper()}");
var lrFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToLower()}");
Assert.IsTrue(lrFilterFunc(LRLanguage));
}
[Test]
public void LanguageWithoutRegion()
{
var language = "en";
var funcFilter = Language.ToFunc(language);
Assert.IsTrue(funcFilter(new LanguageIndexerStub(language)));
Assert.IsTrue(funcFilter(new LanguageIndexerStub($"{language}-region1")));
Assert.IsFalse(funcFilter(new LanguageIndexerStub($"language2-{language}")));
}
}
}

View File

@@ -0,0 +1,47 @@
using NUnit.Framework;
using static Jackett.Common.Utils.FilterFunc;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
[TestFixture]
public class TagFuncTests
{
private class TagsIndexerStub : IndexerStub
{
public TagsIndexerStub(params string[] tags)
{
Tags = tags;
}
public override bool IsConfigured => true;
public override string[] Tags { get; }
}
[Test]
public void CaseInsensitiveFilter()
{
var tagId = "g1";
var tag = new TagsIndexerStub(tagId);
var upperTarget = Tag.ToFunc(tagId.ToUpper());
Assert.IsTrue(upperTarget(tag));
var lowerTarget = Tag.ToFunc(tagId.ToLower());
Assert.IsTrue(lowerTarget(tag));
}
[Test]
public void ContainsTagId()
{
var tagId = "g1";
var target = Tag.ToFunc(tagId);
Assert.IsTrue(target(new TagsIndexerStub(tagId)));
Assert.IsTrue(target(new TagsIndexerStub(tagId, "g2")));
Assert.IsTrue(target(new TagsIndexerStub("g2", tagId)));
Assert.IsFalse(target(new TagsIndexerStub("g2")));
}
}
}

View File

@@ -0,0 +1,52 @@
using NUnit.Framework;
using static Jackett.Common.Utils.FilterFunc;
namespace Jackett.Test.Common.Utils.FilterFuncs
{
[TestFixture]
public class TypeFuncTests
{
private class TypeIndexerStub : IndexerStub
{
public TypeIndexerStub(string type)
{
Type = type;
}
public override bool IsConfigured => true;
public override string Type { get; }
}
[Test]
public void CaseInsensitiveSource_CaseInsensitiveFilter()
{
var typeId = "type-id";
var lowerType = new TypeIndexerStub(typeId.ToLower());
var upperType = new TypeIndexerStub(typeId.ToUpper());
var upperFilterFunc = Type.ToFunc(typeId.ToUpper());
Assert.IsTrue(upperFilterFunc(lowerType));
Assert.IsTrue(upperFilterFunc(upperType));
var lowerFilterFunc = Type.ToFunc(typeId.ToLower());
Assert.IsTrue(lowerFilterFunc(lowerType));
Assert.IsTrue(lowerFilterFunc(upperType));
}
[Test]
public void PartialType()
{
var typeId = "type-id";
var funcFilter = Type.ToFunc($"{typeId}");
Assert.IsFalse(funcFilter(new TypeIndexerStub($"{typeId}suffix")));
Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}")));
Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}suffix")));
}
}
}

View File

@@ -0,0 +1,9 @@
using NUnit.Framework;
namespace Jackett.Test.TestHelpers
{
internal static class TestExceptions
{
public static AssertionException UnexpectedInvocation => new AssertionException("Unexpected Invocation");
}
}

View File

@@ -25,6 +25,6 @@ namespace Jackett.Test.TestHelpers
public Task TestIndexer(string name) => throw new NotImplementedException();
public void InitAggregateIndexer() => throw new NotImplementedException();
public void InitMetaIndexers() => throw new NotImplementedException();
}
}