mirror of
https://github.com/Jackett/Jackett
synced 2024-12-31 12:18:37 +00:00
[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
30 changed files with 1091 additions and 170 deletions
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue