diff --git a/src/Jackett/Content/custom.css b/src/Jackett/Content/custom.css index 34874d758..bdf75e583 100644 --- a/src/Jackett/Content/custom.css +++ b/src/Jackett/Content/custom.css @@ -231,4 +231,16 @@ hr { .jackettlogError { background-color: #FF6060 !important; +} + +.jackettdownloaded { + color: blueviolet; +} + +.jacketdownloadlocal { + padding-left: 10px; +} + +.downloadcolumn { + text-align:center; } \ No newline at end of file diff --git a/src/Jackett/Content/custom.js b/src/Jackett/Content/custom.js index 1161ca1a6..7ed47288c 100644 --- a/src/Jackett/Content/custom.js +++ b/src/Jackett/Content/custom.js @@ -2,14 +2,36 @@ $.ajaxSetup({ cache: false }); reloadIndexers(); loadJackettSettings(); + + $('body').on('click', '.downloadlink', function (e, b) { + $(e.target).addClass('jackettdownloaded'); + }); + + + $('body').on('click', '.jacketdownloadserver', function (event) { + var href = $(event.target).parent().attr('href'); + var jqxhr = $.get(href, function (data) { + if (data.result == "error") { + doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert"); + return; + } else { + doNotify("Downloaded sent to the blackhole successfully.", "success", "glyphicon glyphicon-ok"); + } + }).fail(function () { + doNotify("Request to Jackett server failed", "danger", "glyphicon glyphicon-alert"); + }); + event.preventDefault(); + return false; + }); + }); function loadJackettSettings() { getJackettConfig(function (data) { - $("#api-key-input").val(data.config.api_key); $("#app-version").html(data.app_version); $("#jackett-port").val(data.config.port); + $("#jackett-savedir").val(data.config.blackholedir); $("#jackett-allowext").attr('checked', data.config.external); var password = data.config.password; $("#jackett-adminpwd").val(password); @@ -33,12 +55,14 @@ $("#jackett-show-releases").click(function () { { "targets": 0, "visible": false, - "searchable": false + "searchable": false, + "type": 'date' }, { "targets": 1, "visible": false, - "searchable": false + "searchable": false, + "type": 'date' }, { "targets": 2, @@ -52,7 +76,31 @@ $("#jackett-show-releases").click(function () { "searchable": false, "iDataSort": 1 } - ] + ], + initComplete: function () { + var count = 0; + this.api().columns().every(function () { + count++; + if (count === 5 || count ===7) { + var column = this; + var select = $('') + .appendTo($(column.footer()).empty()) + .on('change', function () { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + + column + .search(val ? '^' + val + '$' : '', true, false) + .draw(); + }); + + column.data().unique().sort().each(function (d, j) { + select.append('') + }); + } + }); + } }); $("#modals").append(releaseDialog); releaseDialog.modal("show"); @@ -80,13 +128,17 @@ $("#view-jackett-logs").click(function () { $("#change-jackett-port").click(function () { var jackett_port = $("#jackett-port").val(); var jackett_external = $("#jackett-allowext").is(':checked'); - var jsonObject = { port: jackett_port, external: jackett_external }; - var jqxhr = $.post("/admin/set_port", JSON.stringify(jsonObject), function (data) { + var jsonObject = { + port: jackett_port, + external: jackett_external, + blackholedir: $("#jackett-savedir").val() + }; + var jqxhr = $.post("/admin/set_config", JSON.stringify(jsonObject), function (data) { if (data.result == "error") { doNotify("Error: " + data.error, "danger", "glyphicon glyphicon-alert"); return; } else { - doNotify("The port has been changed. Redirecting you to the new port.", "success", "glyphicon glyphicon-ok"); + doNotify("Redirecting you to complete configuration update..", "success", "glyphicon glyphicon-ok"); window.setTimeout(function () { url = window.location.href; if (data.external) { diff --git a/src/Jackett/Content/index.html b/src/Jackett/Content/index.html index 8035cff32..6f03dc8c3 100644 --- a/src/Jackett/Content/index.html +++ b/src/Jackett/Content/index.html @@ -50,17 +50,36 @@ {{PublishDate}} {{FirstSeen}} - {{dateFormatRel PublishDate}} - {{dateFormatRel FirstSeen}} + {{jacketTimespan PublishDate}} + {{jacketTimespan FirstSeen}} {{Tracker}} {{Title}} {{CategoryDesc}} {{Seeders}} {{Peers}} - + + + {{#if BlackholeLink}} + + {{/if}} + {{/each}} + + + + + + + + + + + + + + +
+ Manual download blackhole directory: + +
External access: diff --git a/src/Jackett/Content/libs/handlebarsmoment.js b/src/Jackett/Content/libs/handlebarsmoment.js index 851e9d7fd..55062fd16 100644 --- a/src/Jackett/Content/libs/handlebarsmoment.js +++ b/src/Jackett/Content/libs/handlebarsmoment.js @@ -8,10 +8,27 @@ Handlebars.registerHelper('dateFormat', function (context, block) { }; }); -Handlebars.registerHelper('dateFormatRel', function (context, block) { - if (window.moment) { - return moment(context).fromNow(true); - } else { - return context; - }; -}); \ No newline at end of file +Handlebars.registerHelper('jacketTimespan', function (context, block) { + var now = moment(); + var from = moment(context); + var timeSpan = moment.duration(now.diff(from)); + + var minutes = timeSpan.asMinutes(); + if (minutes < 120) { + return Math.round(minutes) + 'm ago'; + } + + var hours = timeSpan.asHours(); + if (hours < 48) { + return Math.round(hours) + 'h ago'; + } + + var days = timeSpan.asDays(); + if (days < 365) { + return Math.round(days) + 'd ago'; + } + + var years = timeSpan.asYears(); + return Math.round(years) + 'y ago'; +}); + diff --git a/src/Jackett/Controllers/AdminController.cs b/src/Jackett/Controllers/AdminController.cs index 04f215dbd..5762c7a64 100644 --- a/src/Jackett/Controllers/AdminController.cs +++ b/src/Jackett/Controllers/AdminController.cs @@ -286,6 +286,7 @@ namespace Jackett.Controllers cfg["port"] = serverService.Config.Port; cfg["external"] = serverService.Config.AllowExternal; cfg["api_key"] = serverService.Config.APIKey; + cfg["blackholedir"] = serverService.Config.BlackholeDir; cfg["password"] = string.IsNullOrEmpty(serverService.Config.AdminPassword )? string.Empty:serverService.Config.AdminPassword.Substring(0,10); jsonReply["config"] = cfg; @@ -300,7 +301,7 @@ namespace Jackett.Controllers return Json(jsonReply); } - [Route("set_port")] + [Route("set_config")] [HttpPost] public async Task SetConfig() { @@ -312,6 +313,7 @@ namespace Jackett.Controllers var postData = await ReadPostDataJson(); int port = (int)postData["port"]; bool external = (bool)postData["external"]; + string saveDir = (string)postData["blackholedir"]; if (port != Engine.Server.Config.Port || external != Engine.Server.Config.AllowExternal) { @@ -361,6 +363,21 @@ namespace Jackett.Controllers })).Start(); } + + if(saveDir != Engine.Server.Config.BlackholeDir) + { + if (!string.IsNullOrEmpty(saveDir)) + { + if (!Directory.Exists(saveDir)) + { + throw new Exception("Blackhole directory does not exist"); + } + } + + Engine.Server.Config.BlackholeDir = saveDir; + Engine.Server.SaveConfig(); + } + jsonReply["result"] = "success"; jsonReply["port"] = port; jsonReply["external"] = external; diff --git a/src/Jackett/Controllers/BlackholeController.cs b/src/Jackett/Controllers/BlackholeController.cs new file mode 100644 index 000000000..88a4ecae1 --- /dev/null +++ b/src/Jackett/Controllers/BlackholeController.cs @@ -0,0 +1,71 @@ +using Jackett.Services; +using Newtonsoft.Json.Linq; +using NLog; +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; +using System.Web; +using System.Web.Http; + +namespace Jackett.Controllers +{ + [AllowAnonymous] + public class BlackholeController : ApiController + { + private Logger logger; + private IIndexerManagerService indexerService; + + public BlackholeController(IIndexerManagerService i, Logger l) + { + logger = l; + indexerService = i; + } + + [HttpGet] + public async Task Blackhole(string indexerID, string path) + { + + var jsonReply = new JObject(); + try + { + var indexer = indexerService.GetIndexer(indexerID); + if (!indexer.IsConfigured) + { + logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName)); + throw new Exception("This indexer is not configured."); + } + + var remoteFile = Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(path)); + var downloadBytes = await indexer.Download(new Uri(remoteFile, UriKind.RelativeOrAbsolute)); + + if (string.IsNullOrWhiteSpace(Engine.Server.Config.BlackholeDir)) + { + throw new Exception("Blackhole directory not set!"); + } + + if (!Directory.Exists(Engine.Server.Config.BlackholeDir)) + { + throw new Exception("Blackhole directory does not exist: " + Engine.Server.Config.BlackholeDir); + } + + var fileName = DateTime.Now.Ticks + ".torrent"; + File.WriteAllBytes(Path.Combine(Engine.Server.Config.BlackholeDir, fileName), downloadBytes); + jsonReply["result"] = "success"; + } + catch (Exception ex) + { + logger.Error(ex, "Error downloading to blackhole " + indexerID + " " + path); + jsonReply["result"] = "error"; + jsonReply["error"] = ex.Message; + } + + return Json(jsonReply); + } + } +} diff --git a/src/Jackett/Controllers/PotatoController.cs b/src/Jackett/Controllers/PotatoController.cs index 66b15a973..5815baa79 100644 --- a/src/Jackett/Controllers/PotatoController.cs +++ b/src/Jackett/Controllers/PotatoController.cs @@ -100,15 +100,14 @@ namespace Jackett.Controllers } releases = indexer.FilterResults(torznabQuery, releases); - - var severUrl = string.Format("{0}://{1}:{2}/", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port); - - var proxiedReleases = releases.Select(s => Mapper.Map(s).ConvertToProxyLink(severUrl, indexerID)); - + var serverUrl = string.Format("{0}://{1}:{2}/", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port); var potatoResponse = new TorrentPotatoResponse(); - foreach(var release in proxiedReleases) + foreach(var r in releases) { + var release = Mapper.Map(r); + release.Link = release.ConvertToProxyLink(serverUrl, indexerID); + 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 >.> diff --git a/src/Jackett/Controllers/TorznabController.cs b/src/Jackett/Controllers/TorznabController.cs index 515e62d44..fed5a110a 100644 --- a/src/Jackett/Controllers/TorznabController.cs +++ b/src/Jackett/Controllers/TorznabController.cs @@ -64,21 +64,25 @@ namespace Jackett.Controllers var releases = await indexer.PerformQuery(torznabQuery); + // Some trackers do not keep their clocks up to date and can be ~20 minutes out! + foreach(var release in releases) + { + if (release.PublishDate > DateTime.Now) + release.PublishDate = DateTime.Now; + } + + // Some trackers do not support multiple category filtering so filter the releases that match manually. + var filteredReleases = releases = indexer.FilterResults(torznabQuery, releases); int? newItemCount = null; + // Cache non query results if (string.IsNullOrEmpty(torznabQuery.SanitizedSearchTerm)) { - newItemCount = cacheService.CacheRssResults(indexer, releases); + newItemCount = cacheService.GetNewItemCount(indexer, filteredReleases); + cacheService.CacheRssResults(indexer, releases); } - var releaseCount = releases.Count(); - releases = indexer.FilterResults(torznabQuery, releases); - - var removedInFilterCount = releaseCount - releases.Count(); - if (newItemCount.HasValue) - newItemCount -= removedInFilterCount; - // Log info var logBuilder = new StringBuilder(); if (newItemCount != null) { @@ -94,20 +98,27 @@ namespace Jackett.Controllers logger.Info(logBuilder.ToString()); - var severUrl = string.Format("{0}://{1}:{2}/", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port); + var serverUrl = string.Format("{0}://{1}:{2}/", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port); var resultPage = new ResultPage(new ChannelInfo { Title = indexer.DisplayName, Description = indexer.DisplayDescription, Link = new Uri(indexer.SiteLink), - ImageUrl = new Uri(severUrl + "logos/" + indexer.ID + ".png"), + ImageUrl = new Uri(serverUrl + "logos/" + indexer.ID + ".png"), ImageTitle = indexer.DisplayName, ImageLink = new Uri(indexer.SiteLink), ImageDescription = indexer.DisplayName }); - resultPage.Releases.AddRange(releases.Select(s=>Mapper.Map(s).ConvertToProxyLink(severUrl, indexerID))); - var xml = resultPage.ToXml(new Uri(severUrl)); + + foreach(var result in releases) + { + var clone = Mapper.Map(result); + clone.Link = clone.ConvertToProxyLink(serverUrl, indexerID); + resultPage.Releases.Add(result); + } + + var xml = resultPage.ToXml(new Uri(serverUrl)); // Force the return as XML return new HttpResponseMessage() { diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 89b91340a..85f6c83d4 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -172,6 +172,7 @@ + diff --git a/src/Jackett/Models/Config/ServerConfig.cs b/src/Jackett/Models/Config/ServerConfig.cs index d2c1b3fc0..02649cead 100644 --- a/src/Jackett/Models/Config/ServerConfig.cs +++ b/src/Jackett/Models/Config/ServerConfig.cs @@ -20,6 +20,7 @@ namespace Jackett.Models.Config public string APIKey { get; set; } public string AdminPassword { get; set; } public string InstanceId { get; set; } + public string BlackholeDir { get; set; } public string[] GetListenAddresses(bool? external = null) { diff --git a/src/Jackett/Models/ReleaseInfo.cs b/src/Jackett/Models/ReleaseInfo.cs index baa3b7990..b4dd95d19 100644 --- a/src/Jackett/Models/ReleaseInfo.cs +++ b/src/Jackett/Models/ReleaseInfo.cs @@ -92,15 +92,14 @@ namespace Jackett.Models return (long)(kb * 1024f); } - public ReleaseInfo ConvertToProxyLink(string serverUrl, string indexerId) + public Uri ConvertToProxyLink(string serverUrl, string indexerId, string action = "download") { if (Link == null || (Link.IsAbsoluteUri && Link.Scheme == "magnet")) - return this; + return Link; var originalLink = Link; var encodedLink = HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(originalLink.ToString())) + "/t.torrent"; - var proxyLink = string.Format("{0}api/{1}/download/{2}", serverUrl, indexerId, encodedLink); - Link = new Uri(proxyLink); - return this; + var proxyLink = string.Format("{0}api/{1}/{2}/{3}", serverUrl, indexerId, action, encodedLink); + return new Uri(proxyLink); } } } diff --git a/src/Jackett/Models/TrackerCacheResult.cs b/src/Jackett/Models/TrackerCacheResult.cs index 13b1569e8..1475c4ac8 100644 --- a/src/Jackett/Models/TrackerCacheResult.cs +++ b/src/Jackett/Models/TrackerCacheResult.cs @@ -11,5 +11,6 @@ namespace Jackett.Models public DateTime FirstSeen { get; set; } public string Tracker { get; set; } public string CategoryDesc { get; set; } + public Uri BlackholeLink { get; set; } } } diff --git a/src/Jackett/Services/CacheService.cs b/src/Jackett/Services/CacheService.cs index 2ff1d73be..536c68620 100644 --- a/src/Jackett/Services/CacheService.cs +++ b/src/Jackett/Services/CacheService.cs @@ -11,21 +11,21 @@ namespace Jackett.Services { public interface ICacheService { - int CacheRssResults(IIndexer indexer, IEnumerable releases); + void CacheRssResults(IIndexer indexer, IEnumerable releases); List GetCachedResults(string serverUrl); + int GetNewItemCount(IIndexer indexer, IEnumerable releases); } public class CacheService : ICacheService { private readonly List cache = new List(); - private readonly int MAX_RESULTS_PER_TRACKER = 250; + private readonly int MAX_RESULTS_PER_TRACKER = 1000; private readonly TimeSpan AGE_LIMIT = new TimeSpan(7, 0, 0, 0); - public int CacheRssResults(IIndexer indexer, IEnumerable releases) + public void CacheRssResults(IIndexer indexer, IEnumerable releases) { lock (cache) { - int newItemCount = 0; var trackerCache = cache.Where(c => c.TrackerId == indexer.ID).FirstOrDefault(); if (trackerCache == null) { @@ -49,7 +49,6 @@ namespace Jackett.Services existingItem = new CachedResult(); existingItem.Created = DateTime.Now; trackerCache.Results.Add(existingItem); - newItemCount++; } existingItem.Result = release; @@ -60,6 +59,28 @@ namespace Jackett.Services { tracker.Results = tracker.Results.OrderByDescending(i => i.Created).Take(MAX_RESULTS_PER_TRACKER).ToList(); } + } + } + + public int GetNewItemCount(IIndexer indexer, IEnumerable releases) + { + lock (cache) + { + int newItemCount = 0; + var trackerCache = cache.Where(c => c.TrackerId == indexer.ID).FirstOrDefault(); + if (trackerCache != null) + { + foreach (var release in releases) + { + if (trackerCache.Results.Where(i => i.Result.Guid == release.Guid).Count() == 0) + { + newItemCount++; + } + } + } + else { + newItemCount++; + } return newItemCount; } @@ -79,12 +100,15 @@ namespace Jackett.Services item.FirstSeen = release.Created; item.Tracker = tracker.TrackerName; item.Peers = item.Peers - item.Seeders; // Use peers as leechers - item.ConvertToProxyLink(serverUrl, tracker.TrackerId); + item.Link = item.ConvertToProxyLink(serverUrl, tracker.TrackerId); + if(item.Link!=null && item.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(Engine.Server.Config.BlackholeDir)) + item.BlackholeLink = item.ConvertToProxyLink(serverUrl, tracker.TrackerId, "blackhole"); + results.Add(item); } } - return results.OrderByDescending(i=>i.PublishDate).ToList(); + return results.Take(3000).OrderByDescending(i=>i.PublishDate).ToList(); } } } diff --git a/src/Jackett/Services/LogCacheService.cs b/src/Jackett/Services/LogCacheService.cs index 66d89b610..4d8751a3b 100644 --- a/src/Jackett/Services/LogCacheService.cs +++ b/src/Jackett/Services/LogCacheService.cs @@ -30,7 +30,7 @@ namespace Jackett.Services Message = l.Message, When = l.TimeStamp }); - logs = logs.Take(100).ToList(); + logs = logs.Take(50).ToList(); } } diff --git a/src/Jackett/Startup.cs b/src/Jackett/Startup.cs index d427afd4a..5a6419223 100644 --- a/src/Jackett/Startup.cs +++ b/src/Jackett/Startup.cs @@ -115,6 +115,12 @@ namespace Jackett defaults: new { controller = "Download", action = "Download" } ); + config.Routes.MapHttpRoute( + name: "blackhole", + routeTemplate: "api/{indexerID}/blackhole/{path}/t.torrent", + defaults: new { controller = "Blackhole", action = "Blackhole" } + ); + appBuilder.UseFileServer(new FileServerOptions { RequestPath = new PathString(string.Empty),