mirror of https://github.com/Jackett/Jackett
Add CouchPotato interface
This commit is contained in:
parent
478c41fed6
commit
d4b61fe3a9
|
@ -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)
|
||||
|
|
|
@ -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]");
|
||||
|
|
|
@ -48,10 +48,6 @@
|
|||
height: 120px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.add-indexer {
|
||||
border: 0;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>();
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue