1
0
Fork 0
mirror of https://github.com/Jackett/Jackett synced 2025-03-04 02:38:08 +00:00

Added Sonarr API

This commit is contained in:
zone117x 2015-04-18 00:05:30 -06:00
parent c13d59f6ad
commit 0f89b630c9
9 changed files with 470 additions and 122 deletions

View file

@ -73,19 +73,40 @@ namespace Jackett
public string ID { get { return Name.Replace(" ", "").ToLower(); } }
}
public class DisplayItem : StringItem
{
public DisplayItem(string value)
{
Value = value;
ItemType = ItemType.DisplayInfo;
}
}
public class StringItem : Item
{
public string Value { get; set; }
public StringItem()
{
ItemType = ConfigurationData.ItemType.InputString;
}
}
public class BoolItem : Item
{
public bool Value { get; set; }
public BoolItem()
{
ItemType = ConfigurationData.ItemType.InputBool;
}
}
public class ImageItem : Item
{
public byte[] Value { get; set; }
public ImageItem()
{
ItemType = ConfigurationData.ItemType.DisplayImage;
}
}
public abstract Item[] GetItems();

View file

@ -13,8 +13,8 @@ namespace Jackett
public ConfigurationDataBasicLogin()
{
Username = new StringItem { Name = "Username", ItemType = ItemType.InputString };
Password = new StringItem { Name = "Password", ItemType = ItemType.InputString };
Username = new StringItem { Name = "Username" };
Password = new StringItem { Name = "Password" };
}
public override Item[] GetItems()

View file

@ -24,10 +24,10 @@ namespace Jackett
public BmtvConfig()
{
Username = new StringItem { Name = "Username", ItemType = ItemType.InputString };
Password = new StringItem { Name = "Password", ItemType = ItemType.InputString };
CaptchaImage = new ImageItem { Name = "Captcha Image", ItemType = ItemType.DisplayImage };
CaptchaText = new StringItem { Name = "Captcha Text", ItemType = ItemType.InputString };
Username = new StringItem { Name = "Username" };
Password = new StringItem { Name = "Password" };
CaptchaImage = new ImageItem { Name = "Captcha Image" };
CaptchaText = new StringItem { Name = "Captcha Text" };
}
public override Item[] GetItems()

View file

@ -22,7 +22,7 @@ namespace Jackett.Indexers
public ThePirateBayConfig()
{
Url = new StringItem { Name = "Url", ItemType = ItemType.InputString, Value = "https://thepiratebay.se/" };
Url = new StringItem { Name = "Url", Value = "https://thepiratebay.se/" };
}
public override Item[] GetItems()

View file

@ -90,6 +90,7 @@
<Compile Include="ReleaseInfo.cs" />
<Compile Include="ResultPage.cs" />
<Compile Include="Server.cs" />
<Compile Include="SonarApi.cs" />
<Compile Include="TorznabQuery.cs" />
<Compile Include="TVRage.cs" />
<Compile Include="WebApi.cs" />

View file

