mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-11 14:19:17 +02:00
Compare commits
18 Commits
v0.17.1036
...
v0.18.15
Author | SHA1 | Date | |
---|---|---|---|
![]() |
01ff410e62 | ||
![]() |
25942ab7f5 | ||
![]() |
f8ff98ed4c | ||
![]() |
4761718ce3 | ||
![]() |
b80d7c97e6 | ||
![]() |
f0eb037dc7 | ||
![]() |
f86acd2721 | ||
![]() |
56a7e432d4 | ||
![]() |
fbad508789 | ||
![]() |
e1511de04b | ||
![]() |
0a5582a323 | ||
![]() |
66bec102db | ||
![]() |
b07543bff6 | ||
![]() |
83c7028a95 | ||
![]() |
1a89baa9a1 | ||
![]() |
75e2f22eb7 | ||
![]() |
67ab3b8a10 | ||
![]() |
2e7813ecfa |
21
README.md
21
README.md
@@ -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.
|
||||
|
@@ -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
|
||||
|
1
src/Jackett.Common/Content/css/tagify.css
Normal file
1
src/Jackett.Common/Content/css/tagify.css
Normal file
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
18
src/Jackett.Common/Content/libs/jQuery.tagify.min.js
vendored
Normal file
18
src/Jackett.Common/Content/libs/jQuery.tagify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/Jackett.Common/Content/libs/tagify.min.js
vendored
Normal file
1
src/Jackett.Common/Content/libs/tagify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 }}"
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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"]
|
||||
|
@@ -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:
|
||||
|
@@ -7,6 +7,8 @@ type: semi-private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://torrents-local.xyz/
|
||||
certificates:
|
||||
- 85f31ff50a1ee0dbc738a12015efe9ef8708505b # expired 5th May 2021
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -6,6 +6,8 @@ language: en-us
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://looptorrent.net/
|
||||
legacylinks:
|
||||
- https://vsttorrents.net/
|
||||
|
||||
caps:
|
||||
|
@@ -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)
|
||||
|
@@ -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(),
|
||||
" ",
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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/",
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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() { }
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,6 @@ namespace Jackett.Common.Services.Interfaces
|
||||
IEnumerable<IIndexer> GetAllIndexers();
|
||||
|
||||
void InitIndexers(IEnumerable<string> path);
|
||||
void InitAggregateIndexer();
|
||||
void InitMetaIndexers();
|
||||
}
|
||||
}
|
||||
|
58
src/Jackett.Common/Utils/FilterFunc.cs
Normal file
58
src/Jackett.Common/Utils/FilterFunc.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs
Normal file
46
src/Jackett.Common/Utils/FilterFuncs/FilterFuncComponent.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
51
src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs
Normal file
51
src/Jackett.Common/Utils/FilterFuncs/FilterFuncExpression.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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 ||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
55
src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs
Normal file
55
src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -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}")));
|
||||
}
|
||||
}
|
||||
}
|
47
src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs
Normal file
47
src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs
Normal 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")));
|
||||
}
|
||||
}
|
||||
}
|
52
src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs
Normal file
52
src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs
Normal 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")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
9
src/Jackett.Test/TestHelpers/TestExceptions.cs
Normal file
9
src/Jackett.Test/TestHelpers/TestExceptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Jackett.Test.TestHelpers
|
||||
{
|
||||
internal static class TestExceptions
|
||||
{
|
||||
public static AssertionException UnexpectedInvocation => new AssertionException("Unexpected Invocation");
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user