Feature/new api (#1584)

* Introducing API v2

There were multiple inconsistencies in the old API and I have been
toying with the idea to replace it. This will suck for everyone who was
building on top of the Jackett API, however as it was probably too
painful to do so I'd say no one really tried.

Now API v2.0 should be much more constistent as it uses DTObjects, and
instead of manually constructing a json response it is handled by the
ASP.NET web api. It is much more RESTful than it was, proper GET
endpoints are introduced, and updating resources are now done via POST -
it might be improved by introducing other type of REST methods.

I know this sucks as completely breaks backward compatibility, however
it'll probably make it easier to maintain and build on top of in the
long run.

* Use DELETE method to unconfigure an indexer

* Remove debugging format from NLog

* Fixing an null exception

* Properly implementing IExceptionFilter interface

* Enable adding public indexers without configuration

* Fix missing manual search results

* Basic modularization of the JS API

* Introduce API versioning

* Fix redirects to the dashboard

* Cleaning up a little bit

* Revamping Torznab and Potato as well

* Move preconditions to FilterAttributes and simplify logic

* Remove legacy filtering... will move to IResultFilter

* Minor adjustment on results interface

* Use Interpolated strings in ResultsController

* DTO-ify

* Remove fallback logic from potato results

* DTO everywhere!!!

* DTO-ify everything!

* I hope this is my last piece of modification to this PR

* Remove test variables...

* Left out a couple conflicts... It's late
This commit is contained in:
chibidev 2017-08-08 17:02:16 +02:00 committed by kaso17
parent dba63857e4
commit 720b5971d3
41 changed files with 1557 additions and 1622 deletions

View File

@ -26,6 +26,13 @@ $(document).ready(function () {
$.ajaxSetup({ cache: false });
window.jackettIsLocal = window.location.hostname === '127.0.0.1';
Handlebars.registerHelper('if_eq', function(a, b, opts) {
if (a == b)
return opts.fn(this);
else
return opts.inverse(this);
});
bindUIButtons();
loadJackettSettings();
openSearchIfNecessary();
@ -47,39 +54,36 @@ function insertWordWrap(str) {
}
function getJackettConfig(callback) {
var jqxhr = $.get("get_jackett_config", function (data) {
callback(data);
}).fail(function () {
api.getServerConfig(callback).fail(function () {
doNotify("Error loading Jackett settings, request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
}
function loadJackettSettings() {
getJackettConfig(function (data) {
$("#api-key-input").val(data.config.api_key);
$(".api-key-text").text(data.config.api_key);
$("#api-key-input").val(data.api_key);
$(".api-key-text").text(data.api_key);
$("#app-version").html(data.app_version);
$("#jackett-port").val(data.config.port);
$("#jackett-basepathoverride").val(data.config.basepathoverride);
basePath = data.config.basepathoverride;
$("#jackett-port").val(data.port);
$("#jackett-basepathoverride").val(data.basepathoverride);
basePath = data.basepathoverride;
if (basePath === null || basePath === undefined) {
basePath = '';
}
$("#jackett-savedir").val(data.config.blackholedir);
$("#jackett-allowext").attr('checked', data.config.external);
$("#jackett-allowupdate").attr('checked', data.config.updatedisabled);
$("#jackett-prerelease").attr('checked', data.config.prerelease);
$("#jackett-logging").attr('checked', data.config.logging);
$("#jackett-omdbkey").val(data.config.omdbkey);
var password = data.config.password;
$("#jackett-savedir").val(data.blackholedir);
$("#jackett-allowext").attr('checked', data.external);
$("#jackett-allowupdate").attr('checked', data.updatedisabled);
$("#jackett-prerelease").attr('checked', data.prerelease);
$("#jackett-logging").attr('checked', data.logging);
$("#jackett-omdbkey").val(data.omdbkey);
var password = data.password;
$("#jackett-adminpwd").val(password);
if (password != null && password != '') {
$("#logoutBtn").show();
}
$.each(data.config.notices, function (index, value) {
$.each(data.notices, function (index, value) {
console.log(value);
doNotify(value, "danger", "glyphicon glyphicon-alert", false);
})
@ -90,15 +94,15 @@ function loadJackettSettings() {
function reloadIndexers() {
$('#indexers').hide();
var jqxhr = $.get("get_indexers", function (data) {
api.getAllIndexers(function (data) {
indexers = data;
configuredIndexers = [];
unconfiguredIndexers = [];
for (var i = 0; i < data.items.length; i++) {
var item = data.items[i];
item.torznab_host = resolveUrl(basePath + "/torznab/" + item.id);
item.potato_host = resolveUrl(basePath + "/potato/" + item.id);
for (var i = 0; i < data.length; i++) {
var item = data[i];
item.torznab_host = resolveUrl(basePath + "/api/v2.0/indexers/" + item.id + "/results/torznab/");
item.potato_host = resolveUrl(basePath + "/api/v2.0/indexers/" + item.id + "/results/potato/");
if (item.last_error)
item.state = "error";
else
@ -117,16 +121,13 @@ function reloadIndexers() {
item.type_icon_content = "";
}
var main_cats_list = [];
for (var catID in item.caps) {
if (catID >= 100000)
continue; // skip custom cats
var cat = item.caps[catID];
var mainCat = cat.split("/")[0];
main_cats_list.push(mainCat);
}
var main_cats_list = item.caps.filter(function(c) {
return c.ID < 100000;
}).map(function(c) {
return c.Name.split("/")[0];
});
item.mains_cats = $.unique(main_cats_list).join(", ");
if (item.configured)
configuredIndexers.push(item);
else
@ -176,16 +177,40 @@ function displayConfiguredIndexersList(indexers) {
function displayUnconfiguredIndexersList() {
var UnconfiguredIndexersDialog = $($("#select-indexer").html());
var indexersTemplate = Handlebars.compile($("#unconfigured-indexer-table").html());
var indexersTable = $(indexersTemplate({ indexers: unconfiguredIndexers, total_unconfigured_indexers: unconfiguredIndexers.length }));
indexersTable.find('.indexer-setup').each(function (i, btn) {
var $btn = $(btn);
var id = $btn.data("id");
var link = $btn.data("link");
$btn.click(function () {
var indexer = unconfiguredIndexers[i];
$(btn).click(function () {
$('#select-indexer-modal').modal('hide').on('hidden.bs.modal', function (e) {
displayIndexerSetup(id, link);
displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks);
});
});
});
indexersTable.find('.indexer-add').each(function (i, btn) {
$(btn).click(function () {
$('#select-indexer-modal').modal('hide').on('hidden.bs.modal', function (e) {
var indexerId = $(btn).attr("data-id");
api.getIndexerConfig(indexerId, function (data) {
if (data.result !== undefined && data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
}
api.updateIndexerConfig(indexerId, data, function (data) {
if (data == undefined) {
reloadIndexers();
doNotify("Successfully configured " + name, "success", "glyphicon glyphicon-ok");
} else if (data.result == "error") {
if (data.config) {
populateConfigItems(configForm, data.config);
}
doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert");
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
});
});
});
});
@ -248,7 +273,7 @@ function displayUnconfiguredIndexersList() {
var undefindexers = UnconfiguredIndexersDialog.find('#unconfigured-indexers');
undefindexers.append(indexersTable);
UnconfiguredIndexersDialog.on('shown.bs.modal', function() {
$(this).find('div.dataTables_filter input').focusWithoutScrolling();
});
@ -258,7 +283,7 @@ function displayUnconfiguredIndexersList() {
});
$("#modals").append(UnconfiguredIndexersDialog);
UnconfiguredIndexersDialog.modal("show");
}
@ -315,12 +340,11 @@ function prepareDeleteButtons(element) {
var $btn = $(btn);
var id = $btn.data("id");
$btn.click(function () {
var jqxhr = $.post("delete_indexer", JSON.stringify({ indexer: id }), function (data) {
if (data.result == "error") {
doNotify("Delete error for " + id + "\n" + data.error, "danger", "glyphicon glyphicon-alert");
}
else {
api.deleteIndexer(id, function (data) {
if (data == undefined) {
doNotify("Deleted " + id, "success", "glyphicon glyphicon-ok");
} else if (data.result == "error") {
doNotify("Delete error for " + id + "\n" + data.error, "danger", "glyphicon glyphicon-alert");
}
}).fail(function () {
doNotify("Error deleting indexer, request to Jackett server error", "danger", "glyphicon glyphicon-alert");
@ -343,11 +367,9 @@ function prepareSearchButtons(element) {
function prepareSetupButtons(element) {
element.find('.indexer-setup').each(function (i, btn) {
var $btn = $(btn);
var id = $btn.data("id");
var link = $btn.data("link");
$btn.click(function () {
displayIndexerSetup(id, link);
var indexer = configuredIndexers[i];
$(btn).click(function () {
displayIndexerSetup(indexer.id, indexer.name, indexer.caps, indexer.link, indexer.alternativesitelinks);
});
});
}
@ -388,19 +410,18 @@ function updateTestState(id, state, message, parent)
function testIndexer(id, notifyResult) {
var indexers = $('#indexers');
updateTestState(id, "inprogres", null, indexers);
if (notifyResult)
doNotify("Test started for " + id, "info", "glyphicon glyphicon-transfer");
var jqxhr = $.post("test_indexer", JSON.stringify({ indexer: id }), function (data) {
if (data.result == "error") {
updateTestState(id, "error", data.error, indexers);
if (notifyResult)
doNotify("Test failed for " + id + ": \n" + data.error, "danger", "glyphicon glyphicon-alert");
}
else {
api.testIndexer(id, function (data) {
if (data == undefined) {
updateTestState(id, "success", "Test successful", indexers);
if (notifyResult)
doNotify("Test successful for " + id, "success", "glyphicon glyphicon-ok");
} else if (data.result == "error") {
updateTestState(id, "error", data.error, indexers);
if (notifyResult)
doNotify("Test failed for " + id + ": \n" + data.error, "danger", "glyphicon glyphicon-alert");
}
}).fail(function () {
doNotify("Error testing indexer, request to Jackett server error", "danger", "glyphicon glyphicon-alert");
@ -421,16 +442,14 @@ function prepareTestButtons(element) {
});
}
function displayIndexerSetup(id, link) {
var jqxhr = $.post("get_config_form", JSON.stringify({ indexer: id }), function (data) {
if (data.result == "error") {
function displayIndexerSetup(id, name, caps, link, alternativesitelinks) {
api.getIndexerConfig(id, function (data) {
if (data.result !== undefined && data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
}
populateSetupForm(id, data.name, data.config, data.caps, link, data.alternativesitelinks);
populateSetupForm(id, name, data, caps, link, alternativesitelinks);
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
@ -569,25 +588,23 @@ function populateSetupForm(indexerId, name, config, caps, link, alternativesitel
var configForm = newConfigModal(name, config, caps, link, alternativesitelinks);
var $goButton = configForm.find(".setup-indexer-go");
$goButton.click(function () {
var data = { indexer: indexerId, name: name };
data.config = getConfigModalJson(configForm);
var data = getConfigModalJson(configForm);
var originalBtnText = $goButton.html();
$goButton.prop('disabled', true);
$goButton.html($('#spinner').html());
var jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) {
if (data.result == "error") {
api.updateIndexerConfig(indexerId, data, function (data) {
if (data == undefined) {
configForm.modal("hide");
reloadIndexers();
doNotify("Successfully configured " + name, "success", "glyphicon glyphicon-ok");
} else if (data.result == "error") {
if (data.config) {
populateConfigItems(configForm, data.config);
}
doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert");
}
else {
configForm.modal("hide");
reloadIndexers();
doNotify("Successfully configured " + data.name, "success", "glyphicon glyphicon-ok");
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
}).always(function () {
@ -640,7 +657,7 @@ function clearNotifications() {
}
function updateReleasesRow(row)
{
{
var labels = $(row).find("span.release-labels");
var TitleLink = $(row).find("td.Title > a");
var IMDBId = $(row).data("imdb");
@ -743,22 +760,29 @@ function showSearch(selectedIndexer, query) {
// We are searchin already
return;
}
var searchString = releaseDialog.find('#searchquery').val();
var queryObj = {
Query: releaseDialog.find('#searchquery').val(),
Query: searchString,
Category: releaseDialog.find('#searchCategory').val(),
Tracker: releaseDialog.find('#searchTracker').val().replace("'", "").replace("'", ""),
};
window.location.hash = "search=" + searchString;
$('#jackett-search-perform').html($('#spinner').html());
$('#searchResults div.dataTables_filter input').val("");
clearSearchResultTable($('#searchResults'));
var jqxhr = $.post("search", queryObj, function (data) {
var trackerId = queryObj.Tracker;
if (trackerId == null || trackerId == "")
trackerId = "all";
api.resultsForIndexer(trackerId, queryObj, function (data) {
for (var i = 0; i < data.Results.length; i++) {
var item = data.Results[i];
item.Title = insertWordWrap(item.Title);
item.CategoryDesc = insertWordWrap(item.CategoryDesc);
}
$('#jackett-search-perform').html($('#search-button-ready').html());
var searchResults = $('#searchResults');
searchResults.empty();
@ -824,7 +848,7 @@ function updateSearchResultTable(element, results) {
if ("deadfilter" in sValue)
settings.deadfilter = sValue.deadfilter;
},
"dom": "lfr<\"dataTables_deadfilter\">tip",
"stateSave": true,
"bAutoWidth": false,
@ -936,7 +960,7 @@ function bindUIButtons() {
});
$("#jackett-show-releases").click(function () {
var jqxhr = $.get("GetCache", function (data) {
api.getServerCache(function (data) {
for (var i = 0; i < data.length; i++) {
var item = data[i];
item.Title = insertWordWrap(item.Title);
@ -950,7 +974,7 @@ function bindUIButtons() {
releaseDialog.on('hidden.bs.modal', function (e) {
$('#indexers div.dataTables_filter input').focusWithoutScrolling();
});
table.DataTable(
{
"stateSave": true,
@ -1030,23 +1054,23 @@ function bindUIButtons() {
$("#jackett-show-search").click(function () {
showSearch(null);
window.location.hash = "search";
});
$("#view-jackett-logs").click(function () {
var jqxhr = $.get("GetLogs", function (data) {
api.getServerLogs(function (data) {
var releaseTemplate = Handlebars.compile($("#jackett-logs").html());
var item = { logs: data };
var releaseDialog = $(releaseTemplate(item));
$("#modals").append(releaseDialog);
releaseDialog.modal("show");
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
});
$("#change-jackett-port").click(function () {
var jackett_port = $("#jackett-port").val();
var jackett_port = Number($("#jackett-port").val());
var jackett_basepathoverride = $("#jackett-basepathoverride").val();
var jackett_external = $("#jackett-allowext").is(':checked');
var jackett_update = $("#jackett-allowupdate").is(':checked');
@ -1063,8 +1087,8 @@ function bindUIButtons() {
basepathoverride: jackett_basepathoverride,
omdbkey: jackett_omdb_key
};
var jqxhr = $.post("set_config", JSON.stringify(jsonObject), function (data) {
if (data.result == "error") {
api.updateServerConfig(function (data) {
if (data !== undefined && data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
} else {
@ -1072,7 +1096,6 @@ function bindUIButtons() {
window.setTimeout(function () {
window.location.reload(true);
}, 3000);
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
@ -1080,7 +1103,7 @@ function bindUIButtons() {
});
$("#trigger-updater").click(function () {
var jqxhr = $.get("trigger_update", function (data) {
api.updateServer(function (data) {
if (data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
@ -1094,20 +1117,17 @@ function bindUIButtons() {
$("#change-jackett-password").click(function () {
var password = $("#jackett-adminpwd").val();
var jsonObject = { password: password };
var jqxhr = $.post("set_admin_password", JSON.stringify(jsonObject), function (data) {
if (data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
} else {
api.updateAdminPassword(password, function (data) {
if (data == undefined) {
doNotify("Admin password has been set.", "success", "glyphicon glyphicon-ok");
window.setTimeout(function () {
window.location = window.location.pathname;
}, 1000);
} else if (data.result == "error") {
doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert");
return;
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");

View File

@ -2,22 +2,22 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta charset="utf-8" />
<link rel="apple-touch-icon" href="../apple-touch-icon.png"/>
<link rel="apple-touch-icon" sizes="57x57" href="../apple-touch-icon-57x57.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="../apple-touch-icon-72x72.png"/>
<link rel="apple-touch-icon" sizes="76x76" href="../apple-touch-icon-76x76.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="../apple-touch-icon-114x114.png"/>
<link rel="apple-touch-icon" sizes="120x120" href="../apple-touch-icon-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="../apple-touch-icon-144x144"/>
<link rel="apple-touch-icon" sizes="152x152" href="../apple-touch-icon-152x152.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="../apple-touch-icon-180x180.png"/>
<link rel="apple-touch-icon" href="../apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="../apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="../apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="../apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="../apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="../apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="../apple-touch-icon-144x144" />
<link rel="apple-touch-icon" sizes="152x152" href="../apple-touch-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="../apple-touch-icon-180x180.png" />
<link rel="mask-icon" href="jackett_medium.png" color="#35c5f4">
<link rel="icon" type="image/ico" href="../favicon.ico"/>
<link rel="icon" type="image/ico" href="../favicon.ico" />
<link rel='shortcut icon' type='image/x-icon' href='../favicon.ico' />
<script src="../libs/filesize.min.js"></script>
<script src="../libs/jquery.min.js"></script>
@ -55,13 +55,13 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add indexer
</button>
<button id="jackett-show-search" class="btn btn-success btn-sm">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span> Manual Search
<span class="glyphicon glyphicon-search" aria-hidden="true"></span> Manual Search
</button>
<button id="jackett-show-releases" class="btn btn-primary btn-sm">
<i class="fa fa-database"></i> View cached releases
</button>
<button id="jackett-test-all" class="btn btn-warning btn-sm">
<span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span> Test All
<span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span> Test All
</button>
</div>
<h3>Configured Indexers</h3>
@ -187,11 +187,11 @@
<div class="setup-item-inputselect">
<select class="form-control" data-id="{{id}}">
{{#each options}}
{{#ifCond ../value @key}}
<option value="{{@key}}" selected>{{this}}</option>
{{else}}
<option value="{{@key}}">{{this}}</option>
{{/ifCond}}
{{#ifCond ../value @key}}
<option value="{{@key}}" selected>{{this}}</option>
{{else}}
<option value="{{@key}}">{{this}}</option>
{{/ifCond}}
{{/each}}
</select>
</div>
@ -216,7 +216,7 @@
This indexer has multiple known URLs which you can change above:
<ul>
{{#each alternativesitelinks}}
<li>{{this}}</li>
<li>{{this}}</li>
{{/each}}
</ul>
</div>
@ -291,9 +291,14 @@
<td class="fit">{{language}}</td>
<td class="fit">
<div class="indexer-buttons">
<button title="Configure" class="btn btn-success btn-xs indexer-setup" data-id="{{id}}" data-link="{{site_link}}">
<button title="Configure" class="btn btn-primary btn-xs indexer-setup" data-id="{{id}}" data-link="{{site_link}}">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
</button>
{{#if_eq type "public"}}
<button title="Add" class="btn btn-success btn-xs indexer-add" data-id="{{id}}" data-link="{{site_link}}">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</button>
{{/if_eq}}
</div>
</td>
</tr>
@ -412,7 +417,7 @@
<div class="modal-body">
<p>You can search all configured indexers from this screen.</p>
<label for="text">Query</label>
<input type="text" name="query" id="searchquery"/>
<input type="text" name="query" id="searchquery" />
<label for="tracker">Tracker</label>
<select name="tracker" id="searchTracker">
<option value="">-- All --</option>
@ -567,7 +572,7 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">{{title}} - <a target="_blank" href="{{link}}">{{link}}</a></h4>
<h4 class="modal-title">{{title}} - <a target="_blank" href="{{link}}">{{link}}</a></h4>
</div>
<div class="modal-body">
<form class="config-setup-form"></form>
@ -608,6 +613,7 @@
<span class="fa fa-search"></span>
</script>
<script src="../libs/api.js"></script>
<script src="../custom.js"></script>
</body>
</html>

View File

@ -0,0 +1,88 @@
var api = {
version: "2.0",
root: "/api",
getApiPath: function(category, action) {
var path = this.root + "/v" + this.version + "/" + category;
if (action !== undefined)
path = path + "/" + action
return path;
},
getAllIndexers: function(callback) {
return $.get(this.getApiPath("indexers"), callback);
},
getServerConfig: function(callback) {
return $.get(this.getApiPath("server", "config"), callback);
},
getIndexerConfig: function(indexerId, callback) {
return $.get(this.getApiPath("indexers", indexerId + "/config"), callback);
},
updateIndexerConfig: function(indexerId, config, callback) {
return $.ajax({
url: this.getApiPath("indexers", indexerId + "/config"),
type: 'POST',
data: JSON.stringify(config),
dataType: 'json',
contentType: 'application/json',
cache: false,
success: callback
});
},
deleteIndexer: function(indexerId, callback) {
return $.ajax({
url: this.getApiPath("indexers", indexerId),
type: 'DELETE',
cache: false,
success: callback
});
},
testIndexer: function(indexerId, callback) {
return $.post(this.getApiPath("indexers", indexerId + "/test"), callback);
},
resultsForIndexer: function(indexerId, query, callback) {
return $.get(this.getApiPath("indexers", indexerId + "/results"), query, callback);
},
getServerCache: function(callback) {
return $.get(this.getApiPath("indexers", "cache"), callback);
},
getServerLogs: function(callback) {
return $.get(this.getApiPath("server", "logs"), callback);
},
updateServerConfig: function(serverConfig, callback) {
return $.ajax({
url: this.getApiPath("server", "config"),
type: 'POST',
data: JSON.stringify(serverConfig),
dataType: 'json',
contentType: 'application/json',
cache: false,
success: callback
});
},
updateServer: function(callback) {
return $.post(this.getApiPath("server", "update"), callback);
},
updateAdminPassword: function(password, callback) {
return $.ajax({
url: this.getApiPath("server", "adminpassword"),
type: 'POST',
data: JSON.stringify(password),
dataType: 'json',
contentType: 'application/json',
cache: false,
success: callback
});
}
}

View File

@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<script src="jquery-2.1.3.min.js"></script>
<script src="common.js"></script>
<style>
#formItemTemplateContainer {
display: none;
}
</style>
<title></title>
</head>
<body>
<script>
$(function () {
var urlParams = getUrlParams();
var jqxhr = $.post("get_config_form", JSON.stringify({ indexer: urlParams.indexer }), function (data) {
populateForm(data.config);
})
.fail(function () {
alert("error");
});
$("#loginButton").click(function () {
var data = { indexer: urlParams.indexer, config: {} };
$("#formItems").children().each(function (i, item) {
var $item = $(item);
var type = $item.data("type");
var id = $item.data("id");
var $valEl = $item.find(".formItemValue").children().first();
switch (type) {
case "inputstring":
data.config[id] = $valEl.val();
break;
case "inputbool":
data.config[id] = $valEl.val();
break;
case "inputselect":
data.config[id] = $valEl.val();
break;
}
});
var jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) {
if (data.result == "error") {
if (data.config) {
populateForm(data.config);
}
alert(data.error);
}
})
.fail(function () {
alert("error");
});
});
});
function populateForm(data) {
$("#formItems").empty();
for (var i = 0; i < data.length; i++) {
$("#formItems").append(createFormItem(data[i]));
}
}
function createFormItem(itemData) {
var $template = $("#formItemTemplate").clone();
$template.attr("id", "item" + itemData.id);
$template.data("id", itemData.id);
$template.data("type", itemData.type);
$template.attr("data-type", itemData.type);
$template.data("value", itemData.value);
$template.find(".formItemName").text(itemData.name);
$valueElement = $template.find(".formItemValue");
switch (itemData.type) {
case "inputstring":
$valueElement.append($("<input type='text'></input>").val(itemData.value));
break;
case "inputbool":
$valueElement.append($("<input type='checkbox'></input>").prop("checked", itemData.value));
break;
case "displayimage":
$valueElement.append($("<img src='" + itemData.value + "'>"));
break;
case "displayinfo":
$valueElement.append($("<span></span>").text(itemData.value));
break;
}
return $template;
}
</script>
<div id="formItems">
</div>
<button id="loginButton">Login</button>
<div id="formItemTemplateContainer">
<div id="formItemTemplate">
<div class="formItemName"></div>
<div class="formItemValue"></div>
</div>
</div>
</body>
</html>

View File

@ -1,610 +0,0 @@
using Autofac;
using AutoMapper;
using Jackett.Indexers;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Results;
using System.Web.Security;
using System.Windows.Forms;
namespace Jackett.Controllers
{
[RoutePrefix("admin")]
[JackettAuthorized]
[JackettAPINoCache]
public class AdminController : ApiController
{
private IConfigurationService config;
private IIndexerManagerService indexerService;
private IServerService serverService;
private ISecuityService securityService;
private IProcessService processService;
private ICacheService cacheService;
private Logger logger;
private ILogCacheService logCache;
private IUpdateService updater;
public AdminController(IConfigurationService config, IIndexerManagerService i, IServerService ss, ISecuityService s, IProcessService p, ICacheService c, Logger l, ILogCacheService lc, IUpdateService u)
{
this.config = config;
indexerService = i;
serverService = ss;
securityService = s;
processService = p;
cacheService = c;
logger = l;
logCache = lc;
updater = u;
}
private async Task<JToken> ReadPostDataJson()
{
var content = await Request.Content.ReadAsStringAsync();
return JObject.Parse(content);
}
private HttpResponseMessage GetFile(string path)
{
var result = new HttpResponseMessage(HttpStatusCode.OK);
var mappedPath = Path.Combine(config.GetContentFolder(), path);
var stream = new FileStream(mappedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(mappedPath));
return result;
}
[HttpGet]
[AllowAnonymous]
public RedirectResult Logout()
{
var ctx = Request.GetOwinContext();
var authManager = ctx.Authentication;
authManager.SignOut("ApplicationCookie");
return Redirect("Admin/Dashboard");
}
[HttpGet]
[HttpPost]
[AllowAnonymous]
public async Task<HttpResponseMessage> Dashboard()
{
if (Request.RequestUri.Query != null && Request.RequestUri.Query.Contains("logout"))
{
var file = GetFile("login.html");
securityService.Logout(file);
return file;
}
if (securityService.CheckAuthorised(Request))
{
return GetFile("index.html");
}
else
{
var formData = await Request.Content.ReadAsFormDataAsync();
if (formData != null && securityService.HashPassword(formData["password"]) == serverService.Config.AdminPassword)
{
var file = GetFile("index.html");
securityService.Login(file);
return file;
}
else
{
return GetFile("login.html");
}
}
}
[Route("set_admin_password")]
[HttpPost]
public async Task<IHttpActionResult> SetAdminPassword()
{
var jsonReply = new JObject();
try
{
var postData = await ReadPostDataJson();
var password = (string)postData["password"];
if (string.IsNullOrEmpty(password))
{
serverService.Config.AdminPassword = string.Empty;
}
else
{
serverService.Config.AdminPassword = securityService.HashPassword(password);
}
serverService.SaveConfig();
jsonReply["result"] = "success";
}
catch (Exception ex)
{
logger.Error(ex, "Exception in SetAdminPassword");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("get_config_form")]
[HttpPost]
public async Task<IHttpActionResult> GetConfigForm()
{
var jsonReply = new JObject();
try
{
var postData = await ReadPostDataJson();
var indexer = indexerService.GetIndexer((string)postData["indexer"]);
var config = await indexer.GetConfigurationForSetup();
jsonReply["config"] = config.ToJson(null);
jsonReply["caps"] = indexer.TorznabCaps.CapsToJson();
jsonReply["name"] = indexer.DisplayName;
jsonReply["alternativesitelinks"] = JToken.FromObject(indexer.AlternativeSiteLinks);
jsonReply["result"] = "success";
}
catch (Exception ex)
{
logger.Error(ex, "Exception in GetConfigForm");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("configure_indexer")]
[HttpPost]
public async Task<IHttpActionResult> Configure()
{
var jsonReply = new JObject();
IIndexer indexer = null;
try
{
var postData = await ReadPostDataJson();
string indexerString = (string)postData["indexer"];
indexer = indexerService.GetIndexer((string)postData["indexer"]);
jsonReply["name"] = indexer.DisplayName;
var configurationResult = await indexer.ApplyConfiguration(postData["config"]);
if (configurationResult == IndexerConfigurationStatus.RequiresTesting)
{
await indexerService.TestIndexer((string)postData["indexer"]);
}
else if (configurationResult == IndexerConfigurationStatus.Failed)
{
throw new Exception("Configuration Failed");
}
jsonReply["result"] = "success";
}
catch (Exception ex)
{
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
var baseIndexer = indexer as BaseIndexer;
if (null != baseIndexer)
baseIndexer.ResetBaseConfig();
if (ex is ExceptionWithConfigData)
{
jsonReply["config"] = ((ExceptionWithConfigData)ex).ConfigData.ToJson(null, false);
}
else
{
logger.Error(ex, "Exception in Configure");
}
}
return Json(jsonReply);
}
[Route("get_indexers")]
[HttpGet]
public IHttpActionResult Indexers()
{
var jsonReply = new JObject();
try
{
jsonReply["result"] = "success";
JArray items = new JArray();
foreach (var indexer in indexerService.GetAllIndexers())
{
var item = new JObject();
item["id"] = indexer.ID;
item["name"] = indexer.DisplayName;
item["description"] = indexer.DisplayDescription;
item["type"] = indexer.Type;
item["configured"] = indexer.IsConfigured;
item["site_link"] = indexer.SiteLink;
item["language"] = indexer.Language;
item["last_error"] = indexer.LastError;
item["potatoenabled"] = indexer.TorznabCaps.Categories.Select(c => c.ID).Any(i => PotatoController.MOVIE_CATS.Contains(i));
var caps = new JObject();
foreach (var cap in indexer.TorznabCaps.Categories)
caps[cap.ID.ToString()] = cap.Name;
item["caps"] = caps;
items.Add(item);
}
jsonReply["items"] = items;
}
catch (Exception ex)
{
logger.Error(ex, "Exception in get_indexers");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("test_indexer")]
[HttpPost]
public async Task<IHttpActionResult> Test()
{
JToken jsonReply = new JObject();
IIndexer indexer = null;
try
{
var postData = await ReadPostDataJson();
string indexerString = (string)postData["indexer"];
indexer = indexerService.GetIndexer(indexerString);
await indexerService.TestIndexer(indexerString);
jsonReply["name"] = indexer.DisplayName;
jsonReply["result"] = "success";
indexer.LastError = null;
}
catch (Exception ex)
{
var msg = ex.Message;
if (ex.InnerException != null)
msg += ": " + ex.InnerException.Message;
logger.Error(ex, "Exception in test_indexer");
jsonReply["result"] = "error";
jsonReply["error"] = msg;
if (indexer != null)
indexer.LastError = msg;
}
return Json(jsonReply);
}
[Route("delete_indexer")]
[HttpPost]
public async Task<IHttpActionResult> Delete()
{
var jsonReply = new JObject();
try
{
var postData = await ReadPostDataJson();
string indexerString = (string)postData["indexer"];
indexerService.DeleteIndexer(indexerString);
}
catch (Exception ex)
{
logger.Error(ex, "Exception in delete_indexer");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("trigger_update")]
[HttpGet]
public IHttpActionResult TriggerUpdates()
{
var jsonReply = new JObject();
updater.CheckForUpdatesNow();
return Json(jsonReply);
}
[Route("get_jackett_config")]
[HttpGet]
public IHttpActionResult GetConfig()
{
var jsonReply = new JObject();
try
{
var cfg = new JObject();
cfg["notices"] = JToken.FromObject(serverService.notices);
cfg["port"] = serverService.Config.Port;
cfg["external"] = serverService.Config.AllowExternal;
cfg["api_key"] = serverService.Config.APIKey;
cfg["blackholedir"] = serverService.Config.BlackholeDir;
cfg["updatedisabled"] = serverService.Config.UpdateDisabled;
cfg["prerelease"] = serverService.Config.UpdatePrerelease;
cfg["password"] = string.IsNullOrEmpty(serverService.Config.AdminPassword) ? string.Empty : serverService.Config.AdminPassword.Substring(0, 10);
cfg["logging"] = Startup.TracingEnabled;
cfg["basepathoverride"] = serverService.Config.BasePathOverride;
cfg["omdbkey"] = serverService.Config.OmdbApiKey;
jsonReply["config"] = cfg;
jsonReply["app_version"] = config.GetVersion();
jsonReply["result"] = "success";
}
catch (Exception ex)
{
logger.Error(ex, "Exception in get_jackett_config");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("set_config")]
[HttpPost]
public async Task<IHttpActionResult> SetConfig()
{
var originalPort = Engine.Server.Config.Port;
var originalAllowExternal = Engine.Server.Config.AllowExternal;
var jsonReply = new JObject();
try
{
var postData = await ReadPostDataJson();
int port = (int)postData["port"];
bool external = (bool)postData["external"];
string saveDir = (string)postData["blackholedir"];
bool updateDisabled = (bool)postData["updatedisabled"];
bool preRelease = (bool)postData["prerelease"];
bool logging = (bool)postData["logging"];
string basePathOverride = (string)postData["basepathoverride"];
string omdbApiKey = (string)postData["omdbkey"];
Engine.Server.Config.UpdateDisabled = updateDisabled;
Engine.Server.Config.UpdatePrerelease = preRelease;
Engine.Server.Config.BasePathOverride = basePathOverride;
Startup.BasePath = Engine.Server.BasePath();
Engine.Server.SaveConfig();
Engine.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
Startup.TracingEnabled = logging;
if (omdbApiKey != Engine.Server.Config.OmdbApiKey)
{
Engine.Server.Config.OmdbApiKey = omdbApiKey;
Engine.Server.SaveConfig();
// HACK
indexerService.InitAggregateIndexer();
}
if (port != Engine.Server.Config.Port || external != Engine.Server.Config.AllowExternal)
{
if (ServerUtil.RestrictedPorts.Contains(port))
{
jsonReply["result"] = "error";
jsonReply["error"] = "The port you have selected is restricted, try a different one.";
return Json(jsonReply);
}
if (port < 1 || port > 65535)
{
jsonReply["result"] = "error";
jsonReply["error"] = "The port you have selected is invalid, it must be below 65535.";
return Json(jsonReply);
}
// Save port to the config so it can be picked up by the if needed when running as admin below.
Engine.Server.Config.AllowExternal = external;
Engine.Server.Config.Port = port;
Engine.Server.SaveConfig();
// On Windows change the url reservations
if (System.Environment.OSVersion.Platform != PlatformID.Unix)
{
if (!ServerUtil.IsUserAdministrator())
{
try
{
processService.StartProcessAndLog(Application.ExecutablePath, "--ReserveUrls", true);
}
catch
{
Engine.Server.Config.Port = originalPort;
Engine.Server.Config.AllowExternal = originalAllowExternal;
Engine.Server.SaveConfig();
jsonReply["result"] = "error";
jsonReply["error"] = "Failed to acquire admin permissions to reserve the new port.";
return Json(jsonReply);
}
}
else
{
serverService.ReserveUrls(true);
}
}
(new Thread(() =>
{
Thread.Sleep(500);
serverService.Stop();
Engine.BuildContainer();
Engine.Server.Initalize();
Engine.Server.Start();
})).Start();
}
if (saveDir != Engine.Server.Config.BlackholeDir)
{
if (!string.IsNullOrEmpty(saveDir))
{
if (!Directory.Exists(saveDir))
{
throw new Exception("Blackhole directory does not exist");
}
}
Engine.Server.Config.BlackholeDir = saveDir;
Engine.Server.SaveConfig();
}
jsonReply["result"] = "success";
jsonReply["port"] = port;
jsonReply["external"] = external;
}
catch (Exception ex)
{
logger.Error(ex, "Exception in set_port");
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
[Route("GetCache")]
[HttpGet]
public List<TrackerCacheResult> GetCache()
{
var results = cacheService.GetCachedResults();
ConfigureCacheResults(results);
return results;
}
private void ConfigureCacheResults(List<TrackerCacheResult> results)
{
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
foreach (var result in results)
{
var link = result.Link;
var file = StringUtil.MakeValidFileName(result.Title, '_', false) + ".torrent";
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.Server.Config.BlackholeDir))
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
}
}
[Route("GetLogs")]
[HttpGet]
public List<CachedLog> GetLogs()
{
return logCache.Logs;
}
[Route("Search")]
[HttpPost]
public ManualSearchResult Search([FromBody]AdminSearch value)
{
var results = new List<TrackerCacheResult>();
var stringQuery = new TorznabQuery();
var queryStr = value.Query;
if (queryStr != null)
{
var seasonMatch = Regex.Match(queryStr, @"S(\d{2,4})");
if (seasonMatch.Success)
{
stringQuery.Season = int.Parse(seasonMatch.Groups[1].Value);
queryStr = queryStr.Remove(seasonMatch.Index, seasonMatch.Length);
}
var episodeMatch = Regex.Match(queryStr, @"E(\d{2,4}[A-Za-z]?)");
if (episodeMatch.Success)
{
stringQuery.Episode = episodeMatch.Groups[1].Value;
queryStr = queryStr.Remove(episodeMatch.Index, episodeMatch.Length);
}
queryStr = queryStr.Trim();
}
stringQuery.SearchTerm = queryStr;
stringQuery.Categories = value.Category == 0 ? new int[0] : new int[1] { value.Category };
stringQuery.ExpandCatsToSubCats();
// try to build an IMDB Query
var imdbID = ParseUtil.GetFullImdbID(stringQuery.SanitizedSearchTerm);
TorznabQuery imdbQuery = null;
if (imdbID != null)
{
imdbQuery = new TorznabQuery()
{
ImdbID = imdbID,
Categories = stringQuery.Categories,
Season = stringQuery.Season,
Episode = stringQuery.Episode,
};
imdbQuery.ExpandCatsToSubCats();
}
var trackers = indexerService.GetAllIndexers().Where(t => t.IsConfigured).ToList();
if (!string.IsNullOrWhiteSpace(value.Tracker))
{
trackers = trackers.Where(t => t.ID == value.Tracker).ToList();
}
if (value.Category != 0)
{
trackers = trackers.Where(t => t.TorznabCaps.Categories.Select(c => c.ID).Contains(value.Category)).ToList();
}
Parallel.ForEach(trackers.ToList(), new ParallelOptions { MaxDegreeOfParallelism = 1000 }, indexer =>
{
try
{
var query = stringQuery;
// use imdb Query for trackers which support it
if (imdbQuery != null && indexer.TorznabCaps.SupportsImdbSearch)
query = imdbQuery;
var searchResults = indexer.ResultsForQuery(query).Result;
cacheService.CacheRssResults(indexer, searchResults);
foreach (var result in searchResults)
{
var item = Mapper.Map<TrackerCacheResult>(result);
item.Tracker = indexer.DisplayName;
item.TrackerId = indexer.ID;
item.Peers = item.Peers - item.Seeders; // Use peers as leechers
lock (results)
{
results.Add(item);
}
}
}
catch (Exception e)
{
logger.Error(e, "An error occured during manual search on " + indexer.DisplayName + ": " + e.Message);
}
});
ConfigureCacheResults(results);
if (trackers.Count > 1)
{
results = results.OrderByDescending(d => d.PublishDate).ToList();
}
var manualResult = new ManualSearchResult()
{
Results = results,
Indexers = trackers.Select(t => t.DisplayName).ToList()
};
if (manualResult.Indexers.Count == 0)
manualResult.Indexers = new List<string>() { "None" };
logger.Info(string.Format("Manual search for \"{0}\" on {1} with {2} results.", stringQuery.GetQueryString(), string.Join(", ", manualResult.Indexers), manualResult.Results.Count));
return manualResult;
}
}
}

View File

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using AutoMapper;
using Jackett.Indexers;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace Jackett.Controllers.V20
{
public interface IIndexerController
{
IIndexerManagerService IndexerService { get; }
IIndexer CurrentIndexer { get; set; }
}
public class RequiresIndexerAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var controller = actionContext.ControllerContext.Controller;
if (!(controller is IIndexerController))
return;
var indexerController = controller as IIndexerController;
var parameters = actionContext.RequestContext.RouteData.Values;
if (!parameters.ContainsKey("indexerId"))
{
indexerController.CurrentIndexer = null;
return;
}
var indexerId = parameters["indexerId"] as string;
if (indexerId.IsNullOrEmptyOrWhitespace())
return;
var indexerService = indexerController.IndexerService;
var indexer = indexerService.GetIndexer(indexerId);
indexerController.CurrentIndexer = indexer;
}
}
[RoutePrefix("api/v2.0/indexers")]
[JackettAuthorized]
[JackettAPINoCache]
public class IndexerApiController : ApiController, IIndexerController
{
public IIndexerManagerService IndexerService { get; private set; }
public IIndexer CurrentIndexer { get; set; }
public IndexerApiController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
{
IndexerService = indexerManagerService;
serverService = ss;
cacheService = c;
this.logger = logger;
}
[HttpGet]
[RequiresIndexer]
public async Task<IHttpActionResult> Config()
{
var config = await CurrentIndexer.GetConfigurationForSetup();
return Ok(config.ToJson(null));
}
[HttpPost]
[ActionName("Config")]
[RequiresIndexer]
public async Task UpdateConfig([FromBody]Models.DTO.ConfigItem[] config)
{
try
{
// HACK
var jsonString = JsonConvert.SerializeObject(config);
var json = JToken.Parse(jsonString);
var configurationResult = await CurrentIndexer.ApplyConfiguration(json);
if (configurationResult == IndexerConfigurationStatus.RequiresTesting)
await IndexerService.TestIndexer(CurrentIndexer.ID);
}
catch
{
var baseIndexer = CurrentIndexer as BaseIndexer;
if (null != baseIndexer)
baseIndexer.ResetBaseConfig();
throw;
}
}
[HttpGet]
[Route("")]
public IEnumerable<Models.DTO.Indexer> Indexers()
{
var dto = IndexerService.GetAllIndexers().Select(i => new Models.DTO.Indexer(i));
return dto;
}
[HttpPost]
[RequiresIndexer]
public async Task Test()
{
JToken jsonReply = new JObject();
try
{
await IndexerService.TestIndexer(CurrentIndexer.ID);
CurrentIndexer.LastError = null;
}
catch (Exception ex)
{
var msg = ex.Message;
if (ex.InnerException != null)
msg += ": " + ex.InnerException.Message;
if (CurrentIndexer != null)
CurrentIndexer.LastError = msg;
throw;
}
}
[HttpDelete]
[RequiresIndexer]
[Route("{indexerId}")]
public void Delete()
{
IndexerService.DeleteIndexer(CurrentIndexer.ID);
}
// TODO
// This should go to ServerConfigurationController
[Route("Cache")]
[HttpGet]
public List<TrackerCacheResult> Cache()
{
var results = cacheService.GetCachedResults();
ConfigureCacheResults(results);
return results;
}
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
{
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
foreach (var result in results)
{
var link = result.Link;
var file = StringUtil.MakeValidFileName(result.Title, '_', false) + ".torrent";
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.Server.Config.BlackholeDir))
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
}
}
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
}
}

View File

@ -1,166 +0,0 @@
using AutoMapper;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using Jackett.Utils.Clients;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Jackett.Indexers;
namespace Jackett.Controllers
{
[AllowAnonymous]
[JackettAPINoCache]
public class PotatoController : ApiController
{
private IIndexerManagerService indexerService;
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
private IWebClient webClient;
public static int[] MOVIE_CATS
{
get
{
var torznabQuery = new TorznabQuery()
{
Categories = new int[1] { TorznabCatType.Movies.ID },
};
torznabQuery.ExpandCatsToSubCats();
return torznabQuery.Categories;
}
}
public PotatoController(IIndexerManagerService i, Logger l, IServerService s, ICacheService c, IWebClient w)
{
indexerService = i;
logger = l;
serverService = s;
cacheService = c;
webClient = w;
}
[HttpGet]
public async Task<HttpResponseMessage> Call(string indexerID, [FromUri]TorrentPotatoRequest request)
{
var indexer = indexerService.GetIndexer(indexerID);
var allowBadApiDueToDebug = false;
#if DEBUG
allowBadApiDueToDebug = Debugger.IsAttached;
#endif
if (!allowBadApiDueToDebug && !string.Equals(request.passkey, serverService.Config.APIKey, StringComparison.InvariantCultureIgnoreCase))
{
logger.Warn(string.Format("A request from {0} was made with an incorrect API key.", Request.GetOwinContext().Request.RemoteIpAddress));
return Request.CreateResponse(HttpStatusCode.Forbidden, "Incorrect API key");
}
if (!indexer.IsConfigured)
{
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
return Request.CreateResponse(HttpStatusCode.Forbidden, "This indexer is not configured.");
}
if (!indexer.TorznabCaps.Categories.Select(c => c.ID).Any(i => MOVIE_CATS.Contains(i)))
{
logger.Warn(string.Format("Rejected a request to {0} which does not support searching for movies.", indexer.DisplayName));
return Request.CreateResponse(HttpStatusCode.Forbidden, "This indexer does not support movies.");
}
var year = 0;
var omdbApiKey = serverService.Config.OmdbApiKey;
if (!request.imdbid.IsNullOrEmptyOrWhitespace() && !omdbApiKey.IsNullOrEmptyOrWhitespace())
{
// We are searching by IMDB id so look up the name
var resolver = new OmdbResolver(webClient, omdbApiKey.ToNonNull());
var movie = await resolver.MovieForId(request.imdbid.ToNonNull());
request.search = movie.Title;
year = ParseUtil.CoerceInt(movie.Year);
}
var torznabQuery = new TorznabQuery()
{
ApiKey = request.passkey,
Categories = MOVIE_CATS,
SearchTerm = request.search,
ImdbID = request.imdbid,
QueryType = "TorrentPotato"
};
IEnumerable<ReleaseInfo> releases = new List<ReleaseInfo>();
if (indexer.CanHandleQuery(torznabQuery))
releases = await indexer.ResultsForQuery(torznabQuery);
// Cache non query results
if (string.IsNullOrEmpty(torznabQuery.SanitizedSearchTerm))
{
cacheService.CacheRssResults(indexer, releases);
}
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
var potatoResponse = new TorrentPotatoResponse();
if (!torznabQuery.SanitizedSearchTerm.IsNullOrEmptyOrWhitespace())
releases = TorznabUtil.FilterResultsToTitle(releases, torznabQuery.SanitizedSearchTerm, year);
if (!torznabQuery.ImdbID.IsNullOrEmptyOrWhitespace())
releases = TorznabUtil.FilterResultsToImdb(releases, request.imdbid);
foreach (var r in releases)
{
var release = Mapper.Map<ReleaseInfo>(r);
release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, indexerID, "dl", release.Title + ".torrent");
// Only accept torrent links, magnet is not supported
// This seems to be no longer the case, allowing magnet URIs for now
if (release.Link != null || release.MagnetUri != null)
{
potatoResponse.results.Add(new TorrentPotatoResponseItem()
{
release_name = release.Title + "[" + indexer.DisplayName + "]", // Suffix the indexer so we can see which tracker we are using in CPS as it just says torrentpotato >.>
torrent_id = release.Guid.ToString(),
details_url = release.Comments.ToString(),
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
imdb_id = release.Imdb.HasValue ? "tt" + release.Imdb : null,
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
type = "movie",
size = (long)release.Size / (1024 * 1024), // This is in MB
leechers = (int)release.Peers - (int)release.Seeders,
seeders = (int)release.Seeders,
publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s")
});
}
}
// Log info
if (string.IsNullOrWhiteSpace(torznabQuery.SanitizedSearchTerm))
{
logger.Info(string.Format("Found {0} torrentpotato releases from {1}", releases.Count(), indexer.DisplayName));
}
else
{
logger.Info(string.Format("Found {0} torrentpotato releases from {1} for: {2}", releases.Count(), indexer.DisplayName, torznabQuery.GetQueryString()));
}
// Force the return as Json
return new HttpResponseMessage()
{
Content = new JsonContent(potatoResponse)
};
}
}
}

View File

@ -0,0 +1,379 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Xml.Linq;
using Jackett.Indexers;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using Jackett.Utils.Clients;
using Newtonsoft.Json;
using NLog;
namespace Jackett.Controllers.V20
{
public class RequiresApiKeyAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var validApiKey = Engine.Server.Config.APIKey;
var queryParams = actionContext.Request.GetQueryNameValuePairs().ToDictionary();
var queryApiKey = queryParams.ContainsKey("apikey") ? queryParams["apikey"] : null;
queryApiKey = queryParams.ContainsKey("passkey") ? queryParams["passkey"] : queryApiKey;
#if DEBUG
if (Debugger.IsAttached)
return;
#endif
if (queryApiKey != validApiKey)
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
public class RequiresConfiguredIndexerAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller;
if (!(controller is IIndexerController))
return;
var indexerController = controller as IIndexerController;
var parameters = actionContext.RequestContext.RouteData.Values;
if (!parameters.ContainsKey("indexerId"))
{
indexerController.CurrentIndexer = null;
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
return;
}
var indexerId = parameters["indexerId"] as string;
if (indexerId.IsNullOrEmptyOrWhitespace())
{
indexerController.CurrentIndexer = null;
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
return;
}
var indexerService = indexerController.IndexerService;
var indexer = indexerService.GetIndexer(indexerId);
if (indexer == null)
{
indexerController.CurrentIndexer = null;
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid parameter");
return;
}
if (!indexer.IsConfigured)
{
indexerController.CurrentIndexer = null;
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Indexer is not configured");
return;
}
indexerController.CurrentIndexer = indexer;
}
}
public class RequiresValidQueryAttribute : RequiresConfiguredIndexerAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
if (actionContext.Response != null)
return;
var controller = actionContext.ControllerContext.Controller;
if (!(controller is IResultController))
return;
var resultController = controller as IResultController;
var query = actionContext.ActionArguments.First().Value;
var queryType = query.GetType();
var converter = queryType.GetMethod("ToTorznabQuery", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
if (converter == null)
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "");
var converted = converter.Invoke(null, new object[] { query });
var torznabQuery = converted as TorznabQuery;
resultController.CurrentQuery = torznabQuery;
if (!resultController.CurrentIndexer.CanHandleQuery(resultController.CurrentQuery))
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, $"{resultController.CurrentIndexer.ID} does not support the requested query.");
}
}
public class JsonResponseAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
var content = actionExecutedContext.Response.Content as ObjectContent;
actionExecutedContext.Response.Content = new JsonContent(content.Value);
}
}
public interface IResultController : IIndexerController
{
TorznabQuery CurrentQuery { get; set; }
}
[JackettAuthorized]
[JackettAPINoCache]
[RoutePrefix("api/v2.0/indexers")]
[RequiresApiKey]
[RequiresValidQuery]
public class ResultsController : ApiController, IResultController
{
public IIndexerManagerService IndexerService { get; private set; }
public IIndexer CurrentIndexer { get; set; }
public TorznabQuery CurrentQuery { get; set; }
public ResultsController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
{
IndexerService = indexerManagerService;
serverService = ss;
cacheService = c;
this.logger = logger;
}
[HttpGet]
public async Task<Models.DTO.ManualSearchResult> Results([FromUri]Models.DTO.ApiSearch request)
{
var trackers = IndexerService.GetAllIndexers().Where(t => t.IsConfigured);
if (CurrentIndexer.ID != "all")
trackers = trackers.Where(t => t.ID == CurrentIndexer.ID).ToList();
trackers = trackers.Where(t => t.IsConfigured && t.CanHandleQuery(CurrentQuery));
var tasks = trackers.ToList().Select(t => t.ResultsForQuery(CurrentQuery)).ToList();
var aggregateTask = Task.WhenAll(tasks);
await aggregateTask;
var results = tasks.Where(t => t.Status == TaskStatus.RanToCompletion).Where(t => t.Result.Count() > 0).SelectMany(t =>
{
var searchResults = t.Result;
var indexer = searchResults.First().Origin;
cacheService.CacheRssResults(indexer, searchResults);
return searchResults.Select(result =>
{
var item = AutoMapper.Mapper.Map<TrackerCacheResult>(result);
item.Tracker = indexer.DisplayName;
item.TrackerId = indexer.ID;
item.Peers = item.Peers - item.Seeders; // Use peers as leechers
return item;
});
}).OrderByDescending(d => d.PublishDate).ToList();
ConfigureCacheResults(results);
var manualResult = new Models.DTO.ManualSearchResult()
{
Results = results,
Indexers = trackers.Select(t => t.DisplayName).ToList()
};
if (manualResult.Indexers.Count() == 0)
manualResult.Indexers = new List<string>() { "None" };
logger.Info(string.Format("Manual search for \"{0}\" on {1} with {2} results.", CurrentQuery.SanitizedSearchTerm, string.Join(", ", manualResult.Indexers), manualResult.Results.Count()));
return manualResult;
}
[HttpGet]
public async Task<IHttpActionResult> Torznab([FromUri]Models.DTO.TorznabRequest request)
{
if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
{
return ResponseMessage(new HttpResponseMessage()
{
Content = new StringContent(CurrentIndexer.TorznabCaps.ToXml(), Encoding.UTF8, "application/xml")
});
}
if (CurrentQuery.ImdbID != null)
{
if (CurrentQuery.QueryType != "movie")
{
logger.Warn($"A non movie request with an imdbid was made from {Request.GetOwinContext().Request.RemoteIpAddress}.");
return GetErrorXML(201, "Incorrect parameter: only movie-search supports the imdbid parameter");
}
if (!string.IsNullOrEmpty(CurrentQuery.SearchTerm))
{
logger.Warn($"A movie-search request from {Request.GetOwinContext().Request.RemoteIpAddress} was made contining q and imdbid.");
return GetErrorXML(201, "Incorrect parameter: please specify either imdbid or q");
}
CurrentQuery.ImdbID = ParseUtil.GetFullImdbID(CurrentQuery.ImdbID); // normalize ImdbID
if (CurrentQuery.ImdbID == null)
{
logger.Warn($"A movie-search request from {Request.GetOwinContext().Request.RemoteIpAddress} was made with an invalid imdbid.");
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
}
if (!CurrentIndexer.TorznabCaps.SupportsImdbSearch)
{
logger.Warn($"A movie-search request with imdbid from {Request.GetOwinContext().Request.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} doesn't support it.");
return GetErrorXML(203, "Function Not Available: imdbid is not supported by this indexer");
}
}
var releases = await CurrentIndexer.ResultsForQuery(CurrentQuery);
// Some trackers do not support multiple category filtering so filter the releases that match manually.
int? newItemCount = null;
// Cache non query results
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
{
newItemCount = cacheService.GetNewItemCount(CurrentIndexer, releases);
cacheService.CacheRssResults(CurrentIndexer, releases);
}
// Log info
var logBuilder = new StringBuilder();
if (newItemCount != null)
{
logBuilder.AppendFormat("Found {0} ({1} new) releases from {2}", releases.Count(), newItemCount, CurrentIndexer.DisplayName);
}
else
{
logBuilder.AppendFormat("Found {0} releases from {1}", releases.Count(), CurrentIndexer.DisplayName);
}
if (!string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
{
logBuilder.AppendFormat(" for: {0}", CurrentQuery.GetQueryString());
}
logger.Info(logBuilder.ToString());
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
var resultPage = new ResultPage(new ChannelInfo
{
Title = CurrentIndexer.DisplayName,
Description = CurrentIndexer.DisplayDescription,
Link = new Uri(CurrentIndexer.SiteLink),
ImageUrl = new Uri(serverUrl + "logos/" + CurrentIndexer.ID + ".png"),
ImageTitle = CurrentIndexer.DisplayName,
ImageLink = new Uri(CurrentIndexer.SiteLink),
ImageDescription = CurrentIndexer.DisplayName
});
var proxiedReleases = releases.Select(r => AutoMapper.Mapper.Map<ReleaseInfo>(r)).Select(r =>
{
r.Link = serverService.ConvertToProxyLink(r.Link, serverUrl, r.Origin.ID, "dl", r.Title + ".torrent");
return r;
});
resultPage.Releases = proxiedReleases.ToList();
var xml = resultPage.ToXml(new Uri(serverUrl));
// Force the return as XML
return ResponseMessage(new HttpResponseMessage()
{
Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml")
});
}
public IHttpActionResult GetErrorXML(int code, string description)
{
var xdoc = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
new XElement("error",
new XAttribute("code", code.ToString()),
new XAttribute("description", description)
)
);
var xml = xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString();
return ResponseMessage(new HttpResponseMessage()
{
Content = new StringContent(xml, Encoding.UTF8, "application/xml")
});
}
[HttpGet]
[JsonResponse]
public async Task<Models.DTO.TorrentPotatoResponse> Potato([FromUri]Models.DTO.TorrentPotatoRequest request)
{
var releases = await CurrentIndexer.ResultsForQuery(CurrentQuery);
// Cache non query results
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
cacheService.CacheRssResults(CurrentIndexer, releases);
// Log info
if (string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
logger.Info($"Found {releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName}");
else
logger.Info($"Found {releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName} for: {CurrentQuery.GetQueryString()}");
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
var potatoReleases = releases.Where(r => r.Link != null || r.MagnetUri != null).Select(r =>
{
var release = AutoMapper.Mapper.Map<ReleaseInfo>(r);
release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, CurrentIndexer.ID, "dl", release.Title + ".torrent");
var item = new Models.DTO.TorrentPotatoResponseItem()
{
release_name = release.Title + "[" + CurrentIndexer.DisplayName + "]", // Suffix the indexer so we can see which tracker we are using in CPS as it just says torrentpotato >.>
torrent_id = release.Guid.ToString(),
details_url = release.Comments.ToString(),
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
imdb_id = release.Imdb.HasValue ? "tt" + release.Imdb : null,
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
type = "movie",
size = (long)release.Size / (1024 * 1024), // This is in MB
leechers = (int)release.Peers - (int)release.Seeders,
seeders = (int)release.Seeders,
publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s")
};
return item;
});
var potatoResponse = new Models.DTO.TorrentPotatoResponse()
{
results = potatoReleases.ToList()
};
return potatoResponse;
}
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
{
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
foreach (var result in results)
{
var link = result.Link;
var file = StringUtil.MakeValidFileName(result.Title, '_', false) + ".torrent";
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.Server.Config.BlackholeDir))
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
}
}
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
}
}

View File

@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web.Http;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using NLog;
namespace Jackett.Controllers.V20
{
[RoutePrefix("api/v2.0/server")]
[JackettAuthorized]
[JackettAPINoCache]
public class ServerConfigurationController : ApiController
{
public ServerConfigurationController(IConfigurationService c, IServerService s, IProcessService p, IIndexerManagerService i, ISecuityService ss, IUpdateService u, ILogCacheService lc, Logger l)
{
config = c;
serverService = s;
processService = p;
indexerService = i;
securityService = ss;
updater = u;
logCache = lc;
logger = l;
}
[HttpPost]
public void AdminPassword([FromBody]string password)
{
var oldPassword = serverService.Config.AdminPassword;
if (string.IsNullOrEmpty(password))
password = string.Empty;
if (oldPassword != password)
{
serverService.Config.AdminPassword = securityService.HashPassword(password);
serverService.SaveConfig();
}
}
[HttpPost]
public void Update()
{
updater.CheckForUpdatesNow();
}
[HttpGet]
public Models.DTO.ServerConfig Config()
{
var dto = new Models.DTO.ServerConfig(serverService.notices, serverService.Config, config.GetVersion());
return dto;
}
[ActionName("Config")]
[HttpPost]
public void UpdateConfig([FromBody]Models.DTO.ServerConfig config)
{
var originalPort = Engine.Server.Config.Port;
var originalAllowExternal = Engine.Server.Config.AllowExternal;
int port = config.port;
bool external = config.external;
string saveDir = config.blackholedir;
bool updateDisabled = config.updatedisabled;
bool preRelease = config.prerelease;
bool logging = config.logging;
string basePathOverride = config.basepathoverride;
string omdbApiKey = config.omdbkey;
Engine.Server.Config.UpdateDisabled = updateDisabled;
Engine.Server.Config.UpdatePrerelease = preRelease;
Engine.Server.Config.BasePathOverride = basePathOverride;
Startup.BasePath = Engine.Server.BasePath();
Engine.Server.SaveConfig();
Engine.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
Startup.TracingEnabled = logging;
if (omdbApiKey != Engine.Server.Config.OmdbApiKey)
{
Engine.Server.Config.OmdbApiKey = omdbApiKey;
Engine.Server.SaveConfig();
// HACK
indexerService.InitAggregateIndexer();
}
if (port != Engine.Server.Config.Port || external != Engine.Server.Config.AllowExternal)
{
if (ServerUtil.RestrictedPorts.Contains(port))
throw new Exception("The port you have selected is restricted, try a different one.");
if (port < 1 || port > 65535)
throw new Exception("The port you have selected is invalid, it must be below 65535.");
// Save port to the config so it can be picked up by the if needed when running as admin below.
Engine.Server.Config.AllowExternal = external;
Engine.Server.Config.Port = port;
Engine.Server.SaveConfig();
// On Windows change the url reservations
if (System.Environment.OSVersion.Platform != PlatformID.Unix)
{
if (!ServerUtil.IsUserAdministrator())
{
try
{
processService.StartProcessAndLog(System.Windows.Forms.Application.ExecutablePath, "--ReserveUrls", true);
}
catch
{
Engine.Server.Config.Port = originalPort;
Engine.Server.Config.AllowExternal = originalAllowExternal;
Engine.Server.SaveConfig();
throw new Exception("Failed to acquire admin permissions to reserve the new port.");
}
}
else
{
serverService.ReserveUrls(true);
}
}
(new Thread(() =>
{
Thread.Sleep(500);
serverService.Stop();
Engine.BuildContainer();
Engine.Server.Initalize();
Engine.Server.Start();
})).Start();
}
if (saveDir != Engine.Server.Config.BlackholeDir)
{
if (!string.IsNullOrEmpty(saveDir))
{
if (!Directory.Exists(saveDir))
{
throw new Exception("Blackhole directory does not exist");
}
}
Engine.Server.Config.BlackholeDir = saveDir;
Engine.Server.SaveConfig();
}
}
[HttpGet]
public List<CachedLog> Logs()
{
return logCache.Logs;
}
private IConfigurationService config;
private IServerService serverService;
private IProcessService processService;
private IIndexerManagerService indexerService;
private ISecuityService securityService;
private IUpdateService updater;
private ILogCacheService logCache;
private Logger logger;
}
}

View File

@ -1,182 +0,0 @@
using AutoMapper;
using Jackett.Models;
using Jackett.Services;
using Jackett.Utils;
using NLog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Xml.Linq;
using Jackett.Indexers;
namespace Jackett.Controllers
{
[AllowAnonymous]
[JackettAPINoCache]
public class TorznabController : ApiController
{
private IIndexerManagerService indexerService;
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
public TorznabController(IIndexerManagerService i, Logger l, IServerService s, ICacheService c)
{
indexerService = i;
logger = l;
serverService = s;
cacheService = c;
}
public HttpResponseMessage GetErrorXML(int code, string description)
{
var xdoc = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
new XElement("error",
new XAttribute("code", code.ToString()),
new XAttribute("description", description)
)
);
var xml = xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString();
return new HttpResponseMessage()
{
Content = new StringContent(xml, Encoding.UTF8, "application/xml")
};
}
[HttpGet]
public async Task<HttpResponseMessage> Call(string indexerID)
{
var indexer = indexerService.GetIndexer(indexerID);
var torznabQuery = TorznabQuery.FromHttpQuery(HttpUtility.ParseQueryString(Request.RequestUri.Query));
if (string.Equals(torznabQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
{
return new HttpResponseMessage()
{
Content = new StringContent(indexer.TorznabCaps.ToXml(), Encoding.UTF8, "application/xml")
};
}
torznabQuery.ExpandCatsToSubCats();
var allowBadApiDueToDebug = false;
#if DEBUG
allowBadApiDueToDebug = Debugger.IsAttached;
#endif
if (!allowBadApiDueToDebug && !string.Equals(torznabQuery.ApiKey, serverService.Config.APIKey, StringComparison.InvariantCultureIgnoreCase))
{
logger.Warn(string.Format("A request from {0} was made with an incorrect API key.", Request.GetOwinContext().Request.RemoteIpAddress));
return Request.CreateResponse(HttpStatusCode.Forbidden, "Incorrect API key");
}
if (!indexer.IsConfigured)
{
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
return Request.CreateResponse(HttpStatusCode.Forbidden, "This indexer is not configured.");
}
if (torznabQuery.ImdbID != null)
{
if (torznabQuery.QueryType != "movie")
{
logger.Warn(string.Format("A non movie request with an imdbid was made from {0}.", Request.GetOwinContext().Request.RemoteIpAddress));
return GetErrorXML(201, "Incorrect parameter: only movie-search supports the imdbid parameter");
}
if (!string.IsNullOrEmpty(torznabQuery.SearchTerm))
{
logger.Warn(string.Format("A movie-search request from {0} was made contining q and imdbid.", Request.GetOwinContext().Request.RemoteIpAddress));
return GetErrorXML(201, "Incorrect parameter: please specify either imdbid or q");
}
torznabQuery.ImdbID = ParseUtil.GetFullImdbID(torznabQuery.ImdbID); // normalize ImdbID
if (torznabQuery.ImdbID == null)
{
logger.Warn(string.Format("A movie-search request from {0} was made with an invalid imdbid.", Request.GetOwinContext().Request.RemoteIpAddress));
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
}
if (!indexer.TorznabCaps.SupportsImdbSearch)
{
logger.Warn(string.Format("A movie-search request with imdbid from {0} was made but the indexer {1} doesn't support it.", Request.GetOwinContext().Request.RemoteIpAddress, indexer.DisplayName));
return GetErrorXML(203, "Function Not Available: imdbid is not supported by this indexer");
}
}
var releases = await indexer.ResultsForQuery(torznabQuery);
// Some trackers do not keep their clocks up to date and can be ~20 minutes out!
foreach (var release in releases.Where(r => r.PublishDate > DateTime.Now))
{
release.PublishDate = DateTime.Now;
}
// Some trackers do not support multiple category filtering so filter the releases that match manually.
int? newItemCount = null;
// Cache non query results
if (string.IsNullOrEmpty(torznabQuery.SanitizedSearchTerm))
{
newItemCount = cacheService.GetNewItemCount(indexer, releases);
cacheService.CacheRssResults(indexer, releases);
}
// Log info
var logBuilder = new StringBuilder();
if (newItemCount != null)
{
logBuilder.AppendFormat(string.Format("Found {0} ({1} new) releases from {2}", releases.Count(), newItemCount, indexer.DisplayName));
}
else
{
logBuilder.AppendFormat(string.Format("Found {0} releases from {1}", releases.Count(), indexer.DisplayName));
}
if (!string.IsNullOrWhiteSpace(torznabQuery.SanitizedSearchTerm))
{
logBuilder.AppendFormat(" for: {0}", torznabQuery.GetQueryString());
}
logger.Info(logBuilder.ToString());
var serverUrl = string.Format("{0}://{1}:{2}{3}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port, serverService.BasePath());
var resultPage = new ResultPage(new ChannelInfo
{
Title = indexer.DisplayName,
Description = indexer.DisplayDescription,
Link = new Uri(indexer.SiteLink),
ImageUrl = new Uri(serverUrl + "logos/" + indexer.ID + ".png"),
ImageTitle = indexer.DisplayName,
ImageLink = new Uri(indexer.SiteLink),
ImageDescription = indexer.DisplayName
});
foreach (var result in releases)
{
var clone = Mapper.Map<ReleaseInfo>(result);
clone.Link = serverService.ConvertToProxyLink(clone.Link, serverUrl, result.Origin.ID, "dl", result.Title + ".torrent");
resultPage.Releases.Add(clone);
}
var xml = resultPage.ToXml(new Uri(serverUrl));
// Force the return as XML
return new HttpResponseMessage()
{
Content = new StringContent(xml, Encoding.UTF8, "application/rss+xml")
};
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Jackett.Services;
using Jackett.Utils;
using NLog;
namespace Jackett.Controllers
{
[RoutePrefix("UI")]
[JackettAuthorized]
[JackettAPINoCache]
public class WebUIController : ApiController
{
public WebUIController(IConfigurationService config, IServerService ss, ISecuityService s, Logger l)
{
this.config = config;
serverService = ss;
securityService = s;
logger = l;
}
private HttpResponseMessage GetFile(string path)
{
var result = new HttpResponseMessage(HttpStatusCode.OK);
var mappedPath = Path.Combine(config.GetContentFolder(), path);
var stream = new FileStream(mappedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(mappedPath));
return result;
}
[HttpGet]
[AllowAnonymous]
public IHttpActionResult Logout()
{
var ctx = Request.GetOwinContext();
var authManager = ctx.Authentication;
authManager.SignOut("ApplicationCookie");
return Redirect("UI/Dashboard");
}
[HttpGet]
[HttpPost]
[AllowAnonymous]
public async Task<HttpResponseMessage> Dashboard()
{
if (Request.RequestUri.Query != null && Request.RequestUri.Query.Contains("logout"))
{
var file = GetFile("login.html");
securityService.Logout(file);
return file;
}
if (securityService.CheckAuthorised(Request))
{
return GetFile("index.html");
}
else
{
var formData = await Request.Content.ReadAsFormDataAsync();
if (formData != null && securityService.HashPassword(formData["password"]) == serverService.Config.AdminPassword)
{
var file = GetFile("index.html");
securityService.Login(file);
return file;
}
else
{
return GetFile("login.html");
}
}
}
private IConfigurationService config;
private IServerService serverService;
private ISecuityService securityService;
private Logger logger;
}
}

View File

@ -10,6 +10,8 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http.Dependencies;
using Autofac.Integration.WebApi;
namespace Jackett
{
@ -37,9 +39,9 @@ namespace Jackett
}
public static IContainer GetContainer()
public static IDependencyResolver DependencyResolver()
{
return container;
return new AutofacWebApiDependencyResolver(container);
}
public static bool IsWindows

View File

@ -216,11 +216,17 @@ namespace Jackett.Indexers
public virtual async Task<IEnumerable<ReleaseInfo>> ResultsForQuery(TorznabQuery query)
{
if (!CanHandleQuery(query))
return new ReleaseInfo[0];
var results = await PerformQuery(query);
results = FilterResults(query, results);
results = results.Select(r =>
{
r.Origin = this;
// Some trackers do not keep their clocks up to date and can be ~20 minutes out!
if (r.PublishDate > DateTime.Now)
r.PublishDate = DateTime.Now;
return r;
});

View File

@ -27,9 +27,9 @@ namespace Jackett.Indexers.Meta
return Task.FromResult(IndexerConfigurationStatus.Completed);
}
public override async Task<IEnumerable<ReleaseInfo>> ResultsForQuery(TorznabQuery query)
protected override IEnumerable<ReleaseInfo> FilterResults(TorznabQuery query, IEnumerable<ReleaseInfo> results)
{
return await PerformQuery(query);
return results;
}
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)

View File

@ -14,6 +14,13 @@ namespace Jackett.Indexers.Meta
{
class AggregateIndexer : BaseMetaIndexer
{
public override string ID
{
get
{
return "all";
}
}
public AggregateIndexer(IFallbackStrategyProvider fallbackStrategyProvider, IResultFilterProvider resultFilterProvider, IIndexerConfigurationService configService, IWebClient wc, Logger l, IProtectionService ps)
: base("AggregateSearch", "This feed includes all configured trackers", fallbackStrategyProvider, resultFilterProvider, configService, wc, l, new ConfigurationData(), ps, x => true)
{

View File

@ -174,8 +174,6 @@
<Compile Include="AuthenticationException.cs" />
<Compile Include="CacheControlAttribute.cs" />
<Compile Include="Controllers\BlackholeController.cs" />
<Compile Include="Controllers\PotatoController.cs" />
<Compile Include="Controllers\TorznabController.cs" />
<Compile Include="Controllers\DownloadController.cs" />
<Compile Include="Engine.cs" />
<Compile Include="Indexers\ArcheTorrent.cs" />
@ -265,7 +263,6 @@
<Compile Include="Models\IndexerConfig\ConfigurationDataAPIKey.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataLoginLink.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginWithRSSAndDisplay.cs" />
<Compile Include="Models\ManualSearchResult.cs" />
<Compile Include="Indexers\TVChaosUK.cs" />
<Compile Include="Indexers\NCore.cs" />
<Compile Include="Indexers\TorrentBytes.cs" />
@ -283,7 +280,6 @@
<Compile Include="Models\CachedLog.cs" />
<Compile Include="Models\CachedResult.cs" />
<Compile Include="Models\CategoryMapping.cs" />
<Compile Include="Models\AdminSearch.cs" />
<Compile Include="Models\IndexerConfigurationStatus.cs" />
<Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataFileList.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginWithRSS.cs" />
@ -296,9 +292,6 @@
<Compile Include="Models\IndexerConfig\ConfigurationDataUrl.cs" />
<Compile Include="Models\IndexerConfig\ISerializableConfig.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataPinNumber.cs" />
<Compile Include="Models\TorrentPotatoRequest.cs" />
<Compile Include="Models\TorrentPotatoResponse.cs" />
<Compile Include="Models\TorrentPotatoResponseItem.cs" />
<Compile Include="Models\TorznabCapabilities.cs" />
<Compile Include="Models\Config\ServerConfig.cs" />
<Compile Include="Models\TorznabCategory.cs" />
@ -332,7 +325,6 @@
<Compile Include="Models\IndexerConfig\ConfigurationDataBasicLogin.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataCookie.cs" />
<Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataRuTor.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="CookieContainerExtensions.cs" />
<Compile Include="Utils\Clients\WebRequest.cs" />
<Compile Include="Utils\DataUrl.cs" />
@ -365,12 +357,10 @@
<Compile Include="Utils\TorznabCapsUtil.cs" />
<Compile Include="Utils\Clients\UnixSafeCurlWebClient.cs" />
<Compile Include="Utils\TvCategoryParser.cs" />
<Compile Include="Utils\WebApiRootRedirectMiddleware.cs" />
<Compile Include="Utils\RedirectMiddleware.cs" />
<Compile Include="Utils\WebAPIRequestLogger.cs" />
<Compile Include="Utils\WebAPIToNLogTracer.cs" />
<Compile Include="Utils\Clients\HttpWebClient.cs" />
<Compile Include="WebAPIExceptionHandler.cs" />
<Compile Include="WebAPIExceptionLogger.cs" />
<Compile Include="Indexers\BakaBT.cs" />
<Compile Include="Indexers\Meta\MetaIndexers.cs" />
<Compile Include="Indexers\Meta\BaseMetaIndexer.cs" />
@ -384,6 +374,19 @@
<Compile Include="Indexers\Feeds\BaseNewznabIndexer.cs" />
<Compile Include="Indexers\Feeds\AnimeTosho.cs" />
<Compile Include="Indexers\Feeds\BaseFeedIndexer.cs" />
<Compile Include="Controllers\IndexerApiController.cs" />
<Compile Include="Controllers\UIController.cs" />
<Compile Include="Controllers\ServerConfigurationController.cs" />
<Compile Include="Controllers\ResultsController.cs" />
<Compile Include="Models\DTO\Config.cs" />
<Compile Include="Models\DTO\Indexer.cs" />
<Compile Include="Models\DTO\ServerConfig.cs" />
<Compile Include="Models\DTO\ApiSearch.cs" />
<Compile Include="Models\DTO\ManualSearchResult.cs" />
<Compile Include="Models\DTO\TorrentPotatoRequest.cs" />
<Compile Include="Models\DTO\TorrentPotatoResponse.cs" />
<Compile Include="Models\DTO\TorrentPotatoResponseItem.cs" />
<Compile Include="Models\DTO\TorznabRequest.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config">
@ -428,6 +431,9 @@
<SubType>Designer</SubType>
</None>
<None Include="Resources\test.xml" />
<Content Include="Content\libs\api.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Content\libs\handlebarsextend.js">
@ -531,9 +537,6 @@
<Content Include="Content\index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Content\setup_indexer.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Models\TorznabCatType.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>TorznabCatType.generated.cs</LastGenOutput>
@ -570,6 +573,7 @@
<ItemGroup />
<ItemGroup>
<Folder Include="Indexers\Feeds\" />
<Folder Include="Models\DTO\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -1,12 +1,9 @@
using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac.Integration.WebApi;
using Jackett.Indexers;
using Jackett.Utils;
using Jackett.Utils.Clients;
using AutoMapper;
using Jackett.Models;

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Models
{
public class AdminSearch
{
public string Query { get; set; }
public string Tracker { get; set; }
public int Category { get; set; }
}
}

View File

@ -0,0 +1,59 @@
using System.Text.RegularExpressions;
using Jackett.Utils;
namespace Jackett.Models.DTO
{
public class ApiSearch
{
public string Query { get; set; }
public int Category { get; set; }
public static TorznabQuery ToTorznabQuery(ApiSearch request)
{
var stringQuery = new TorznabQuery();
var queryStr = request.Query;
if (queryStr != null)
{
var seasonMatch = Regex.Match(queryStr, @"S(\d{2,4})");
if (seasonMatch.Success)
{
stringQuery.Season = int.Parse(seasonMatch.Groups[1].Value);
queryStr = queryStr.Remove(seasonMatch.Index, seasonMatch.Length);
}
var episodeMatch = Regex.Match(queryStr, @"E(\d{2,4}[A-Za-z]?)");
if (episodeMatch.Success)
{
stringQuery.Episode = episodeMatch.Groups[1].Value;
queryStr = queryStr.Remove(episodeMatch.Index, episodeMatch.Length);
}
queryStr = queryStr.Trim();
}
stringQuery.SearchTerm = queryStr;
stringQuery.Categories = request.Category == 0 ? new int[0] : new int[1] { request.Category };
stringQuery.ExpandCatsToSubCats();
// try to build an IMDB Query
var imdbID = ParseUtil.GetFullImdbID(stringQuery.SanitizedSearchTerm);
TorznabQuery imdbQuery = null;
if (imdbID != null)
{
imdbQuery = new TorznabQuery()
{
ImdbID = imdbID,
Categories = stringQuery.Categories,
Season = stringQuery.Season,
Episode = stringQuery.Episode,
};
imdbQuery.ExpandCatsToSubCats();
return imdbQuery;
}
return stringQuery;
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Jackett.Models.DTO
{
public class ConfigItem
{
public string id { get; set; }
public string value { get; set; }
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using Jackett.Controllers.V20;
using Jackett.Indexers;
namespace Jackett.Models.DTO
{
public class Capability
{
public string ID { get; set; }
public string Name { get; set; }
}
public class Indexer
{
public string id { get; private set; }
public string name { get; private set; }
public string description { get; private set; }
public string type { get; private set; }
public bool configured { get; private set; }
public string site_link { get; private set; }
public IEnumerable<string> alternativesitelinks { get; private set; }
public string language { get; private set; }
public string last_error { get; private set; }
public bool potatoenabled { get; private set; }
public IEnumerable<Capability> caps { get; private set; }
public Indexer(IIndexer indexer)
{
id = indexer.ID;
name = indexer.DisplayName;
description = indexer.DisplayDescription;
type = indexer.Type;
configured = indexer.IsConfigured;
site_link = indexer.SiteLink;
language = indexer.Language;
last_error = indexer.LastError;
potatoenabled = indexer.TorznabCaps.Categories.Select(c => c.ID).Any(i => i == TorznabCatType.Movies.ID);
alternativesitelinks = indexer.AlternativeSiteLinks;
caps = indexer.TorznabCaps.Categories.Select(c => new Capability
{
ID = c.ID.ToString(),
Name = c.Name
});
}
}
}

View File

@ -5,11 +5,11 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett
namespace Jackett.Models.DTO
{
public class ManualSearchResult
{
public List<TrackerCacheResult> Results { get; set; }
public List<string> Indexers { get; set; }
public IEnumerable<TrackerCacheResult> Results { get; set; }
public IEnumerable<string> Indexers { get; set; }
}
}

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using Jackett.Services;
namespace Jackett.Models.DTO
{
public class ServerConfig
{
public IEnumerable<string> notices { get; set; }
public int port { get; set; }
public bool external { get; set; }
public string api_key { get; set; }
public string blackholedir { get; set; }
public bool updatedisabled { get; set; }
public bool prerelease { get; set; }
public string password { get; set; }
public bool logging { get; set; }
public string basepathoverride { get; set; }
public string omdbkey { get; set; }
public string app_version { get; set; }
public ServerConfig()
{
notices = new string[0];
}
public ServerConfig(IEnumerable<string> notices, Models.Config.ServerConfig config, string version)
{
this.notices = notices;
port = config.Port;
external = config.AllowExternal;
api_key = config.APIKey;
blackholedir = config.BlackholeDir;
updatedisabled = config.UpdateDisabled;
prerelease = config.UpdatePrerelease;
password = string.IsNullOrEmpty(config.AdminPassword) ? string.Empty : config.AdminPassword.Substring(0, 10);
logging = Startup.TracingEnabled;
basepathoverride = config.BasePathOverride;
omdbkey = config.OmdbApiKey;
app_version = version;
}
}
}

View File

@ -0,0 +1,23 @@
namespace Jackett.Models.DTO
{
public class TorrentPotatoRequest
{
public string Username { get; set; }
public string Imdbid { get; set; }
public string Search { get; set; }
public static TorznabQuery ToTorznabQuery(TorrentPotatoRequest request)
{
var torznabQuery = new TorznabQuery()
{
Categories = new int[1] { TorznabCatType.Movies.ID },
SearchTerm = request.Search,
ImdbID = request.Imdbid,
QueryType = "TorrentPotato"
};
torznabQuery.ExpandCatsToSubCats();
return torznabQuery;
}
}
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
namespace Jackett.Models.DTO
{
public class TorrentPotatoResponse
{
public IEnumerable<TorrentPotatoResponseItem> results { get; set; }
public int total_results
{
get
{
if (results == null)
return 0;
return results.Count();
}
}
}
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Models
namespace Jackett.Models.DTO
{
public class TorrentPotatoResponseItem
{

View File

@ -0,0 +1,59 @@
using System;
using System.Linq;
using Jackett.Utils;
namespace Jackett.Models.DTO
{
public class TorznabRequest
{
public string t { get; set; }
public string q { get; set; }
public string cat { get; set; }
public string imdbid { get; set; }
public string extended { get; set; }
public string limit { get; set; }
public string offset { get; set; }
public string rid { get; set; }
public string season { get; set; }
public string ep { get; set; }
public static TorznabQuery ToTorznabQuery(TorznabRequest request)
{
var query = new TorznabQuery()
{
QueryType = request.t,
SearchTerm = request.q,
ImdbID = request.imdbid,
Episode = request.ep,
};
if (!request.extended.IsNullOrEmptyOrWhitespace())
query.Extended = ParseUtil.CoerceInt(request.extended);
if (!request.limit.IsNullOrEmptyOrWhitespace())
query.Limit = ParseUtil.CoerceInt(request.limit);
if (!request.offset.IsNullOrEmptyOrWhitespace())
query.Offset = ParseUtil.CoerceInt(request.offset);
if (request.cat != null)
{
query.Categories = request.cat.Split(',').Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => int.Parse(s)).ToArray();
}
else
{
if (query.QueryType == "movie" && !string.IsNullOrWhiteSpace(request.imdbid))
query.Categories = new int[] { TorznabCatType.Movies.ID };
else
query.Categories = new int[0];
}
if (!request.rid.IsNullOrEmptyOrWhitespace())
query.RageID = int.Parse(request.rid);
if (!request.season.IsNullOrEmptyOrWhitespace())
query.Season = int.Parse(request.season);
query.ExpandCatsToSubCats();
return query;
}
}
}

View File

@ -9,7 +9,6 @@ namespace Jackett.Models
public enum IndexerConfigurationStatus
{
Completed,
RequiresTesting,
Failed
RequiresTesting
}
}

View File

@ -17,7 +17,7 @@ namespace Jackett.Models
static XNamespace torznabNs = "http://torznab.com/schemas/2015/feed";
public ChannelInfo ChannelInfo { get; private set; }
public List<ReleaseInfo> Releases { get; private set; }
public IEnumerable<ReleaseInfo> Releases { get; set; }
public ResultPage(ChannelInfo channelInfo)
{
@ -90,8 +90,8 @@ namespace Jackett.Models
getTorznabElement("infohash", r.InfoHash),
getTorznabElement("minimumratio", r.MinimumRatio),
getTorznabElement("minimumseedtime", r.MinimumSeedTime),
getTorznabElement("downloadvolumefactor", r.DownloadVolumeFactor),
getTorznabElement("uploadvolumefactor", r.UploadVolumeFactor)
getTorznabElement("downloadvolumefactor", r.DownloadVolumeFactor),
getTorznabElement("uploadvolumefactor", r.UploadVolumeFactor)
)
)
)

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Models
{
public class TorrentPotatoRequest
{
public string username { get; set; }
public string passkey { get; set; }
public string imdbid { get; set; }
public string search { get; set; }
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Models
{
public class TorrentPotatoResponse
{
public TorrentPotatoResponse()
{
results = new List<TorrentPotatoResponseItem>();
}
public List<TorrentPotatoResponseItem> results { get; set; }
public int total_results
{
get { return results.Count; }
}
}
}

View File

@ -83,12 +83,10 @@ namespace Jackett.Models
{
get
{
var term = SearchTerm;
if (SearchTerm == null)
return string.Empty;
char[] arr = SearchTerm.ToCharArray();
arr = Array.FindAll<char>(arr, c => (char.IsLetterOrDigit(c)
term = "";
var safetitle = term.Where(c => (char.IsLetterOrDigit(c)
|| char.IsWhiteSpace(c)
|| c == '-'
|| c == '.'
@ -99,8 +97,7 @@ namespace Jackett.Models
|| c == '\''
|| c == '['
|| c == ']'
));
var safetitle = new string(arr);
)).AsString();
return safetitle;
}
}
@ -111,9 +108,11 @@ namespace Jackett.Models
IsTest = false;
}
public TorznabQuery CreateFallback(string search) {
public TorznabQuery CreateFallback(string search)
{
var ret = Clone();
if (Categories == null || Categories.Length == 0) {
if (Categories == null || Categories.Length == 0)
{
ret.Categories = new int[]{ TorznabCatType.Movies.ID,
TorznabCatType.MoviesForeign.ID,
TorznabCatType.MoviesOther.ID,
@ -121,8 +120,8 @@ namespace Jackett.Models
TorznabCatType.MoviesHD.ID,
TorznabCatType.Movies3D.ID,
TorznabCatType.MoviesBluRay.ID,
TorznabCatType.MoviesDVD.ID,
TorznabCatType.MoviesWEBDL.ID,
TorznabCatType.MoviesDVD.ID,
TorznabCatType.MoviesWEBDL.ID,
};
}
ret.SearchTerm = search;
@ -130,12 +129,14 @@ namespace Jackett.Models
return ret;
}
public TorznabQuery Clone() {
public TorznabQuery Clone()
{
var ret = new TorznabQuery();
ret.QueryType = QueryType;
if (Categories != null && Categories.Length > 0) {
ret.Categories = new int [Categories.Length];
Array.Copy (Categories, ret.Categories, Categories.Length);
if (Categories != null && Categories.Length > 0)
{
ret.Categories = new int[Categories.Length];
Array.Copy(Categories, ret.Categories, Categories.Length);
}
ret.Extended = Extended;
ret.ApiKey = ApiKey;
@ -145,9 +146,10 @@ namespace Jackett.Models
ret.Episode = Episode;
ret.SearchTerm = SearchTerm;
ret.IsTest = IsTest;
if (QueryStringParts != null && QueryStringParts.Length > 0) {
ret.QueryStringParts = new string [QueryStringParts.Length];
Array.Copy (QueryStringParts, ret.QueryStringParts, QueryStringParts.Length);
if (QueryStringParts != null && QueryStringParts.Length > 0)
{
ret.QueryStringParts = new string[QueryStringParts.Length];
Array.Copy(QueryStringParts, ret.QueryStringParts, QueryStringParts.Length);
}
return ret;
@ -206,75 +208,16 @@ namespace Jackett.Models
try
{
episodeString = string.Format("S{0:00}E{1:00}", Season, ParseUtil.CoerceInt(Episode));
} catch (FormatException) // e.g. seaching for S01E01A
}
catch (FormatException) // e.g. seaching for S01E01A
{
episodeString = string.Format("S{0:00}E{1}", Season, Episode);
}
}
return episodeString;
}
public static TorznabQuery FromHttpQuery(NameValueCollection query)
{
//{t=tvsearch&cat=5030%2c5040&extended=1&apikey=test&offset=0&limit=100&rid=24493&season=5&ep=1}
var q = new TorznabQuery();
q.QueryType = query["t"];
if (query["q"] == null)
{
q.SearchTerm = string.Empty;
}
else
{
q.SearchTerm = query["q"];
}
if (query["cat"] != null)
{
q.Categories = query["cat"].Split(',').Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => int.Parse(s)).ToArray();
}else
{
if (q.QueryType == "movie" && string.IsNullOrWhiteSpace(query["imdbid"]))
q.Categories = new int[] { TorznabCatType.Movies.ID };
else
q.Categories = new int[0];
}
if (query["extended"] != null)
{
q.Extended = ParseUtil.CoerceInt(query["extended"]);
}
q.ApiKey = query["apikey"];
if (query["limit"] != null)
{
q.Limit = ParseUtil.CoerceInt(query["limit"]);
}
if (query["offset"] != null)
{
q.Offset = ParseUtil.CoerceInt(query["offset"]);
}
q.ImdbID = query["imdbid"];
int rageId;
if (int.TryParse(query["rid"], out rageId))
{
q.RageID = rageId;
}
int season;
if (int.TryParse(query["season"], out season))
{
q.Season = season;
}
q.Episode = query["ep"];
return q;
}
public void ExpandCatsToSubCats()
{
if (Categories.Count() == 0)

View File

@ -1,30 +1,20 @@
using Autofac;
using Jackett.Models.Config;
using Jackett.Services;
using Jackett.Models.Config;
using Jackett.Utils;
using Jackett.Utils.Clients;
using Microsoft.Owin.Hosting;
using Newtonsoft.Json.Linq;
using NLog;
using NLog.Config;
using NLog.Targets;
using NLog.Windows.Forms;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace Jackett.Services

View File

@ -1,27 +1,59 @@
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http;
using Autofac.Integration.WebApi;
using Microsoft.Owin;
using Jackett;
using Microsoft.Owin.StaticFiles;
using Microsoft.Owin.FileSystems;
using Autofac;
using Jackett.Services;
using System.Web.Http.Tracing;
using Jackett.Utils;
using Microsoft.AspNet.Identity;
using System.Web.Http.ExceptionHandling;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Web.Http.Filters;
using Newtonsoft.Json.Linq;
[assembly: OwinStartup(typeof(Startup))]
namespace Jackett
{
class ApiExceptionHandler : System.Web.Http.Filters.IExceptionFilter
{
public bool AllowMultiple
{
get
{
return false;
}
}
public Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
string msg = "";
var json = new JObject();
var exception = actionExecutedContext.Exception;
var message = exception.Message;
if (exception.InnerException != null)
message += ": " + exception.InnerException.Message;
msg = message;
if (exception is ExceptionWithConfigData)
json["config"] = ((ExceptionWithConfigData)exception).ConfigData.ToJson(null, false);
json["result"] = "error";
json["error"] = msg;
var response = actionExecutedContext.Request.CreateResponse();
response.Content = new JsonContent(json);
response.StatusCode = HttpStatusCode.InternalServerError;
actionExecutedContext.Response = response;
return Task.FromResult(0);
}
}
public class Startup
{
public static bool TracingEnabled
@ -101,9 +133,10 @@ namespace Jackett
}
appBuilder.Use<WebApiRootRedirectMiddleware>();
appBuilder.Use<LegacyApiRedirectMiddleware>();
// register exception handler
config.Services.Replace(typeof(IExceptionHandler), new WebAPIExceptionHandler());
config.Filters.Add(new ApiExceptionHandler());
// Setup tracing if enabled
if (TracingEnabled)
@ -113,53 +146,67 @@ namespace Jackett
}
// Add request logging if enabled
if (LogRequests)
{
config.MessageHandlers.Add(new WebAPIRequestLogger());
}
config.DependencyResolver = new AutofacWebApiDependencyResolver(Engine.GetContainer());
config.DependencyResolver = Engine.DependencyResolver();
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "Admin",
routeTemplate: "admin/{action}",
defaults: new { controller = "Admin" }
name: "IndexerResultsAPI",
routeTemplate: "api/v2.0/indexers/{indexerId}/results/{action}",
defaults: new
{
controller = "Results",
action = "Results"
}
);
config.Routes.MapHttpRoute(
name: "apiDefault",
routeTemplate: "api/{indexerID}",
defaults: new { controller = "Torznab", action = "Call" }
name: "IndexerAPI",
routeTemplate: "api/v2.0/indexers/{indexerId}/{action}",
defaults: new
{
controller = "IndexerApi",
indexerId = ""
}
);
config.Routes.MapHttpRoute(
name: "api",
routeTemplate: "api/{indexerID}/api",
defaults: new { controller = "Torznab", action = "Call" }
);
name: "ServerConfiguration",
routeTemplate: "api/v2.0/server/{action}",
defaults: new
{
controller = "ServerConfiguration"
}
);
// Legacy fallback for Torznab results
config.Routes.MapHttpRoute(
name: "LegacyTorznab",
routeTemplate: "torznab/{indexerId}",
defaults: new
{
controller = "Results",
action = "Torznab"
}
);
// Legacy fallback for Potato results
config.Routes.MapHttpRoute(
name: "LegacyPotato",
routeTemplate: "potato/{indexerId}",
defaults: new
{
controller = "Results",
action = "Potato"
}
);
config.Routes.MapHttpRoute(
name: "torznabDefault",
routeTemplate: "torznab/{indexerID}",
defaults: new { controller = "Torznab", action = "Call" }
);
config.Routes.MapHttpRoute(
name: "torznab",
routeTemplate: "torznab/{indexerID}/api",
defaults: new { controller = "Torznab", action = "Call" }
);
config.Routes.MapHttpRoute(
name: "potatoDefault",
routeTemplate: "potato/{indexerID}",
defaults: new { controller = "Potato", action = "Call" }
);
config.Routes.MapHttpRoute(
name: "potato",
routeTemplate: "potato/{indexerID}/api",
defaults: new { controller = "Potato", action = "Call" }
);
name: "WebUI",
routeTemplate: "UI/{action}",
defaults: new { controller = "WebUI" }
);
config.Routes.MapHttpRoute(
name: "download",
@ -171,7 +218,7 @@ namespace Jackett
name: "blackhole",
routeTemplate: "bh/{indexerID}/{apikey}",
defaults: new { controller = "Blackhole", action = "Blackhole" }
);
);
appBuilder.UseWebApi(config);
@ -181,9 +228,7 @@ namespace Jackett
RequestPath = new PathString(string.Empty),
FileSystem = new PhysicalFileSystem(Engine.ConfigService.GetContentFolder()),
EnableDirectoryBrowsing = false,
});
}
}
}

View File

@ -31,27 +31,37 @@ namespace Jackett.Utils
private T Value;
}
public static class IEnumerableExtension
public static class GenericConversionExtensions
{
public static IEnumerable<T> ToEnumerable<T>(this T obj)
{
return new T[] { obj };
}
public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> list)
{
return list.SelectMany(x => x);
}
}
public static class ToNonNullExtension
{
public static NonNull<T> ToNonNull<T>(this T obj) where T : class
{
return new NonNull<T>(obj);
}
}
public static class EnumerableExtension
{
public static string AsString(this IEnumerable<char> chars)
{
return String.Concat(chars);
}
public static bool IsEmpty<T>(this IEnumerable<T> collection)
{
return collection.Count() > 0;
}
public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> list)
{
return list.SelectMany(x => x);
}
}
public static class StringExtension
{
public static bool IsNullOrEmptyOrWhitespace(this string str)
@ -96,6 +106,14 @@ namespace Jackett.Utils
}
}
public static class KeyValuePairsExtension
{
public static IDictionary<Key, Value> ToDictionary<Key, Value>(this IEnumerable<KeyValuePair<Key, Value>> pairs)
{
return pairs.ToDictionary(x => x.Key, x => x.Value);
}
}
public static class ParseExtension
{
public static T? TryParse<T>(this string value) where T : struct

View File

@ -1,12 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
@ -25,7 +19,7 @@ namespace Jackett.Utils
if (!Engine.SecurityService.CheckAuthorised(actionContext.Request))
{
if(actionContext.ControllerContext.ControllerDescriptor.ControllerType.GetCustomAttributes(true).Where(a => a.GetType() == typeof(AllowAnonymousAttribute)).Any())
if (actionContext.ControllerContext.ControllerDescriptor.ControllerType.GetCustomAttributes(true).Where(a => a.GetType() == typeof(AllowAnonymousAttribute)).Any())
{
return;
}

View File

@ -0,0 +1,59 @@
using Microsoft.Owin;
using System;
using System.Threading.Tasks;
namespace Jackett.Utils
{
public class WebApiRootRedirectMiddleware : OwinMiddleware
{
public WebApiRootRedirectMiddleware(OwinMiddleware next)
: base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
if (context.Request.Path != null && context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(Startup.BasePath, StringComparison.Ordinal))
{
context.Request.Path = new PathString(context.Request.Path.Value.Substring(Startup.BasePath.Length - 1));
}
if (context.Request.Path == null || string.IsNullOrWhiteSpace(context.Request.Path.ToString()) || context.Request.Path.ToString() == "/")
{
// 301 is the status code of permanent redirect
context.Response.StatusCode = 302;
var redir = Startup.BasePath + "UI/Dashboard";
Engine.Logger.Info("redirecting to " + redir);
context.Response.Headers.Set("Location", redir);
}
else
{
await Next.Invoke(context);
}
}
}
public class LegacyApiRedirectMiddleware : OwinMiddleware
{
public LegacyApiRedirectMiddleware(OwinMiddleware next)
: base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
if (context.Request.Path == null || string.IsNullOrWhiteSpace(context.Request.Path.ToString()) || context.Request.Path.Value.StartsWith("/Admin", StringComparison.OrdinalIgnoreCase))
{
// 301 is the status code of permanent redirect
context.Response.StatusCode = 302;
var redir = context.Request.Path.Value.Replace("/Admin", "/UI");
Engine.Logger.Info("redirecting to " + redir);
context.Response.Headers.Set("Location", redir);
}
else
{
await Next.Invoke(context);
}
}
}
}

View File

@ -32,7 +32,7 @@ namespace Jackett.Utils
if (match.Success)
{
var year = ParseUtil.CoerceInt(match.Value);
if(year>1850 && year < 2100)
if (year > 1850 && year < 2100)
{
return year;
}
@ -40,55 +40,5 @@ namespace Jackett.Utils
return 0;
}
public static IEnumerable<ReleaseInfo> FilterResultsToTitle(IEnumerable<ReleaseInfo> results, string name, int imdbYear)
{
if (string.IsNullOrWhiteSpace(name))
return results;
name = CleanTitle(name);
var filteredResults = new List<ReleaseInfo>();
foreach (var result in results)
{
// don't filter results with IMDBID (will be filtered seperately)
if (result.Imdb != null)
{
filteredResults.Add(result);
continue;
}
if (result.Title == null)
continue;
// Match on title
if (CultureInfo.InvariantCulture.CompareInfo.IndexOf(CleanTitle(result.Title), name, CompareOptions.IgnoreNonSpace) >= 0)
{
// Match on year
var titleYear = GetYearFromTitle(result.Title);
if (imdbYear == 0 || titleYear == 0 || titleYear == imdbYear)
{
filteredResults.Add(result);
}
}
}
return filteredResults;
}
public static IEnumerable<ReleaseInfo> FilterResultsToImdb(IEnumerable<ReleaseInfo> results, string imdb)
{
if (string.IsNullOrWhiteSpace(imdb))
return results;
// Filter out releases that do have a valid imdb ID, that is not equal to the one we're searching for.
return
results.Where(
result => !result.Imdb.HasValue || result.Imdb.Value == 0 || ("tt" + result.Imdb.Value.ToString("D7")).Equals(imdb));
}
private static string CleanTitle(string title)
{
title = title.Replace(':', ' ').Replace('.', ' ').Replace('-', ' ').Replace('_', ' ').Replace('+', ' ').Replace("'", "").Replace("[", "").Replace("]", "").Replace("(", "").Replace(")", "");
return reduceSpacesRegex.Replace(title, " ").ToLowerInvariant();
}
}
}

View File

@ -1,42 +0,0 @@
using Jackett.Services;
using Microsoft.Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Utils
{
public class WebApiRootRedirectMiddleware : OwinMiddleware
{
public WebApiRootRedirectMiddleware(OwinMiddleware next)
: base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
var url = context.Request.Uri;
if(context.Request.Path != null && context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(Startup.BasePath))
{
context.Request.Path = new PathString(context.Request.Path.Value.Substring(Startup.BasePath.Length-1));
}
if (context.Request.Path == null || string.IsNullOrWhiteSpace(context.Request.Path.ToString()) || context.Request.Path.ToString() == "/")
{
// 301 is the status code of permanent redirect
context.Response.StatusCode = 302;
var redir = Startup.BasePath + "Admin/Dashboard";
Engine.Logger.Info("redirecting to " + redir);
context.Response.Headers.Set("Location", redir);
}
else
{
await Next.Invoke(context);
}
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.ExceptionHandling;
using Jackett.Utils;
using System.Net.Http;
using System.Net;
using System.Web.Http;
using System.Net.Sockets;
namespace Jackett
{
class WebAPIExceptionHandler : IExceptionHandler
{
public virtual Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
if (!ShouldHandle(context))
{
return Task.FromResult(0);
}
return HandleAsyncCore(context, cancellationToken);
}
public virtual Task HandleAsyncCore(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
HandleCore(context);
return Task.FromResult(0);
}
public virtual void HandleCore(ExceptionHandlerContext context)
{
// attempt to fix #930
if (context.Exception is SocketException)
{
Engine.Logger.Error("Ignoring unhandled SocketException: " + context.Exception.GetExceptionDetails());
return;
}
Engine.Logger.Error("HandleCore(): unhandled exception: " + context.Exception.GetExceptionDetails());
var resp = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(context.Exception.Message),
ReasonPhrase = "Jackett_InternalServerError"
};
throw new HttpResponseException(resp);
}
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
return context.ExceptionContext.CatchBlock.IsTopLevel;
}
}
}

View File

@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.ExceptionHandling;
using Jackett.Utils;
namespace Jackett
{
class WebAPIExceptionLogger : IExceptionLogger
{
public async Task LogAsync(ExceptionLoggerContext context, CancellationToken cancellationToken)
{
// OWIN seems to give lots of these exceptions but we are not interested in them.
if (context.Exception.Message != "Error while copying content to a stream.")
{
Engine.Logger.Error("Unhandled exception: " + context.Exception.GetExceptionDetails());
var request = await context.Request.Content.ReadAsStringAsync();
Engine.Logger.Error("Unhandled exception url: " + context.Request.RequestUri);
}
}
}
}