@ -16,13 +16,15 @@ namespace Jackett
HttpListener listener;
IndexerManager indexerManager;
WebApi webApi;
SonarrApi sonarrApi;
public Server()
{
LoadApiKey();
indexerManager = new IndexerManager();
webApi = new WebApi(indexerManager);
sonarrApi = new SonarrApi();
webApi = new WebApi(indexerManager, sonarrApi);
listener = new HttpListener();
listener.Prefixes.Add("http://*:9117/");

122
src/Jackett/SonarApi.cs Normal file
View file

@ -0,0 +1,122 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Jackett
{
public class SonarrApi
{
public class ConfigurationSonarr : ConfigurationData
{
public StringItem Host { get; private set; }
public StringItem Port { get; private set; }
public StringItem ApiKey { get; private set; }
DisplayItem ApiInfo;
public ConfigurationSonarr()
{
Host = new StringItem { Name = "Host", Value = "http://localhost" };
Port = new StringItem { Name = "Port", Value = "8989" };
ApiKey = new StringItem { Name = "API Key" };
ApiInfo = new DisplayItem("API Key can be found in Sonarr > Settings > General > Security") { Name = "API Info" };
}
public override Item[] GetItems()
{
return new Item[] { Host, Port, ApiKey, ApiInfo };
}
}
static string SonarrConfigFile = Path.Combine(Program.AppConfigDirectory, "sonarr_api.json");
string Host;
int Port;
string ApiKey;
CookieContainer cookies;
HttpClientHandler handler;
HttpClient client;
public SonarrApi()
{
LoadSettings();
cookies = new CookieContainer();
handler = new HttpClientHandler
{
CookieContainer = cookies,
AllowAutoRedirect = true,
UseCookies = true,
};
client = new HttpClient(handler);
}
void LoadSettings()
{
try
{
if (File.Exists(SonarrConfigFile))
{
var json = JObject.Parse(File.ReadAllText(SonarrConfigFile));
Host = (string)json["host"];
Port = (int)json["port"];
ApiKey = (string)json["api_key"];
}
}
catch (Exception) { }
}
void SaveSettings()
{
JObject json = new JObject();
json["host"] = Host;
json["port"] = Port;
json["api_key"] = ApiKey;
File.WriteAllText(SonarrConfigFile, json.ToString());
}
public ConfigurationSonarr GetConfiguration()
{
var config = new ConfigurationSonarr();
if (ApiKey != null)
{
config.Host.Value = Host;
config.Port.Value = Port.ToString();
config.ApiKey.Value = ApiKey;
}
return config;
}
public async Task ApplyConfiguration(JToken configJson)
{
var config = new ConfigurationSonarr();
config.LoadValuesFromJson(configJson);
await TestConnection(config.Host.Value, int.Parse(config.Port.Value), config.ApiKey.Value);
Host = "http://" + new Uri(config.Host.Value).Host;
Port = int.Parse(config.Port.Value);
ApiKey = config.ApiKey.Value;
SaveSettings();
}
public async Task TestConnection()
{
await TestConnection(Host, Port, ApiKey);
}
async Task TestConnection(string host, int port, string apiKey)
{
Uri hostUri = new Uri(host);
var queryUrl = string.Format("http://{0}:{1}/api/series?apikey={2}", hostUri.Host, port, apiKey);
var response = await client.GetStringAsync(queryUrl);
var json = JArray.Parse(response);
}
}
}

View file

@ -22,7 +22,10 @@ namespace Jackett
ConfigureIndexer,
GetIndexers,
TestIndexer,
DeleteIndexer
DeleteIndexer,
GetSonarrConfig,
ApplySonarrConfig,
TestSonarr
}
static Dictionary<string, WebApiMethod> WebApiMethods = new Dictionary<string, WebApiMethod>
{
@ -30,14 +33,19 @@ namespace Jackett
{ "configure_indexer", WebApiMethod.ConfigureIndexer },
{ "get_indexers", WebApiMethod.GetIndexers },
{ "test_indexer", WebApiMethod.TestIndexer },
{ "delete_indexer", WebApiMethod.DeleteIndexer }
{ "delete_indexer", WebApiMethod.DeleteIndexer },
{ "get_sonarr_config", WebApiMethod.GetSonarrConfig },
{ "apply_sonarr_config", WebApiMethod.ApplySonarrConfig },
{ "test_sonarr", WebApiMethod.TestSonarr }
};
IndexerManager indexerManager;
SonarrApi sonarrApi;
public WebApi(IndexerManager indexerManager)
public WebApi(IndexerManager indexerManager, SonarrApi sonarrApi)
{
this.indexerManager = indexerManager;
this.sonarrApi = sonarrApi;
}
public bool HandleRequest(HttpListenerContext context)
@ -110,6 +118,15 @@ namespace Jackett
case WebApiMethod.DeleteIndexer:
handlerTask = HandleDeleteIndexer;
break;
case WebApiMethod.GetSonarrConfig:
handlerTask = HandleGetSonarrConfig;
break;
case WebApiMethod.ApplySonarrConfig:
handlerTask = HandleApplySonarrConfig;
break;
case WebApiMethod.TestSonarr:
handlerTask = HandleTestSonarr;
break;
default:
handlerTask = HandleInvalidApiMethod;
break;
@ -125,15 +142,61 @@ namespace Jackett
context.Response.OutputStream.Close();
}
async Task<JToken> HandleTestSonarr(HttpListenerContext context)
{
JToken jsonReply = new JObject();
try
{
await sonarrApi.TestConnection();
jsonReply["result"] = "success";
}
catch (Exception ex)
{
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return jsonReply;
}
async Task<JToken> HandleApplySonarrConfig(HttpListenerContext context)
{
JToken jsonReply = new JObject();
try
{
var postData = await ReadPostDataJson(context.Request.InputStream);
await sonarrApi.ApplyConfiguration(postData);
jsonReply["result"] = "success";
}
catch (Exception ex)
{
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return jsonReply;
}
Task<JToken> HandleGetSonarrConfig(HttpListenerContext context)
{
JObject jsonReply = new JObject();
try
{
jsonReply["config"] = sonarrApi.GetConfiguration().ToJson();
jsonReply["result"] = "success";
}
catch (Exception ex)
{
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Task.FromResult<JToken>(jsonReply);
}
Task<JToken> HandleInvalidApiMethod(HttpListenerContext context)
{
return Task<JToken>.Run(() =>
{
JToken jsonReply = new JObject();
jsonReply["result"] = "error";
jsonReply["error"] = "Invalid API method";
return jsonReply;
});
JToken jsonReply = new JObject();
jsonReply["result"] = "error";
jsonReply["error"] = "Invalid API method";
return Task.FromResult<JToken>(jsonReply);
}
async Task<JToken> HandleConfigForm(HttpListenerContext context)

View file

@ -22,7 +22,7 @@
#page {
border-radius: 6px;
background-color: white;
max-width: 800px;
max-width: 900px;
margin: 0 auto;
margin-top: 30px;
padding: 20px;
@ -35,26 +35,15 @@
display: none;
}
#api-key-input {
width: 300px;
display: inline-block;
font-family: monospace;
}
#api-key-header {
font-size: 20px;
}
.card {
background-color: #f9f9f9;
border-radius: 6px;
box-shadow: 1px 1px 5px 2px #cdcdcd;
padding: 10px;
width: 220px;
width: 260px;
display: inline-block;
margin-right: 30px;
vertical-align: top;
margin-bottom: 30px;
margin: 10px;
}
.unconfigured-indexer {
@ -62,11 +51,12 @@
}
.indexer {
height: 265px;
height: 230px;
}
#add-indexer {
margin-right: 0px;
border: 0;
}
.indexer-logo {
@ -87,13 +77,13 @@
}
.indexer-buttons > .btn {
width: 90px;
margin-bottom: 10px;
}
.indexer-buttons > .btn:nth-child(even) {
margin-left: 5px;
}
.indexer-button-test {
width: 60px;
}
.indexer-add-content {
color: gray;
@ -161,18 +151,67 @@
#setup-indexer-go {
width: 70px;
}
hr {
border-top-color: #cdcdcd;
}
.input-area {
}
.input-area > * {
vertical-align: middle;
}
.input-area > p {
margin-top: 10px;
}
.input-header {
font-size: 18px;
width: 140px;
display: inline-block;
}
.input-right {
width: 300px;
display: inline-block;
font-family: monospace;
}
#sonarr-warning {
display: none;
}
</style>
</head>
<body>
<div id="page">
<h3><span>need a logo..</span></h3>
<div id="api-key">
<span>need a logo..</span>
<span id="api-key-header">API Key: </span>
<input id="api-key-input" class="form-control" type="text" value="" placeholder="API Key" readonly="">
<span>(Same key works for all indexers)</span>
<hr />
<div class="input-area">
<span class="input-header">Sonarr API Host: </span>
<input id="sonarr-host" class="form-control input-right" type="text" readonly />
<button id="sonarr-settings" class="btn btn-primary btn-sm">
Settings <span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
</button>
<button id="sonarr-test" class="btn btn-warning btn-sm">
Test <span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span>
</button>
<p id="sonarr-warning" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign"></span>
Sonarr API must be configured
</p>
</div>
<hr />
<div class="input-area">
<span class="input-header">Jackett API Key: </span>
<input id="api-key-input" class="form-control input-right" type="text" value="" placeholder="API Key" readonly="">
<p>Use this key when adding indexers to Sonarr. This key works for all indexers.</p>
</div>
<br />
<hr />
@ -208,42 +247,44 @@
</div>
</div>
<div id="setup-indexer-modal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-sm">
<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">Setup indexer</h4>
</div>
<div class="modal-body">
<form id="indexer-setup-form"></form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="setup-indexer-go">Okay</button>
<div id="modals"></div>
<div id="templates">
<div class="config-setup-modal modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<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}}</h4>
</div>
<div class="modal-body">
<form class="config-setup-form"></form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary setup-indexer-go">Okay</button>
</div>
</div>
</div>
</div>
</div>
<div id="templates">
<div class="indexer card">
<div class="indexer-logo"><img src="logos/{{id}}.png" /></div>
<div class="indexer-name"><h3>{{name}}</h3></div>
<div class="indexer-buttons">
<button class="btn btn-primary btn-sm" data-id="{{id}}">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
</button>
<button class="btn btn-danger btn-sm indexer-button-delete" data-id="{{id}}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</button>
<a class="btn btn-info btn-sm" target="_blank" href="{{site_link}}">
Visit <span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
<span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
</a>
<button class="btn btn-warning btn-sm indexer-button-test" data-id="{{id}}">
Test <span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span>
</button>
<button class="btn btn-danger btn-sm indexer-button-delete" data-id="{{id}}">
Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</button>
<button class="btn btn-primary btn-sm" data-id="{{id}}">
Configure <span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
</button>
</div>
<div class="indexer-host">
<b>Torznab Host:</b>
@ -266,7 +307,6 @@
<div class="setup-item-value">{{{value_element}}}</div>
</div>
<h4 class="setup-item-header" data-type="indexer" data-name="{{name}}" data-indexer="{{indexer}}">{{name}}</h4>
<input class="setup-item-inputstring form-control" type="text" value="{{{value}}}"></input>
<div class="setup-item-checkbox">
{{#if value}}
@ -276,7 +316,7 @@
{{/if}}
</div>
<img class="setup-item-displayimage" src="{{{value}}}" />
<span class="setup-item-displayinfo">{{{value}}}</span>
<div class="setup-item-displayinfo alert alert-info" role="alert">{{{value}}}</div>
<span class="spinner glyphicon glyphicon-refresh"></span>
@ -284,6 +324,90 @@
<script>
reloadIndexers();
loadSonarrInfo();
function loadSonarrInfo() {
getSonarrConfig(function (data) {
$("#sonarr-host").val("");
var host, port, apiKey;
for (var i = 0; i < data.config.length; i++) {
if (data.config[i].id == "host")
host = data.config[i].value;
if (data.config[i].id == "port")
port = data.config[i].value;
if (data.config[i].id == "apikey")
apiKey = data.config[i].value;
}
if (!apiKey)
$("#sonarr-warning").show();
else {
$("#sonarr-warning").hide();
$("#sonarr-host").val(host + ":" + port);
}
});
}
function getSonarrConfig(callback) {
var jqxhr = $.get("get_sonarr_config", function (data) {
callback(data);
}).fail(function () {
doNotify("Error loading Sonarr API configuration, request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
}
$("#sonarr-test").click(function () {
var jqxhr = $.get("get_indexers", function (data) {
if (data.result == "error")
doNotify("Test failed for Sonarr API\n" + data.error, "danger", "glyphicon glyphicon-alert");
else
doNotify("Test successful for Sonarr API", "success", "glyphicon glyphicon-ok");
}).fail(function () {
doNotify("Error testing Sonarr, request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
});
$("#sonarr-settings").click(function () {
getSonarrConfig(function (data) {
var config = data.config;
var configForm = newConfigModal("Sonarr API", config);
var $goButton = configForm.find(".setup-indexer-go");
$goButton.click(function () {
var data = getConfigModalJson(configForm);
var originalBtnText = $goButton.html();
$goButton.prop('disabled', true);
$goButton.html($('#templates > .spinner')[0].outerHTML);
var jqxhr = $.post("apply_sonarr_config", JSON.stringify(data), function (data) {
if (data.result == "error") {
if (data.config) {
populateSetupForm(data.indexer, data.name, data.config);
}
doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert");
}
else {
configForm.modal("hide");
loadSonarrInfo();
doNotify("Successfully configured Sonarr API", "success", "glyphicon glyphicon-ok");
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
}).always(function () {
$goButton.html(originalBtnText);
$goButton.prop('disabled', false);
});
});
configForm.modal("show");
});
});
function reloadIndexers() {
$('#indexers').hide();
$('#indexers > div.indexer').remove();
@ -372,7 +496,7 @@
return;
}
populateSetupForm(id, data.name, data.config);
$("#setup-indexer-modal").modal("show");
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
});
@ -380,17 +504,80 @@
$("#select-indexer-modal").modal("hide");
}
function populateSetupForm(indexerId, name, config) {
$("#indexer-setup-form").empty();
var setupItemHeader = Handlebars.compile($("#templates > .setup-item-header")[0].outerHTML);
$("#indexer-setup-form").append(setupItemHeader({ indexer: indexerId, name: name }));
function newConfigModal(title, config) {
//config-setup-modal
var configTemplate = Handlebars.compile($("#templates > .config-setup-modal")[0].outerHTML);
var configForm = $(configTemplate({ title: title }));
$("#modals").append(configForm);
var $formItemContainer = configForm.find(".config-setup-form");
var setupItemTemplate = Handlebars.compile($("#templates > .setup-item")[0].outerHTML);
for (var i = 0; i < config.length; i++) {
var item = config[i];
var setupValueTemplate = Handlebars.compile($("#templates > .setup-item-" + item.type)[0].outerHTML);
item.value_element = setupValueTemplate(item);
$("#indexer-setup-form").append(setupItemTemplate(item));
$formItemContainer.append(setupItemTemplate(item));
}
return configForm;
//modal.remove();
}
function getConfigModalJson(configForm) {
var configJson = {};
configForm.find(".config-setup-form").children().each(function (i, el) {
$el = $(el);
var type = $el.data("type");
var id = $el.data("id");
switch (type) {
case "inputstring":
configJson[id] = $el.find(".setup-item-inputstring").val();
break;
case "inputbool":
configJson[id] = $el.find(".setup-item-checkbox").val();
break;
}
});
return configJson;
}
function populateSetupForm(indexerId, name, config) {
var configForm = newConfigModal(name, config);
var $goButton = configForm.find(".setup-indexer-go");
$goButton.click(function () {
var data = { indexer: indexerId, name: name };
data.config = getConfigModalJson(configForm);
var originalBtnText = $goButton.html();
$goButton.prop('disabled', true);
$goButton.html($('#templates > .spinner')[0].outerHTML);
var jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) {
if (data.result == "error") {
if (data.config) {
populateSetupForm(data.indexer, data.name, 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 () {
$goButton.html(originalBtnText);
$goButton.prop('disabled', false);
});
});
configForm.modal("show");
}
function resolveUrl(url) {
@ -400,55 +587,7 @@
return url;
}
$("#setup-indexer-go").click(function () {
var data = { config: {} };
$("#indexer-setup-form").children().each(function (i, el) {
$el = $(el);
var type = $el.data("type");
var id = $el.data("id");
switch (type) {
case "indexer":
data.indexer = $el.data("indexer");
data.name = $el.data("name")
return;
case "inputstring":
data.config[id] = $el.find(".setup-item-inputstring").val();
break;
case "inputbool":
data.config[id] = $el.find(".setup-item-checkbox").val();
break;
}
});
sendSetupData(data.indexer, data.name, data);
});
function sendSetupData(indexerId, name, data) {
var originalBtnText = $('#setup-indexer-go').html();
$('#setup-indexer-go').prop('disabled', true);
$('#setup-indexer-go').html($('#templates > .spinner')[0].outerHTML);
var jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) {
if (data.result == "error") {
if (data.config) {
populateSetupForm(indexerId, name, data.config);
}
doNotify("Configuration failed: " + data.error, "danger", "glyphicon glyphicon-alert");
}
else {
$("#setup-indexer-modal").modal("hide");
reloadIndexers();
doNotify("Successfully configured " + name, "success", "glyphicon glyphicon-ok");
}
}).fail(function () {
doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert");
}).always(function () {
$('#setup-indexer-go').html(originalBtnText);
$('#setup-indexer-go').prop('disabled', false);
});
}
reloadIndexers();
function doNotify(message, type, icon) {
$.notify({