mirror of https://github.com/Jackett/Jackett
[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>
This commit is contained in:
parent
b07543bff6
commit
66bec102db
|
@ -2,7 +2,7 @@
|
||||||
name: $(majorVersion).$(minorVersion).$(patchVersion)
|
name: $(majorVersion).$(minorVersion).$(patchVersion)
|
||||||
variables:
|
variables:
|
||||||
majorVersion: 0
|
majorVersion: 0
|
||||||
minorVersion: 17
|
minorVersion: 18
|
||||||
patchVersion: $[counter(variables['minorVersion'], 1)] # this will reset when we bump minor
|
patchVersion: $[counter(variables['minorVersion'], 1)] # this will reset when we bump minor
|
||||||
jackettVersion: $(majorVersion).$(minorVersion).$(patchVersion)
|
jackettVersion: $(majorVersion).$(minorVersion).$(patchVersion)
|
||||||
buildConfiguration: Release
|
buildConfiguration: Release
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -76,6 +76,10 @@ body {
|
||||||
max-width: 255px;
|
max-width: 255px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-item-inputtags {
|
||||||
|
max-width: 255px;
|
||||||
|
}
|
||||||
|
|
||||||
[data-type=hiddendata]{
|
[data-type=hiddendata]{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -328,3 +332,21 @@ input#searchquery {
|
||||||
#proxy-warning {
|
#proxy-warning {
|
||||||
color: red;
|
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 indexers = [];
|
||||||
var configuredIndexers = [];
|
var configuredIndexers = [];
|
||||||
var unconfiguredIndexers = [];
|
var unconfiguredIndexers = [];
|
||||||
|
var configuredTags = [];
|
||||||
|
var availableFilters = [];
|
||||||
|
|
||||||
$.fn.inView = function () {
|
$.fn.inView = function () {
|
||||||
if (!this.length) return false;
|
if (!this.length) return false;
|
||||||
|
@ -58,7 +60,7 @@ function openSearchIfNecessary() {
|
||||||
decodeURIComponent(item.split('=')[1].replace(/\+/g, '%20')))
|
decodeURIComponent(item.split('=')[1].replace(/\+/g, '%20')))
|
||||||
}, prev), {});
|
}, prev), {});
|
||||||
if ("search" in hashArgs) {
|
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");
|
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) {
|
function getJackettConfig(callback) {
|
||||||
api.getServerConfig(callback).fail(function () {
|
api.getServerConfig(callback).fail(function () {
|
||||||
doNotify("Error loading Jackett settings, request to Jackett server failed, is server running ?", "danger", "glyphicon glyphicon-alert");
|
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() {
|
function reloadIndexers() {
|
||||||
|
$('#filters').hide();
|
||||||
$('#indexers').hide();
|
$('#indexers').hide();
|
||||||
api.getAllIndexers(function (data) {
|
api.getAllIndexers(function (data) {
|
||||||
indexers = data;
|
indexers = data;
|
||||||
configuredIndexers = [];
|
configuredIndexers = [];
|
||||||
unconfiguredIndexers = [];
|
unconfiguredIndexers = [];
|
||||||
|
configuredTags = [];
|
||||||
|
availableFilters = [];
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var item = data[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=");
|
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
|
else
|
||||||
unconfiguredIndexers.push(item);
|
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);
|
displayConfiguredIndexersList(configuredIndexers);
|
||||||
|
|
||||||
$('#indexers div.dataTables_filter input').focusWithoutScrolling();
|
$('#indexers div.dataTables_filter input').focusWithoutScrolling();
|
||||||
openSearchIfNecessary();
|
openSearchIfNecessary();
|
||||||
}).fail(function () {
|
}).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) {
|
function displayConfiguredIndexersList(indexers) {
|
||||||
var indexersTemplate = Handlebars.compile($("#configured-indexer-table").html());
|
var indexersTemplate = Handlebars.compile($("#configured-indexer-table").html());
|
||||||
var indexersTable = $(indexersTemplate({
|
var indexersTable = $(indexersTemplate({
|
||||||
|
@ -484,17 +520,20 @@ function prepareSearchButtons(element) {
|
||||||
var id = $btn.data("id");
|
var id = $btn.data("id");
|
||||||
$btn.click(function () {
|
$btn.click(function () {
|
||||||
window.location.hash = "search&tracker=" + id;
|
window.location.hash = "search&tracker=" + id;
|
||||||
showSearch(id);
|
showSearch(null, id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareSetupButtons(element) {
|
function prepareSetupButtons(element) {
|
||||||
element.find('.indexer-setup').each(function (i, btn) {
|
element.find('.indexer-setup').each(function (i, btn) {
|
||||||
var indexer = configuredIndexers[i];
|
var $btn = $(btn);
|
||||||
$(btn).click(function () {
|
var id = $btn.data("id");
|
||||||
displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks, indexer.description);
|
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 item = config[i];
|
||||||
var setupValueTemplate = Handlebars.compile($("#setup-item-" + item.type).html());
|
var setupValueTemplate = Handlebars.compile($("#setup-item-" + item.type).html());
|
||||||
item.value_element = setupValueTemplate(item);
|
item.value_element = setupValueTemplate(item);
|
||||||
var template = setupItemTemplate(item);
|
var template = $(setupItemTemplate(item));
|
||||||
$formItemContainer.append(template);
|
$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) {
|
function newConfigModal(title, config, caps, link, alternativesitelinks, description) {
|
||||||
var configTemplate = Handlebars.compile($("#jackett-config-setup-modal").html());
|
var configTemplate = Handlebars.compile($("#jackett-config-setup-modal").html());
|
||||||
var configForm = $(configTemplate({
|
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;
|
return configForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,9 +730,13 @@ function getConfigModalJson(configForm) {
|
||||||
$el.find(".setup-item-inputcheckbox input:checked").each(function () {
|
$el.find(".setup-item-inputcheckbox input:checked").each(function () {
|
||||||
itemEntry.values.push($(this).val());
|
itemEntry.values.push($(this).val());
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
case "inputselect":
|
case "inputselect":
|
||||||
itemEntry.value = $el.find(".setup-item-inputselect select").val();
|
itemEntry.value = $el.find(".setup-item-inputselect select").val();
|
||||||
break;
|
break;
|
||||||
|
case "inputtags":
|
||||||
|
itemEntry.value = $el.find(".setup-item-inputtags input").val();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
configJson.push(itemEntry)
|
configJson.push(itemEntry)
|
||||||
});
|
});
|
||||||
|
@ -802,14 +868,15 @@ function updateReleasesRow(row) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch(selectedIndexer, query, category) {
|
function showSearch(selectedFilter, selectedIndexer, query, category) {
|
||||||
var selectedIndexers = [];
|
var selectedIndexers = [];
|
||||||
if (selectedIndexer)
|
if (selectedIndexer)
|
||||||
selectedIndexers = selectedIndexer.split(",");
|
selectedIndexers = selectedIndexer.split(",");
|
||||||
$('#select-indexer-modal').remove();
|
$('#select-indexer-modal').remove();
|
||||||
var releaseTemplate = Handlebars.compile($("#jackett-search").html());
|
var releaseTemplate = Handlebars.compile($("#jackett-search").html());
|
||||||
var releaseDialog = $(releaseTemplate({
|
var releaseDialog = $(releaseTemplate({
|
||||||
indexers: configuredIndexers
|
filters: availableFilters,
|
||||||
|
active: selectedFilter
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$("#modals").append(releaseDialog);
|
$("#modals").append(releaseDialog);
|
||||||
|
@ -823,6 +890,29 @@ function showSearch(selectedIndexer, query, category) {
|
||||||
window.location.hash = '';
|
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 setCategories = function (trackers, items) {
|
||||||
var cats = {};
|
var cats = {};
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
@ -869,6 +959,7 @@ function showSearch(selectedIndexer, query, category) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var searchString = releaseDialog.find('#searchquery').val();
|
var searchString = releaseDialog.find('#searchquery').val();
|
||||||
|
var filterId = releaseDialog.find('#searchFilter').val();
|
||||||
var queryObj = {
|
var queryObj = {
|
||||||
Query: searchString,
|
Query: searchString,
|
||||||
Category: releaseDialog.find('#searchCategory').val(),
|
Category: releaseDialog.find('#searchCategory').val(),
|
||||||
|
@ -878,14 +969,15 @@ function showSearch(selectedIndexer, query, category) {
|
||||||
window.location.hash = Object.entries({
|
window.location.hash = Object.entries({
|
||||||
search: encodeURIComponent(queryObj.Query).replace(/%20/g, '+'),
|
search: encodeURIComponent(queryObj.Query).replace(/%20/g, '+'),
|
||||||
tracker: queryObj.Tracker.join(","),
|
tracker: queryObj.Tracker.join(","),
|
||||||
category: queryObj.Category.join(",")
|
category: queryObj.Category.join(","),
|
||||||
}).map(([k, v], i) => k + '=' + v).join('&');
|
filter: filterId ? encodeURIComponent(filterId) : ""
|
||||||
|
}).filter(([k, v]) => v).map(([k, v], i) => k + '=' + v).join('&');
|
||||||
|
|
||||||
$('#jackett-search-perform').html($('#spinner').html());
|
$('#jackett-search-perform').html($('#spinner').html());
|
||||||
$('#searchResults div.dataTables_filter input').val("");
|
$('#searchResults div.dataTables_filter input').val("");
|
||||||
clearSearchResultTable($('#searchResults'));
|
clearSearchResultTable($('#searchResults'));
|
||||||
|
|
||||||
var trackerId = "all";
|
var trackerId = filterId || "all";
|
||||||
api.resultsForIndexer(trackerId, queryObj, function (data) {
|
api.resultsForIndexer(trackerId, queryObj, function (data) {
|
||||||
for (var i = 0; i < data.Results.length; i++) {
|
for (var i = 0; i < data.Results.length; i++) {
|
||||||
var item = data.Results[i];
|
var item = data.Results[i];
|
||||||
|
@ -906,16 +998,14 @@ function showSearch(selectedIndexer, query, category) {
|
||||||
|
|
||||||
var searchTracker = releaseDialog.find("#searchTracker");
|
var searchTracker = releaseDialog.find("#searchTracker");
|
||||||
var searchCategory = releaseDialog.find('#searchCategory');
|
var searchCategory = releaseDialog.find('#searchCategory');
|
||||||
searchCategory.multiselect({
|
var searchFilter = releaseDialog.find('#searchFilter');
|
||||||
|
|
||||||
|
searchFilter.multiselect({
|
||||||
maxHeight: 400,
|
maxHeight: 400,
|
||||||
enableFiltering: true,
|
enableFiltering: true,
|
||||||
includeSelectAllOption: true,
|
|
||||||
enableCaseInsensitiveFiltering: true,
|
enableCaseInsensitiveFiltering: true,
|
||||||
nonSelectedText: 'Any'
|
nonSelectedText: 'All'
|
||||||
});
|
});
|
||||||
if (selectedIndexers)
|
|
||||||
searchTracker.val(selectedIndexers);
|
|
||||||
searchTracker.trigger("change");
|
|
||||||
|
|
||||||
updateSearchResultTable($('#searchResults'), []);
|
updateSearchResultTable($('#searchResults'), []);
|
||||||
clearSearchResultTable($('#searchResults'));
|
clearSearchResultTable($('#searchResults'));
|
||||||
|
@ -928,6 +1018,29 @@ function showSearch(selectedIndexer, query, category) {
|
||||||
nonSelectedText: 'All'
|
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) {
|
if (category !== undefined) {
|
||||||
searchCategory.val(category.split(","));
|
searchCategory.val(category.split(","));
|
||||||
|
@ -1231,7 +1344,7 @@ function bindUIButtons() {
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#jackett-show-search").click(function () {
|
$("#jackett-show-search").click(function () {
|
||||||
showSearch(null);
|
showSearch();
|
||||||
window.location.hash = "search";
|
window.location.hash = "search";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1348,4 +1461,4 @@ function proxyWarning(input) {
|
||||||
} else {
|
} else {
|
||||||
$('#proxy-warning').hide();
|
$('#proxy-warning').hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,3 +330,21 @@ input#searchquery {
|
||||||
#proxy-warning {
|
#proxy-warning {
|
||||||
color: red;
|
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="../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-notify.js?changed=2017083001"></script>
|
||||||
<script type="text/javascript" src="../libs/bootstrap-multiselect.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="../bootstrap/bootstrap.min.css?changed=2017083001">
|
||||||
<link rel="stylesheet" type="text/css" href="../animate.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="../css/tagify.css?changed=11662">
|
||||||
<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="../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/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/bootstrap-multiselect.css?changed=2017083001" />
|
||||||
<link rel="stylesheet" type="text/css" href="../css/font-awesome.min.css?changed=2017083001">
|
<link rel="stylesheet" type="text/css" href="../css/font-awesome.min.css?changed=2017083001">
|
||||||
|
@ -65,7 +68,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>Configured Indexers</h3>
|
<h3>Configured Indexers</h3>
|
||||||
<div id="indexers"> </div>
|
<div id="indexers"></div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
|
@ -213,7 +216,7 @@
|
||||||
<div id="modals"></div>
|
<div id="modals"></div>
|
||||||
|
|
||||||
<script id="setup-item" type="text/x-handlebars-template">
|
<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-label">{{name}}</div>
|
||||||
<div class="setup-item-value">{{{value_element}}}</div>
|
<div class="setup-item-value">{{{value_element}}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -289,10 +292,14 @@
|
||||||
Click on an URL to copy it to the Site Link field.
|
Click on an URL to copy it to the Site Link field.
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</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">
|
<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">
|
<table id="configured-indexer-datatable" class="indexer-table dataTable compact cell-border hover stripe table table-responsive">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -303,7 +310,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each indexers}}
|
{{#each indexers}}
|
||||||
<tr class="configured-indexer-row">
|
<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">
|
<td class="fit">
|
||||||
<div class="indexer-buttons">
|
<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>
|
<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>
|
<p>You can search all configured indexers from this screen.</p>
|
||||||
<label for="text">Query</label>
|
<label for="text">Query</label>
|
||||||
<input class="form-control" type="text" name="query" id="searchquery" />
|
<input class="form-control" type="text" name="query" id="searchquery" />
|
||||||
<label for="tracker">Tracker</label>
|
{{#if filters}}
|
||||||
<select name="tracker" id="searchTracker" multiple="multiple">
|
<label for="filter">Filter</label>
|
||||||
{{#each indexers}}
|
<select name="filter" id="searchFilter">
|
||||||
<option value="{{id}}" selected>{{name}}</option>
|
<option value="all">all</option>
|
||||||
{{/each}}
|
{{#each filters}}
|
||||||
|
<option value="{{id}}">{{id}}</option>
|
||||||
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
|
{{/if}}
|
||||||
|
<label for="tracker">Tracker</label>
|
||||||
|
<select name="tracker" id="searchTracker" multiple="multiple"></select>
|
||||||
<label for="category">Category</label>
|
<label for="category">Category</label>
|
||||||
<select name="category" id="searchCategory" multiple="multiple"></select>
|
<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>
|
<button id="jackett-search-perform" class="btn btn-success btn-sm"><span class="fa fa-search"></span></button>
|
||||||
|
@ -698,6 +710,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../libs/api.js?changed=2017083001"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -34,6 +34,8 @@ namespace Jackett.Common.Indexers
|
||||||
public Encoding Encoding { get; protected set; }
|
public Encoding Encoding { get; protected set; }
|
||||||
|
|
||||||
public virtual bool IsConfigured { get; protected set; }
|
public virtual bool IsConfigured { get; protected set; }
|
||||||
|
public virtual string[] Tags { get; protected set; }
|
||||||
|
|
||||||
protected Logger logger;
|
protected Logger logger;
|
||||||
protected IIndexerConfigurationService configurationService;
|
protected IIndexerConfigurationService configurationService;
|
||||||
protected IProtectionService protectionService;
|
protected IProtectionService protectionService;
|
||||||
|
@ -148,6 +150,8 @@ namespace Jackett.Common.Indexers
|
||||||
// check whether the site link is well-formatted
|
// check whether the site link is well-formatted
|
||||||
var siteUri = new Uri(configData.SiteLink.Value);
|
var siteUri = new Uri(configData.SiteLink.Value);
|
||||||
SiteLink = configData.SiteLink.Value;
|
SiteLink = configData.SiteLink.Value;
|
||||||
|
|
||||||
|
Tags = configData.Tags.Values.Select(t => t.ToLowerInvariant()).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFromSavedConfiguration(JToken jsonConfig)
|
public void LoadFromSavedConfiguration(JToken jsonConfig)
|
||||||
|
|
|
@ -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
|
// Whether this indexer has been configured, verified and saved in the past and has the settings required for functioning
|
||||||
bool IsConfigured { get; }
|
bool IsConfigured { get; }
|
||||||
|
|
||||||
|
string[] Tags { get; }
|
||||||
|
|
||||||
// Retrieved for starting setup for the indexer via web API
|
// Retrieved for starting setup for the indexer via web API
|
||||||
Task<ConfigurationData> GetConfigurationForSetup();
|
Task<ConfigurationData> GetConfigurationForSetup();
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ namespace Jackett.Common.Indexers.Meta
|
||||||
|
|
||||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
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
|
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);
|
var fallbackStrategies = fallbackStrategyProvider.FallbackStrategiesForQuery(query);
|
||||||
|
@ -109,11 +109,13 @@ namespace Jackett.Common.Indexers.Meta
|
||||||
return result;
|
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;
|
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;
|
public IEnumerable<IIndexer> Indexers;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
using Jackett.Common.Models.IndexerConfig;
|
using Jackett.Common.Models.IndexerConfig;
|
||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
using Jackett.Common.Utils.Clients;
|
using Jackett.Common.Utils.Clients;
|
||||||
|
|
||||||
using NLog;
|
using NLog;
|
||||||
|
|
||||||
namespace Jackett.Common.Indexers.Meta
|
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() { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,9 @@
|
||||||
<Content Include="Content\css\jquery.dataTables.min.css">
|
<Content Include="Content\css\jquery.dataTables.min.css">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="Content\css\tagify.css">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="Content\custom.css">
|
<Content Include="Content\custom.css">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -124,9 +127,15 @@
|
||||||
<Content Include="Content\libs\jquery.min.js">
|
<Content Include="Content\libs\jquery.min.js">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="Content\libs\jQuery.tagify.min.js">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="Content\libs\moment.min.js">
|
<Content Include="Content\libs\moment.min.js">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="Content\libs\tagify.min.js">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="Content\login.html">
|
<Content Include="Content\login.html">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -34,6 +34,8 @@ namespace Jackett.Common.Models.DTO
|
||||||
[DataMember]
|
[DataMember]
|
||||||
public string language { get; private set; }
|
public string language { get; private set; }
|
||||||
[DataMember]
|
[DataMember]
|
||||||
|
public IEnumerable<string> tags { get; private set; }
|
||||||
|
[DataMember]
|
||||||
public string last_error { get; private set; }
|
public string last_error { get; private set; }
|
||||||
[DataMember]
|
[DataMember]
|
||||||
public bool potatoenabled { get; private set; }
|
public bool potatoenabled { get; private set; }
|
||||||
|
@ -55,6 +57,8 @@ namespace Jackett.Common.Models.DTO
|
||||||
|
|
||||||
alternativesitelinks = indexer.AlternativeSiteLinks;
|
alternativesitelinks = indexer.AlternativeSiteLinks;
|
||||||
|
|
||||||
|
tags = indexer.Tags;
|
||||||
|
|
||||||
caps = indexer.TorznabCaps.Categories.GetTorznabCategoryList(true)
|
caps = indexer.TorznabCaps.Categories.GetTorznabCategoryList(true)
|
||||||
.Select(c => new Capability
|
.Select(c => new Capability
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
using Jackett.Common.Utils;
|
using Jackett.Common.Utils;
|
||||||
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Jackett.Common.Models.IndexerConfig
|
namespace Jackett.Common.Models.IndexerConfig
|
||||||
|
@ -12,9 +14,10 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
private const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||";
|
private const string PASSWORD_REPLACEMENT = "|||%%PREVJACKPASSWD%%|||";
|
||||||
protected Dictionary<string, ConfigurationItem> dynamics = new Dictionary<string, ConfigurationItem>(); // list for dynamic items
|
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 CookieHeader { get; private set; } = new HiddenStringConfigurationItem(name: "CookieHeader");
|
||||||
public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name:"LastError");
|
public HiddenStringConfigurationItem LastError { get; private set; } = new HiddenStringConfigurationItem(name: "LastError");
|
||||||
public StringConfigurationItem SiteLink { get; private set; } = new StringConfigurationItem(name:"Site Link");
|
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()
|
public ConfigurationData()
|
||||||
{
|
{
|
||||||
|
@ -36,67 +39,10 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
var jsonToken = jsonArray.FirstOrDefault(f => f.Value<string>("id") == item.ID);
|
var jsonToken = jsonArray.FirstOrDefault(f => f.Value<string>("id") == item.ID);
|
||||||
if (jsonToken == null)
|
if (jsonToken == null)
|
||||||
continue;
|
continue;
|
||||||
|
item.FromJson(jsonToken, ps);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
public JToken ToJson(IProtectionService ps, bool forDisplay = true)
|
||||||
{
|
{
|
||||||
var jArray = new JArray();
|
var jArray = new JArray();
|
||||||
|
@ -104,43 +50,7 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
var configurationItems = GetConfigurationItems(forDisplay);
|
var configurationItems = GetConfigurationItems(forDisplay);
|
||||||
foreach (var configurationItem in configurationItems)
|
foreach (var configurationItem in configurationItems)
|
||||||
{
|
{
|
||||||
JObject jObject = null;
|
var jObject = configurationItem.ToJson(ps, forDisplay);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jObject != null)
|
if (jObject != null)
|
||||||
{
|
{
|
||||||
|
@ -163,8 +73,13 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
properties.Remove(SiteLink);
|
properties.Remove(SiteLink);
|
||||||
properties.Insert(0, SiteLink);
|
properties.Insert(0, SiteLink);
|
||||||
|
|
||||||
|
// remove/insert Tags manualy to make sure it shows up last
|
||||||
|
properties.Remove(Tags);
|
||||||
|
|
||||||
properties.AddRange(dynamics.Values);
|
properties.AddRange(dynamics.Values);
|
||||||
|
|
||||||
|
properties.Add(Tags);
|
||||||
|
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +119,14 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
["name"] = Name
|
["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>
|
/// <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();
|
var jObject = CreateJObject();
|
||||||
|
|
||||||
|
@ -245,6 +168,22 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
: base(name, itemType: "inputstring")
|
: 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
|
public class HiddenStringConfigurationItem : ConfigurationItemMaybePassword
|
||||||
|
@ -253,6 +192,11 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
: base(name, itemType: "hiddendata", canBeShownToUser: false)
|
: base(name, itemType: "hiddendata", canBeShownToUser: false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
|
||||||
|
{
|
||||||
|
Value = ReadValueAs<string>(jsonToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DisplayInfoConfigurationItem : ConfigurationItemMaybePassword
|
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();
|
var jObject = CreateJObject();
|
||||||
jObject["value"] = Value;
|
jObject["value"] = Value;
|
||||||
return jObject;
|
return jObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
|
||||||
|
{
|
||||||
|
Value = ReadValueAs<bool>(jsonToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DisplayImageConfigurationItem : ConfigurationItem
|
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();
|
var jObject = CreateJObject();
|
||||||
|
|
||||||
|
@ -310,7 +259,7 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
public SingleSelectConfigurationItem(string name, Dictionary<string, string> options)
|
public SingleSelectConfigurationItem(string name, Dictionary<string, string> options)
|
||||||
: base(name, itemType: "inputselect") => Options = options;
|
: base(name, itemType: "inputselect") => Options = options;
|
||||||
|
|
||||||
public JObject ToJson()
|
public override JObject ToJson(IProtectionService ps = null, bool forDisplay = true)
|
||||||
{
|
{
|
||||||
var jObject = CreateJObject();
|
var jObject = CreateJObject();
|
||||||
|
|
||||||
|
@ -323,6 +272,11 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
|
|
||||||
return jObject;
|
return jObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void FromJson(JToken jsonToken, IProtectionService ps = null)
|
||||||
|
{
|
||||||
|
Value = ReadValueAs<string>(jsonToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MultiSelectConfigurationItem : ConfigurationItem
|
public class MultiSelectConfigurationItem : ConfigurationItem
|
||||||
|
@ -334,7 +288,7 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
public MultiSelectConfigurationItem(string name, Dictionary<string, string> options)
|
public MultiSelectConfigurationItem(string name, Dictionary<string, string> options)
|
||||||
: base(name, itemType: "inputcheckbox") => Options = options;
|
: base(name, itemType: "inputcheckbox") => Options = options;
|
||||||
|
|
||||||
public JObject ToJson()
|
public override JObject ToJson(IProtectionService ps, bool forDisplay)
|
||||||
{
|
{
|
||||||
var jObject = CreateJObject();
|
var jObject = CreateJObject();
|
||||||
|
|
||||||
|
@ -347,6 +301,15 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
|
|
||||||
return jObject;
|
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
|
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();
|
var jObject = CreateJObject();
|
||||||
|
|
||||||
|
@ -373,6 +336,76 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||||
|
|
||||||
return jObject;
|
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;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -8,10 +9,12 @@ using Jackett.Common.Indexers.Meta;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
using Jackett.Common.Models.Config;
|
using Jackett.Common.Models.Config;
|
||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
using Jackett.Common.Utils.Clients;
|
using Jackett.Common.Utils.Clients;
|
||||||
using NLog;
|
using NLog;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
using FilterFunc = Jackett.Common.Utils.FilterFunc;
|
||||||
|
|
||||||
namespace Jackett.Common.Services
|
namespace Jackett.Common.Services
|
||||||
{
|
{
|
||||||
|
@ -29,6 +32,7 @@ namespace Jackett.Common.Services
|
||||||
|
|
||||||
private readonly Dictionary<string, IIndexer> indexers = new Dictionary<string, IIndexer>();
|
private readonly Dictionary<string, IIndexer> indexers = new Dictionary<string, IIndexer>();
|
||||||
private AggregateIndexer aggregateIndexer;
|
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
|
// 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)
|
// (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();
|
MigrateRenamedIndexers();
|
||||||
InitIndexers();
|
InitIndexers();
|
||||||
InitCardigannIndexers(path);
|
InitCardigannIndexers(path);
|
||||||
InitAggregateIndexer();
|
InitMetaIndexers();
|
||||||
RemoveLegacyConfigurations();
|
RemoveLegacyConfigurations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,28 +222,25 @@ namespace Jackett.Common.Services
|
||||||
logger.Info($"Loaded {indexers.Count} indexers in total");
|
logger.Info($"Loaded {indexers.Count} indexers in total");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitAggregateIndexer()
|
public void InitMetaIndexers()
|
||||||
{
|
{
|
||||||
var omdbApiKey = serverConfig.OmdbApiKey;
|
var (fallbackStrategyProvider, resultFilterProvider) = GetStrategyProviders();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Adding aggregate indexer ('all' indexer) ...");
|
logger.Info("Adding aggregate indexer ('all' indexer) ...");
|
||||||
aggregateIndexer = new AggregateIndexer(fallbackStrategyProvider, resultFilterProvider, configService, webClient, logger, protectionService, cacheService)
|
aggregateIndexer = new AggregateIndexer(fallbackStrategyProvider, resultFilterProvider, configService, webClient, logger, protectionService, cacheService)
|
||||||
{
|
{
|
||||||
Indexers = indexers.Values
|
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()
|
public void RemoveLegacyConfigurations()
|
||||||
|
@ -271,16 +272,10 @@ namespace Jackett.Common.Services
|
||||||
This may stop working in the future.");
|
This may stop working in the future.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexers.ContainsKey(realName))
|
return GetWebIndexer(realName);
|
||||||
return indexers[realName];
|
|
||||||
|
|
||||||
if (realName == "all")
|
|
||||||
return aggregateIndexer;
|
|
||||||
|
|
||||||
logger.Error($"Request for unknown indexer: {realName}");
|
|
||||||
throw new Exception($"Unknown indexer: {realName}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IWebIndexer GetWebIndexer(string name)
|
public IWebIndexer GetWebIndexer(string name)
|
||||||
{
|
{
|
||||||
if (indexers.ContainsKey(name))
|
if (indexers.ContainsKey(name))
|
||||||
|
@ -289,6 +284,12 @@ namespace Jackett.Common.Services
|
||||||
if (name == "all")
|
if (name == "all")
|
||||||
return aggregateIndexer;
|
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}");
|
logger.Error($"Request for unknown indexer: {name}");
|
||||||
throw new Exception($"Unknown indexer: {name}");
|
throw new Exception($"Unknown indexer: {name}");
|
||||||
}
|
}
|
||||||
|
@ -318,5 +319,47 @@ namespace Jackett.Common.Services
|
||||||
configService.Delete(indexer);
|
configService.Delete(indexer);
|
||||||
indexer.Unconfigure();
|
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();
|
IEnumerable<IIndexer> GetAllIndexers();
|
||||||
|
|
||||||
void InitIndexers(IEnumerable<string> path);
|
void InitIndexers(IEnumerable<string> path);
|
||||||
void InitAggregateIndexer();
|
void InitMetaIndexers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 manualResult = new ManualSearchResult();
|
||||||
|
|
||||||
var trackers = CurrentIndexer is BaseMetaIndexer
|
var trackers = CurrentIndexer is BaseMetaIndexer
|
||||||
? (CurrentIndexer as BaseMetaIndexer).Indexers.Where(t => t.IsConfigured)
|
? (CurrentIndexer as BaseMetaIndexer).ValidIndexers
|
||||||
: (new[] { CurrentIndexer });
|
: (new[] { CurrentIndexer });
|
||||||
|
|
||||||
// Filter current trackers list on Tracker query parameter if available
|
// Filter current trackers list on Tracker query parameter if available
|
||||||
|
|
|
@ -132,7 +132,7 @@ namespace Jackett.Server.Controllers
|
||||||
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
|
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
|
||||||
configService.SaveConfig(serverConfig);
|
configService.SaveConfig(serverConfig);
|
||||||
// HACK
|
// HACK
|
||||||
indexerService.InitAggregateIndexer();
|
indexerService.InitMetaIndexers();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.proxy_type != serverConfig.ProxyType ||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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 Task TestIndexer(string name) => throw new NotImplementedException();
|
||||||
|
|
||||||
public void InitAggregateIndexer() => throw new NotImplementedException();
|
public void InitMetaIndexers() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue