From a911b3932ed185a7b28007c50861a7c890362a66 Mon Sep 17 00:00:00 2001 From: Kayomani Date: Sun, 2 Aug 2015 18:39:32 +0100 Subject: [PATCH] Add CouchPotato interface --- README.md | 3 +- src/Jackett.Test/Indexers/BakaBTTests.cs | 2 +- src/Jackett/Content/custom.css | 4 - src/Jackett/Content/custom.js | 3 +- src/Jackett/Content/index.html | 7 + src/Jackett/Controllers/AdminController.cs | 1 + src/Jackett/Controllers/PotatoController.cs | 151 ++++++++++++++++++ ...{APIController.cs => TorznabController.cs} | 4 +- src/Jackett/Indexers/BakaBT.cs | 6 +- src/Jackett/Jackett.csproj | 7 +- src/Jackett/Models/TorrentPotatoRequest.cs | 16 ++ src/Jackett/Models/TorrentPotatoResponse.cs | 22 +++ .../Models/TorrentPotatoResponseItem.cs | 22 +++ src/Jackett/Models/TorznabQuery.cs | 35 ++-- src/Jackett/Services/CacheService.cs | 2 +- src/Jackett/Startup.cs | 28 +++- src/Jackett/Utils/Clients/WebRequest.cs | 7 + src/Jackett/Utils/JsonContent.cs | 40 +++++ 18 files changed, 328 insertions(+), 32 deletions(-) create mode 100644 src/Jackett/Controllers/PotatoController.cs rename src/Jackett/Controllers/{APIController.cs => TorznabController.cs} (96%) create mode 100644 src/Jackett/Models/TorrentPotatoRequest.cs create mode 100644 src/Jackett/Models/TorrentPotatoResponse.cs create mode 100644 src/Jackett/Models/TorrentPotatoResponseItem.cs create mode 100644 src/Jackett/Utils/JsonContent.cs diff --git a/README.md b/README.md index cf634077d..a822d6b4e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/Jackett.Test/Indexers/BakaBTTests.cs b/src/Jackett.Test/Indexers/BakaBTTests.cs index 13c7c26b3..660ae9328 100644 --- a/src/Jackett.Test/Indexers/BakaBTTests.cs +++ b/src/Jackett.Test/Indexers/BakaBTTests.cs @@ -156,7 +156,7 @@ namespace JackettTest.Indexers var indexer = TestUtil.Container.ResolveNamed(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]"); diff --git a/src/Jackett/Content/custom.css b/src/Jackett/Content/custom.css index 32415c386..73159edbc 100644 --- a/src/Jackett/Content/custom.css +++ b/src/Jackett/Content/custom.css @@ -48,10 +48,6 @@ height: 120px; } -.indexer { - height: 180px; -} - .add-indexer { border: 0; } diff --git a/src/Jackett/Content/custom.js b/src/Jackett/Content/custom.js index ad73e89be..5aca89c2b 100644 --- a/src/Jackett/Content/custom.js +++ b/src/Jackett/Content/custom.js @@ -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 diff --git a/src/Jackett/Content/index.html b/src/Jackett/Content/index.html index e7f9e1507..22157c4ff 100644 --- a/src/Jackett/Content/index.html +++ b/src/Jackett/Content/index.html @@ -214,6 +214,13 @@
Torznab Host: + CouchPotato Host: + {{#if potatoenabled}} + + + {{else}} + + {{/if}}
diff --git a/src/Jackett/Controllers/AdminController.cs b/src/Jackett/Controllers/AdminController.cs index 37a0f3b8b..651a92aac 100644 --- a/src/Jackett/Controllers/AdminController.cs +++ b/src/Jackett/Controllers/AdminController.cs @@ -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; diff --git a/src/Jackett/Controllers/PotatoController.cs b/src/Jackett/Controllers/PotatoController.cs new file mode 100644 index 000000000..b3245f534 --- /dev/null +++ b/src/Jackett/Controllers/PotatoController.cs @@ -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 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 releases = new List(); + + 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) + }; + } + } +} diff --git a/src/Jackett/Controllers/APIController.cs b/src/Jackett/Controllers/TorznabController.cs similarity index 96% rename from src/Jackett/Controllers/APIController.cs rename to src/Jackett/Controllers/TorznabController.cs index 836aad9bc..304921ba2 100644 --- a/src/Jackett/Controllers/APIController.cs +++ b/src/Jackett/Controllers/TorznabController.cs @@ -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; diff --git a/src/Jackett/Indexers/BakaBT.cs b/src/Jackett/Indexers/BakaBT.cs index 7575501a3..5680e1748 100644 --- a/src/Jackett/Indexers/BakaBT.cs +++ b/src/Jackett/Indexers/BakaBT.cs @@ -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(); diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 21bc6aeac..a8507582c 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -170,7 +170,8 @@ - + + @@ -209,6 +210,9 @@ + + + @@ -243,6 +247,7 @@ + diff --git a/src/Jackett/Models/TorrentPotatoRequest.cs b/src/Jackett/Models/TorrentPotatoRequest.cs new file mode 100644 index 000000000..4581bcaa0 --- /dev/null +++ b/src/Jackett/Models/TorrentPotatoRequest.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; } + } +} diff --git a/src/Jackett/Models/TorrentPotatoResponse.cs b/src/Jackett/Models/TorrentPotatoResponse.cs new file mode 100644 index 000000000..ea76ff6ac --- /dev/null +++ b/src/Jackett/Models/TorrentPotatoResponse.cs @@ -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(); + } + public List results { get; set; } + + public int total_results + { + get { return results.Count; } + } + } +} diff --git a/src/Jackett/Models/TorrentPotatoResponseItem.cs b/src/Jackett/Models/TorrentPotatoResponseItem.cs new file mode 100644 index 000000000..809ada2b6 --- /dev/null +++ b/src/Jackett/Models/TorrentPotatoResponseItem.cs @@ -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; } + } +} diff --git a/src/Jackett/Models/TorznabQuery.cs b/src/Jackett/Models/TorznabQuery.cs index fd4b64efc..b3bd85782 100644 --- a/src/Jackett/Models/TorznabQuery.cs +++ b/src/Jackett/Models/TorznabQuery.cs @@ -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(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(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) diff --git a/src/Jackett/Services/CacheService.cs b/src/Jackett/Services/CacheService.cs index 1289a393d..84548bf37 100644 --- a/src/Jackett/Services/CacheService.cs +++ b/src/Jackett/Services/CacheService.cs @@ -17,7 +17,7 @@ namespace Jackett.Services public class CacheService : ICacheService { private readonly List cache = new List(); - 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 releases) diff --git a/src/Jackett/Startup.cs b/src/Jackett/Startup.cs index 019fa2685..d427afd4a 100644 --- a/src/Jackett/Startup.cs +++ b/src/Jackett/Startup.cs @@ -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( diff --git a/src/Jackett/Utils/Clients/WebRequest.cs b/src/Jackett/Utils/Clients/WebRequest.cs index 5dd60ea9b..78fc2f463 100644 --- a/src/Jackett/Utils/Clients/WebRequest.cs +++ b/src/Jackett/Utils/Clients/WebRequest.cs @@ -14,6 +14,13 @@ namespace Jackett.Utils.Clients Type = RequestType.GET; } + public WebRequest(string url) + { + PostData = new Dictionary(); + Type = RequestType.GET; + Url = url; + } + public string Url { get; set; } public Dictionary PostData { get; set; } public string Cookies { get; set; } diff --git a/src/Jackett/Utils/JsonContent.cs b/src/Jackett/Utils/JsonContent.cs new file mode 100644 index 000000000..412962bda --- /dev/null +++ b/src/Jackett/Utils/JsonContent.cs @@ -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; + } + } +}