Web UI working

This commit is contained in:
zone117x 2015-04-14 22:32:52 -06:00
parent a4f18471a8
commit e27b696c61
8 changed files with 408 additions and 28 deletions

28
src/Jackett/ApiKey.cs Normal file
View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Jackett
{
public class ApiKey
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
public static string Generate()
{
var randBytes = new byte[32];
var rngCsp = new RNGCryptoServiceProvider();
rngCsp.GetBytes(randBytes);
var key = "";
foreach (var b in randBytes)
{
key += chars[b % chars.Length];
}
return key;
}
}
}

View File

@ -58,7 +58,7 @@ namespace Jackett
client = new HttpClient(handler);
}
public string DisplayName { get { return "BitMeTV.org"; } }
public string DisplayName { get { return "BitMeTV"; } }
public string DisplayDescription { get { return "TV Episode specialty tracker"; } }
public Uri SiteLink { get { return new Uri("https://bitmetv.org"); } }
@ -102,6 +102,7 @@ namespace Jackett
var errorMessage = messageEl.Text();
var captchaImage = await client.GetByteArrayAsync(CaptchaUrl);
config.CaptchaImage.Value = captchaImage;
config.CaptchaText.Value = "";
throw new ExceptionWithConfigData(errorMessage, (ConfigurationData)config);
}
else
@ -114,6 +115,8 @@ namespace Jackett
if (OnSaveConfigurationRequested != null)
OnSaveConfigurationRequested(configSaveData);
IsConfigured = true;
}
});
}
@ -123,6 +126,8 @@ namespace Jackett
return Task.Run(async () =>
{
var result = await client.GetStringAsync(new Uri(SearchUrl));
if (result.Contains("<h1>Not logged in!</h1>"))
throw new Exception("Detected as not logged in");
});
}

View File

