Add CouchPotato interface

This commit is contained in:
KZ 2015-08-02 18:39:32 +01:00
parent 478c41fed6
commit d4b61fe3a9
18 changed files with 328 additions and 32 deletions

View File

@ -1,6 +1,6 @@
## Jackett
This software creates a [Torznab](https://github.com/Sonarr/Sonarr/wiki/Implementing-a-Torznab-indexer) API server on your machine that any Torznab enabled software such as [Sonarr](https://sonarr.tv) can consume.
This software creates a [Torznab](https://github.com/Sonarr/Sonarr/wiki/Implementing-a-Torznab-indexer) (with [nZEDb](https://github.com/nZEDb/nZEDb/blob/master/docs/newznab_api_specification.txt) category numbering) and [TorrentPotato](https://github.com/RuudBurger/CouchPotatoServer/wiki/Couchpotato-torrent-provider) API server on your machine. Torznab enables software such as [Sonarr](https://sonarr.tv) to access data from your favorite indexers in a similar fashion to rss but with added features such as searching. TorrentPotato is an interface accessible to [CouchPotato](https://couchpota.to/).
Jackett works as a proxy server: it translates Torznab queries into tracker-site-specific http queries, parses the html response into Torznab results, then sends results back to the requesting software which allows for getting recent uploads and performing searches.
@ -27,6 +27,7 @@ Download in the [Releases page](https://github.com/zone117x/Jackett/releases)
* [Freshon](https://freshon.tv/)
* [HD-Space](https://hd-space.org/)
* [HD-Torrents.org](https://hd-torrents.org/)
* [Immortalseed.me](http://immortalseed.me)
* [IPTorrents](https://iptorrents.com/)
* [MoreThan.tv](https://morethan.tv/)
* [pretome](https://pretome.info)

View File

@ -156,7 +156,7 @@ namespace JackettTest.Indexers
var indexer = TestUtil.Container.ResolveNamed<IIndexer>(BakaBT.GetIndexerID(typeof(BakaBT))) as BakaBT;
indexer.LoadFromSavedConfiguration(JObject.Parse("{\"cookies\":\"bbtid=c\"}"));
var results = await indexer.PerformQuery(new Jackett.Models.TorznabQuery() { SanitizedSearchTerm = "Series S1", Season = 1 });
var results = await indexer.PerformQuery(new Jackett.Models.TorznabQuery() { SearchTerm = "Series S1", Season = 1 });
results.Count().Should().Be(44);
results.First().Title.Should().Be("Golden Time Season 1 (BD 720p) [FFF]");

View File

@ -48,10 +48,6 @@
height: 120px;
}
.indexer {
height: 180px;
}
.add-indexer {
border: 0;
}

View File

@ -136,7 +136,8 @@ function displayIndexers(items) {
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("/api/" + item.id);
item.torznab_host = resolveUrl("/torznab/" + item.id);
item.potato_host = resolveUrl("/potato/" + item.id);
if (item.configured)
$('#indexers').append(indexerTemplate(item));
else

View File

@ -214,6 +214,13 @@
<div class="indexer-host">
<b>Torznab Host:</b>
<input class="form-control" type="text" value="{{torznab_host}}" placeholder="Torznab Host" readonly="">
<b>CouchPotato Host:</b>
{{#if potatoenabled}}
<input class="form-control" type="text" value="{{potato_host}}" placeholder="Torznab Host" readonly="">
{{else}}
<input class="form-control" type="text" value="Not availible" placeholder="Torznab Host" readonly="">
{{/if}}
</div>
</div>

View File

@ -216,6 +216,7 @@ namespace Jackett.Controllers
item["description"] = indexer.DisplayDescription;
item["configured"] = indexer.IsConfigured;
item["site_link"] = indexer.SiteLink;
item["potatoenabled"] = indexer.TorznabCaps.Categories.Select(c => c.ID).Any(i => PotatoController.MOVIE_CATS.Contains(i));
items.Add(item);
}
jsonReply["items"] = items;

View File

@ -0,0 +1,151 @@
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;
namespace Jackett.Controllers
{
[AllowAnonymous]
public class PotatoController : ApiController
{
private IIndexerManagerService indexerService;
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
private IWebClient webClient;
public static readonly int[] MOVIE_CATS = new int[] {2000, 2040, 2030, 2010};
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.");
}
if (string.IsNullOrWhiteSpace(request.search))
{
// We are searching by IMDB id so look up the name
var response = await webClient.GetString(new Utils.Clients.WebRequest("http://www.omdbapi.com/?type=movie&i=" + request.imdbid));
if (response.Status == HttpStatusCode.OK)
{
JObject result = JObject.Parse(response.Content);
if (result["Title"] != null)
{
request.search = result["Title"].ToString();
}
}
}
var torznabQuery = new TorznabQuery()
{
ApiKey = request.passkey,
Categories = MOVIE_CATS,
SearchTerm = request.search
};
IEnumerable<ReleaseInfo> releases = new List<ReleaseInfo>();
if (!string.IsNullOrWhiteSpace(torznabQuery.SanitizedSearchTerm))
{
releases = await indexer.PerformQuery(torznabQuery);
}
// Cache non query results
if (string.IsNullOrEmpty(torznabQuery.SanitizedSearchTerm))
{
cacheService.CacheRssResults(indexer.DisplayName, releases);
}
releases = indexer.FilterResults(torznabQuery, releases);
var severUrl = string.Format("{0}://{1}:{2}/", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port);
// add Jackett proxy to download links...
foreach (var release in releases)
{
if (release.Link == null || (release.Link.IsAbsoluteUri && release.Link.Scheme == "magnet"))
continue;
var originalLink = release.Link;
var encodedLink = HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(originalLink.ToString())) + "/t.torrent";
var proxyLink = string.Format("{0}api/{1}/download/{2}", severUrl, indexer.ID, encodedLink);
release.Link = new Uri(proxyLink);
}
var potatoResponse = new TorrentPotatoResponse();
foreach(var release in releases)
{
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.ToString(),
// imdb_id = request.imdbid,
freeleech = false,
type = "movie",
size = (long)release.Size/ (1024 * 1024), // This is in MB
leechers = (int)release.Peers - (int)release.Seeders,
seeders = (int)release.Seeders
});
}
// 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} {3}", releases.Count(), indexer.DisplayName, torznabQuery.SanitizedSearchTerm, torznabQuery.GetEpisodeSearchString()));
}
// Force the return as Json
return new HttpResponseMessage()
{
Content = new JsonContent(potatoResponse)
};
}
}
}

View File

@ -15,14 +15,14 @@ using System.Web.Http;
namespace Jackett.Controllers
{
[AllowAnonymous]
public class APIController : ApiController
public class TorznabController : ApiController
{
private IIndexerManagerService indexerService;
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
public APIController(IIndexerManagerService i, Logger l, IServerService s, ICacheService c)
public TorznabController(IIndexerManagerService i, Logger l, IServerService s, ICacheService c)
{
indexerService = i;
logger = l;

View File

@ -69,11 +69,11 @@ namespace Jackett.Indexers
{
// This tracker only deals with full seasons so chop off the episode/season number if we have it D:
if (!string.IsNullOrWhiteSpace(query.SanitizedSearchTerm))
if (!string.IsNullOrWhiteSpace(query.SearchTerm))
{
var splitindex = query.SanitizedSearchTerm.LastIndexOf(' ');
var splitindex = query.SearchTerm.LastIndexOf(' ');
if (splitindex > -1)
query.SanitizedSearchTerm = query.SanitizedSearchTerm.Substring(0, splitindex);
query.SearchTerm = query.SearchTerm.Substring(0, splitindex);
}
var releases = new List<ReleaseInfo>();

View File

@ -170,7 +170,8 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\APIController.cs" />
<Compile Include="Controllers\PotatoController.cs" />
<Compile Include="Controllers\TorznabController.cs" />
<Compile Include="Controllers\DownloadController.cs" />
<Compile Include="Engine.cs" />
<Compile Include="Indexers\AlphaRatio.cs" />
@ -209,6 +210,9 @@
<Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginAnimeBytes.cs" />
<Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginFrenchTorrentDb.cs" />
<Compile Include="Models\IndexerConfig\PretomeConfiguration.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" />
@ -243,6 +247,7 @@
<Compile Include="Utils\DateTimeUtil.cs" />
<Compile Include="Utils\Clients\IWebClient.cs" />
<Compile Include="Utils\JackettAuthorizedAttribute.cs" />
<Compile Include="Utils\JsonContent.cs" />
<Compile Include="Utils\ParseUtil.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Models
{
public class TorrentPotatoResponseItem
{
public string release_name { get; set; }
public string torrent_id { get; set; }
public string details_url { get; set; }
public string download_url { get; set; }
// public string imdb_id { get; set; }
public bool freeleech { get; set; }
public string type { get; set; }
public long size { get; set; }
public int leechers { get; set; }
public int seeders { get; set; }
}
}

View File

@ -22,7 +22,25 @@ namespace Jackett.Models
public int Season { get; set; }
public string Episode { get; set; }
public string SearchTerm { get; set; }
public string SanitizedSearchTerm { get; set; }
public string SanitizedSearchTerm
{
get
{
if (SearchTerm == null)
return string.Empty;
char[] arr = SearchTerm.ToCharArray();
arr = Array.FindAll<char>(arr, c => (char.IsLetterOrDigit(c)
|| char.IsWhiteSpace(c)
|| c == '-'
|| c == '.'
));
var safetitle = new string(arr);
return safetitle;
}
}
public TorznabQuery()
{
@ -46,19 +64,6 @@ namespace Jackett.Models
return episodeString;
}
static string SanitizeSearchTerm(string title)
{
char[] arr = title.ToCharArray();
arr = Array.FindAll<char>(arr, c => (char.IsLetterOrDigit(c)
|| char.IsWhiteSpace(c)
|| c == '-'
|| c == '.'
));
title = new string(arr);
return title;
}
public static TorznabQuery FromHttpQuery(NameValueCollection query)
{
@ -69,12 +74,10 @@ namespace Jackett.Models
if (query["q"] == null)
{
q.SearchTerm = string.Empty;
q.SanitizedSearchTerm = string.Empty;
}
else
{
q.SearchTerm = query["q"];
q.SanitizedSearchTerm = SanitizeSearchTerm(q.SearchTerm);
}
if (query["cat"] != null)

View File

@ -17,7 +17,7 @@ namespace Jackett.Services
public class CacheService : ICacheService
{
private readonly List<TrackerCache> cache = new List<TrackerCache>();
private readonly int MAX_RESULTS_PER_TRACKER = 100;
private readonly int MAX_RESULTS_PER_TRACKER = 250;
private readonly TimeSpan AGE_LIMIT = new TimeSpan(2, 0, 0, 0);
public void CacheRssResults(string trackerId, IEnumerable<ReleaseInfo> releases)

View File

@ -76,13 +76,37 @@ namespace Jackett
config.Routes.MapHttpRoute(
name: "apiDefault",
routeTemplate: "api/{indexerID}",
defaults: new { controller = "API", action = "Call" }
defaults: new { controller = "Torznab", action = "Call" }
);
config.Routes.MapHttpRoute(
name: "api",
routeTemplate: "api/{indexerID}/api",
defaults: new { controller = "API", action = "Call" }
defaults: new { controller = "Torznab", action = "Call" }
);
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" }
);
config.Routes.MapHttpRoute(

View File

@ -14,6 +14,13 @@ namespace Jackett.Utils.Clients
Type = RequestType.GET;
}
public WebRequest(string url)
{
PostData = new Dictionary<string, string>();
Type = RequestType.GET;
Url = url;
}
public string Url { get; set; }
public Dictionary<string, string> PostData { get; set; }
public string Cookies { get; set; }

View File

@ -0,0 +1,40 @@
using Newtonsoft.Json;
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.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Jackett.Utils
{
public class JsonContent : HttpContent
{
private readonly object _value;
public JsonContent(object value)
{
_value = value;
Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
protected override async Task SerializeToStreamAsync(Stream stream,
TransportContext context)
{
var json = JsonConvert.SerializeObject(_value, Formatting.Indented);
var writer = new StreamWriter(stream);
writer.Write(json);
await writer.FlushAsync();
}
protected override bool TryComputeLength(out long length)
{
length = -1;
return false;
}
}
}