@ -67,6 +67,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ApiKey.cs" />
<Compile Include="ChannelInfo.cs" />
<Compile Include="ConfigurationData.cs" />
<Compile Include="DataUrl.cs" />
@ -91,7 +92,7 @@
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
<None Include="WebContent\bootstrap\glyphicons-halflings-regular.woff">
<None Include="WebContent\fonts\glyphicons-halflings-regular.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
@ -105,15 +106,15 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Content Include="WebContent\congruent_outline.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\handlebars-v3.0.1.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\logos\bitmetv.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\logos\freshon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\bootstrap\bootstrap.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -129,6 +130,9 @@
<Content Include="WebContent\jquery-2.1.3.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\logos\freshon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebContent\setup_indexer.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Web;
@ -19,13 +20,15 @@ namespace Jackett
{
GetConfigForm,
ConfigureIndexer,
GetIndexers
GetIndexers,
TestIndexer
}
static Dictionary<string, WebApiMethod> WebApiMethods = new Dictionary<string, WebApiMethod>
{
{ "get_config_form", WebApiMethod.GetConfigForm },
{ "configure_indexer", WebApiMethod.ConfigureIndexer },
{ "get_indexers", WebApiMethod.GetIndexers }
{ "get_indexers", WebApiMethod.GetIndexers },
{ "test_indexer", WebApiMethod.TestIndexer}
};
IndexerManager indexerManager;
@ -63,8 +66,12 @@ namespace Jackett
var contentFile = File.ReadAllBytes(Path.Combine(WebContentFolder, file));
context.Response.ContentType = MimeMapping.GetMimeMapping(file);
context.Response.StatusCode = (int)HttpStatusCode.OK;
await context.Response.OutputStream.WriteAsync(contentFile, 0, contentFile.Length);
context.Response.OutputStream.Close();
try
{
await context.Response.OutputStream.WriteAsync(contentFile, 0, contentFile.Length);
context.Response.OutputStream.Close();
}
catch (HttpListenerException) { }
}
async Task<JToken> ReadPostDataJson(Stream stream)
@ -92,6 +99,7 @@ namespace Jackett
var indexer = indexerManager.GetIndexer(indexerString);
var config = await indexer.GetConfigurationForSetup();
jsonReply["config"] = config.ToJson();
jsonReply["name"] = indexer.DisplayName;
jsonReply["result"] = "success";
}
catch (Exception ex)
@ -106,6 +114,7 @@ namespace Jackett
var postData = await ReadPostDataJson(context.Request.InputStream);
string indexerString = (string)postData["indexer"];
var indexer = indexerManager.GetIndexer(indexerString);
jsonReply["name"] = indexer.DisplayName;
await indexer.ApplyConfiguration(postData["config"]);
await indexer.VerifyConnection();
jsonReply["result"] = "success";
@ -124,15 +133,16 @@ namespace Jackett
try
{
jsonReply["result"] = "success";
jsonReply["api_key"] = ApiKey.Generate();
JArray items = new JArray();
foreach (var i in indexerManager.Indexers)
{
var indexer = i.Value;
var item = new JObject();
item["id"] = i.Key;
item["display_name"] = indexer.DisplayName;
item["display_description"] = indexer.DisplayDescription;
item["is_configured"] = indexer.IsConfigured;
item["name"] = indexer.DisplayName;
item["description"] = indexer.DisplayDescription;
item["configured"] = indexer.IsConfigured;
item["site_link"] = indexer.SiteLink;
items.Add(item);
}
@ -144,6 +154,22 @@ namespace Jackett
jsonReply["error"] = ex.Message;
}
break;
case WebApiMethod.TestIndexer:
try
{
var postData = await ReadPostDataJson(context.Request.InputStream);
string indexerString = (string)postData["indexer"];
var indexer = indexerManager.GetIndexer(indexerString);
jsonReply["name"] = indexer.DisplayName;
await indexer.VerifyConnection();
jsonReply["result"] = "success";
}
catch (Exception ex)
{
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
break;
default:
jsonReply["result"] = "error";
jsonReply["error"] = "Invalid API method";

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -9,36 +9,353 @@
<link href="bootstrap/bootstrap.min.css" rel="stylesheet">
<title>Jackett</title>
<style>
body {
background-image: url("congruent_outline.png");
background-repeat: repeat;
}
#page {
border-radius: 6px;
background-color: white;
max-width: 800px;
margin: 0 auto;
margin-top: 30px;
padding: 20px;
}
.container-fluid {
}
#templates {
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: 225px;
display: inline-block;
margin-right: 30px;
vertical-align: top;
}
.unconfigured-indexer {
height: 170px;
}
.indexer {
height: 220px;
}
#add-indexer {
margin-right: 0px;
}
.indexer-logo {
text-align: center;
}
.indexer-logo > img {
border: 1px solid #828282;
}
.indexer-name > h3 {
margin-top: 13px;
text-align: center;
}
.indexer-buttons {
text-align: center;
}
.indexer-add-content {
color: gray;
text-align: center;
}
.indexer-add-content > .glyphicon {
font-size: 50px;
margin-top: 35%;
vertical-align: bottom;
}
.indexer-add-content > .light-text {
margin-top: 11px;
font-size: 18px;
margin-left: -5px;
}
.indexer-host {
margin-top: 10px;
}
.indexer-host > input {
font-size: 12px;
padding: 2px;
}
.setup-item-inputstring {
max-width: 260px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div id="indexers"></div>
<div id="page">
<div id="api-key">
<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>
</div>
<br />
<hr />
<h3>Configured Indexers</h3>
<div id="indexers">
<a class="indexer card" id="add-indexer" href="#" data-toggle="modal" data-target="#select-indexer-modal">
<div class="indexer-add-content">
<span class="glyphicon glyphicon glyphicon-plus" aria-hidden="true"></span>
<div class="light-text">Add</div>
</div>
</a>
</div>
</div>
<div id="templates">
<div class="indexer">
<div class="indexer-name">{{name}}</div>
<div class="indexer-description">{{description}}</div>
<div class="indexer-link"><a href="{{link}}">{{link}}</a></div>
<div class="indexer-configured">
{{#if configured}}
<span>Configred</span>
{{else}}
<span>Not configured</span>
{{/if}}
<div id="select-indexer-modal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg">
<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">Select an indexer to setup</h4>
</div>
<div class="modal-body">
<div id="unconfigured-indexers">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</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>
</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">
<a class="btn btn-info btn-sm" target="_blank" href="{{site_link}}">Visit <span class="glyphicon glyphicon-new-window" aria-hidden="true"></span></a>
<a class="btn btn-danger btn-sm" href="#">Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
<a class="btn btn-primary btn-sm indexer-button-test" href="#" data-id="{{id}}">Test <span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span></a>
</div>
<div class="indexer-host">
<b>Torznab Host:</b>
<input class="form-control" type="text" value="{{torznab_host}}" placeholder="Torznab Host" readonly="">
</div>
</div>
<div class="unconfigured-indexer card">
<div class="indexer-logo"><img src="logos/{{id}}.png" /></div>
<div class="indexer-name"><h3>{{name}}</h3></div>
<div class="indexer-buttons">
<a class="btn btn-info" target="_blank" href="{{site_link}}">Visit <span class="glyphicon glyphicon-new-window" aria-hidden="true"></span></a>
<button class="indexer-setup btn btn-success" data-id="{{id}}">Setup <span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>
</div>
</div>
<div class="setup-item form-group" data-id="{{id}}" data-value="{{value}}" data-type="{{type}}">
<div class="setup-item-label">{{name}}</div>
<div class="setup-item-value">{{{value_element}}}</div>
</div>
<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}}
<input type="checkbox" class="form-control" checked></input>
{{else}}
<input type="checkbox" class="form-control"></input>
{{/if}}
</div>
<img class="setup-item-displayimage" src="{{{value}}}" />
<span class="setup-item-displayinfo">{{{value}}}</span>
</div>
<script>
$(function () {
var indexerTemplate = Handlebars.compile($("#templates > .indexer").html());
$('#indexers').append(indexerTemplate({ name: "n", description: "desc", link: "http://google.com", configured: true }));
function reloadIndexers() {
$('#indexers').hide();
$('#indexers > div.indexer').remove();
$('#unconfigured-indexers').empty();
var jqxhr = $.get("get_indexers", function (data) {
$("#api-key-input").val(data.api_key);
displayIndexers(data.items);
}).fail(function () {
alert("error");
});
}
function displayIndexers(items) {
var indexerTemplate = Handlebars.compile($("#templates > .indexer")[0].outerHTML);
var unconfiguredIndexerTemplate = Handlebars.compile($("#templates > .unconfigured-indexer")[0].outerHTML);
for (var i = 0; i < items.length; i++) {
var item = items[i];
item.torznab_host = resolveUrl("/torznab/" + item.id);
if (item.configured)
$('#indexers').prepend(indexerTemplate(item));
else
$('#unconfigured-indexers').prepend($(unconfiguredIndexerTemplate(item)));
}
$('#indexers').fadeIn();
prepareSetupButtons();
prepareTestButtons();
}
function prepareSetupButtons() {
$('.indexer-setup').each(function (i, btn) {
var $btn = $(btn);
var id = $btn.data("id");
$btn.click(function () {
displayIndexerSetup(id);
});
});
}
function prepareTestButtons() {
$(".indexer-button-test").each(function (i, btn) {
var $btn = $(btn);
var id = $btn.data("id");
$btn.click(function () {
var jqxhr = $.post("test_indexer", JSON.stringify({ indexer: id }), function (data) {
if (data.result == "error") {
alert("Test failed for " + data.name + "\n" + data.error);
}
else {
alert("Test successful for " + data.name);
}
}).fail(function () {
alert("error");
});
});
});
}
function displayIndexerSetup(id) {
var jqxhr = $.post("get_config_form", JSON.stringify({ indexer: id }), function (data) {
if (data.result == "error") {
alert(data.error);
return;
}
populateSetupForm(id, data.name, data.config);
$("#setup-indexer-modal").modal("show");
}).fail(function () {
alert("error");
});
$("#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 }));
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));
}
}
function resolveUrl(url) {
var a = document.createElement('a');
a.href = url;
url = a.href;
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 jqxhr = $.post("configure_indexer", JSON.stringify(data), function (data) {
if (data.result == "error") {
if (data.config) {
populateSetupForm(indexerId, name, data.config);
}
alert(data.error);
}
else {
$("#setup-indexer-modal").modal("hide");
reloadIndexers();
alert("success");
}
}).fail(function () {
alert("error");
});
}
reloadIndexers();
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 28 KiB