diff --git a/README.md b/README.md index b82168956..cc30745d4 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,20 @@ We were previously focused on TV but are working on extending searches to allow #### Supported Systems * Windows using .NET 4.5 -* Linux and OSX using Mono 4 +* Linux and OSX using Mono 4 (v3 should work but you may experience crashes). #### Supported Trackers * [AlphaRatio](https://alpharatio.cc/) * [AnimeBytes](https://animebytes.tv/) + * [Avistaz](https://avistaz.to/) * [BakaBT](http://bakabt.me/) * [bB](http://reddit.com/r/baconbits) * [BeyondHD](https://beyondhd.me/) * [BIT-HDTV](https://www.bit-hdtv.com) * [BitMeTV](http://www.bitmetv.org/) * [Demonoid](http://www.demonoid.pw/) + * [EuTorrents](https://eutorrents.to/) * [FileList](http://filelist.ro/) * [FrenchTorrentDb](http://www.frenchtorrentdb.com/) * [Freshon](https://freshon.tv/) @@ -36,6 +38,7 @@ We were previously focused on TV but are working on extending searches to allow * [MoreThan.tv](https://morethan.tv/) * [pretome](https://pretome.info) * [PrivateHD](https://privatehd.to/) + * [RARGB](https://rarbg.to/) * [RuTor](http://rutor.org/) * [SceneAccess](https://sceneaccess.eu/login) * [SceneTime](https://www.scenetime.com/) @@ -55,21 +58,19 @@ We were previously focused on TV but are working on extending searches to allow 2. Install libcurl: * Debian/Ubunutu: apt-get install libcurl-dev * Redhat/Fedora: yum install libcurl-devel - * Or see the [Curl docs](http://curl.haxx.se/dlwiz/?type=devel). + * For other distros see the [Curl docs](http://curl.haxx.se/dlwiz/?type=devel). +3. Download and extract the latest ```.tar.bz2``` release from the [website](http://jackett.net/Download) and run Jackett using mono with the command "mono JackettConsole.exe". #### Installation on Windows +Grab the latest release from the [web site](http://jackett.net/Download). + We recommend you install Jackett as a Windows service using the supplied installer. When installed as a service the tray icon acts as a way to open/start/stop Jackett. If you opted to not install it as a service then Jackett will run its web server from the tray tool. Jackett can also be run from the command line using JackettConsole.exe if you would like to see log messages (Ensure the server isn't already running from the tray/service). -#### Installation on Linux/OSX - -Run Jackett using mono with the command "mono JackettConsole.exe". - - #### Troubleshooting @@ -87,11 +88,11 @@ You can get additional logging with the switches "-t -l". Please post logs if y ### Additional Trackers -Jackett's framework allows our team (and any other volunteering dev) to implement new trackers in an hour or two. If you'd like support for a new tracker then feel free to leave a request on the [issues page](https://github.com/zone117x/Jackett/issues) or contact us on IRC (see below). +Jackett's framework allows our team (and any other volunteering dev) to implement new trackers in an hour or two. If you'd like support for a new tracker then feel free to leave a request on the [issues page](https://github.com/zone117x/Jackett/issues) or contact us on IRC (see below). Pull requests must be made to the develop branch. ### Contact & Support Use the github issues pages or talk to us directly at: [irc.freenode.net#jackett](http://webchat.freenode.net/?channels=#jackett). ### Screenshots -![screenshot](http://i.imgur.com/t1sVva6.png "screenshot") +![screenshot](http://i.imgur.com/t1sVva6.png "screenshot") \ No newline at end of file diff --git a/src/Jackett/Content/logos/avistaz.png b/src/Jackett/Content/logos/avistaz.png new file mode 100644 index 000000000..4944a926e Binary files /dev/null and b/src/Jackett/Content/logos/avistaz.png differ diff --git a/src/Jackett/Content/logos/eutorrents.png b/src/Jackett/Content/logos/eutorrents.png new file mode 100644 index 000000000..3c3983696 Binary files /dev/null and b/src/Jackett/Content/logos/eutorrents.png differ diff --git a/src/Jackett/Content/logos/rarbg.png b/src/Jackett/Content/logos/rarbg.png new file mode 100644 index 000000000..da8bebbbc Binary files /dev/null and b/src/Jackett/Content/logos/rarbg.png differ diff --git a/src/Jackett/Controllers/AdminController.cs b/src/Jackett/Controllers/AdminController.cs index 47d49fa86..3a091f2ab 100644 --- a/src/Jackett/Controllers/AdminController.cs +++ b/src/Jackett/Controllers/AdminController.cs @@ -67,7 +67,7 @@ namespace Jackett.Controllers result.Content = new StreamContent(stream); result.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(mappedPath)); - + return result; } @@ -86,7 +86,7 @@ namespace Jackett.Controllers [AllowAnonymous] public async Task Dashboard() { - if(Request.RequestUri.Query!=null && Request.RequestUri.Query.Contains("logout")) + if (Request.RequestUri.Query != null && Request.RequestUri.Query.Contains("logout")) { var file = GetFile("login.html"); securityService.Logout(file); @@ -98,16 +98,18 @@ namespace Jackett.Controllers { return GetFile("index.html"); - } else + } + else { var formData = await Request.Content.ReadAsFormDataAsync(); - - if (formData!=null && securityService.HashPassword(formData["password"]) == serverService.Config.AdminPassword) + + if (formData != null && securityService.HashPassword(formData["password"]) == serverService.Config.AdminPassword) { var file = GetFile("index.html"); securityService.Login(file); return file; - } else + } + else { return GetFile("login.html"); } @@ -180,8 +182,15 @@ namespace Jackett.Controllers string indexerString = (string)postData["indexer"]; indexer = indexerService.GetIndexer((string)postData["indexer"]); jsonReply["name"] = indexer.DisplayName; - await indexer.ApplyConfiguration(postData["config"]); - await indexerService.TestIndexer((string)postData["indexer"]); + var configurationResult = await indexer.ApplyConfiguration(postData["config"]); + if (configurationResult == IndexerConfigurationStatus.RequiresTesting) + { + await indexerService.TestIndexer((string)postData["indexer"]); + } + else if (configurationResult == IndexerConfigurationStatus.Failed) + { + throw new Exception("Configuration Failed"); + } jsonReply["result"] = "success"; } catch (Exception ex) @@ -194,10 +203,11 @@ namespace Jackett.Controllers if (ex is ExceptionWithConfigData) { jsonReply["config"] = ((ExceptionWithConfigData)ex).ConfigData.ToJson(null); - } else + } + else { logger.Error(ex, "Exception in Configure"); - } + } } return Json(jsonReply); } @@ -293,12 +303,13 @@ namespace Jackett.Controllers 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); + cfg["password"] = string.IsNullOrEmpty(serverService.Config.AdminPassword) ? string.Empty : serverService.Config.AdminPassword.Substring(0, 10); jsonReply["config"] = cfg; jsonReply["app_version"] = config.GetVersion(); jsonReply["result"] = "success"; - }catch (Exception ex) + } + catch (Exception ex) { logger.Error(ex, "Exception in get_jackett_config"); jsonReply["result"] = "error"; @@ -360,7 +371,8 @@ namespace Jackett.Controllers } } - (new Thread(() => { + (new Thread(() => + { Thread.Sleep(500); serverService.Stop(); Engine.BuildContainer(); @@ -370,7 +382,7 @@ namespace Jackett.Controllers } - if(saveDir != Engine.Server.Config.BlackholeDir) + if (saveDir != Engine.Server.Config.BlackholeDir) { if (!string.IsNullOrEmpty(saveDir)) { @@ -435,12 +447,12 @@ namespace Jackett.Controllers var query = new TorznabQuery() { SearchTerm = value.Query, - Categories = value.Category ==0?new int[0]: new int[1] { value.Category } + Categories = value.Category == 0 ? new int[0] : new int[1] { value.Category } }; query.ExpandCatsToSubCats(); - var trackers = indexerService.GetAllIndexers().Where(t=>t.IsConfigured).ToList(); + var trackers = indexerService.GetAllIndexers().Where(t => t.IsConfigured).ToList(); if (!string.IsNullOrWhiteSpace(value.Tracker)) { trackers = trackers.Where(t => t.ID == value.Tracker).ToList(); @@ -453,7 +465,8 @@ namespace Jackett.Controllers Parallel.ForEach(trackers.ToList(), indexer => { - try { + try + { var searchResults = indexer.PerformQuery(query).Result; cacheService.CacheRssResults(indexer, searchResults); searchResults = indexer.FilterResults(query, searchResults); @@ -470,7 +483,7 @@ namespace Jackett.Controllers } } } - catch(Exception e) + catch (Exception e) { logger.Error(e, "An error occured during manual search on " + indexer.DisplayName + ": " + e.Message); } @@ -483,10 +496,10 @@ namespace Jackett.Controllers results = results.OrderByDescending(d => d.PublishDate).ToList(); } - var manualResult = new ManualSearchResult() + var manualResult = new ManualSearchResult() { Results = results, - Indexers = trackers.Select(t=>t.DisplayName).ToList() + Indexers = trackers.Select(t => t.DisplayName).ToList() }; diff --git a/src/Jackett/Indexers/Abstract/AvistazTracker.cs b/src/Jackett/Indexers/Abstract/AvistazTracker.cs new file mode 100644 index 000000000..72ef2a29e --- /dev/null +++ b/src/Jackett/Indexers/Abstract/AvistazTracker.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jackett.Models; +using Newtonsoft.Json.Linq; +using NLog; +using Jackett.Utils; +using System.Net; +using System.Net.Http; +using CsQuery; +using System.Web; +using Jackett.Services; +using Jackett.Utils.Clients; +using System.Text.RegularExpressions; +using Jackett.Models.IndexerConfig; + +namespace Jackett.Indexers +{ + public abstract class AvistazTracker : BaseIndexer + { + private string LoginUrl { get { return SiteLink + "auth/login"; } } + private string SearchUrl { get { return SiteLink + "torrents?in=1&type={0}&search={1}"; } } + + new ConfigurationDataBasicLogin configData + { + get { return (ConfigurationDataBasicLogin)base.configData; } + set { base.configData = value; } + } + + public AvistazTracker(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService, string name, string desc, string link) + : base(name: name, + description: desc, + link: link, + caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + manager: indexerManager, + client: webClient, + logger: logger, + p: protectionService, + configData: new ConfigurationDataBasicLogin()) + { + AddCategoryMapping(1, TorznabCatType.Movies); + AddCategoryMapping(1, TorznabCatType.MoviesForeign); + AddCategoryMapping(1, TorznabCatType.MoviesHD); + AddCategoryMapping(1, TorznabCatType.MoviesSD); + AddCategoryMapping(2, TorznabCatType.TV); + AddCategoryMapping(3, TorznabCatType.Audio); + } + + public async Task ApplyConfiguration(JToken configJson) + { + configData.LoadValuesFromJson(configJson); + var loginPage = await RequestStringWithCookies(LoginUrl, string.Empty); + var token = new Regex("Avz.CSRF_TOKEN = '(.*?)';").Match(loginPage.Content).Groups[1].ToString(); + var pairs = new Dictionary { + { "_token", token }, + { "username_email", configData.Username.Value }, + { "password", configData.Password.Value }, + { "remember", "on" } + }; + + var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true, null, LoginUrl); + await ConfigureIfOK(result.Cookies, result.Content != null && result.Content.Contains("auth/logout"), () => + { + CQ dom = result.Content; + var messageEl = dom[".form-error"]; + var errorMessage = messageEl.Text().Trim(); + throw new ExceptionWithConfigData(errorMessage, configData); + }); + + return IndexerConfigurationStatus.RequiresTesting; + } + + public async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + + var categoryMapping = MapTorznabCapsToTrackers(query).Distinct(); + string category = "0"; // Aka all + if (categoryMapping.Count() == 1) + { + category = categoryMapping.First(); + } + + + var episodeSearchUrl = string.Format(SearchUrl, category, HttpUtility.UrlEncode(query.GetQueryString())); + + var response = await RequestStringWithCookiesAndRetry(episodeSearchUrl); + + try + { + CQ dom = response.Content; + var rows = dom["table > tbody > tr"]; + foreach (var row in rows) + { + CQ qRow = row.Cq(); + var release = new ReleaseInfo(); + + release.MinimumRatio = 1; + release.MinimumSeedTime = 172800; + + var qLink = row.ChildElements.ElementAt(1).FirstElementChild.Cq(); + release.Title = qLink.Text().Trim(); + release.Comments = new Uri(qLink.Attr("href")); + release.Guid = release.Comments; + + var qDownload = row.ChildElements.ElementAt(3).FirstElementChild.Cq(); + release.Link = new Uri(qDownload.Attr("href")); + + var dateStr = row.ChildElements.ElementAt(5).Cq().Text().Trim(); + release.PublishDate = DateTimeUtil.FromTimeAgo(dateStr); + + var sizeStr = row.ChildElements.ElementAt(6).Cq().Text(); + release.Size = ReleaseInfo.GetBytes(sizeStr); + + release.Seeders = ParseUtil.CoerceInt(row.ChildElements.ElementAt(8).Cq().Text()); + release.Peers = ParseUtil.CoerceInt(row.ChildElements.ElementAt(9).Cq().Text()) + release.Seeders; + + var cat = row.Cq().Find("td:eq(0) i").First().Attr("class") + .Replace("gi gi-film", "1") + .Replace("gi gi-tv", "2") + .Replace("gi gi-music", "3") + .Replace("text-pink", string.Empty); + release.Category = MapTrackerCatToNewznab(cat.Trim()); + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(response.Content, ex); + } + return releases; + } + } +} \ No newline at end of file diff --git a/src/Jackett/Indexers/AlphaRatio.cs b/src/Jackett/Indexers/AlphaRatio.cs index f466a50a2..2b1e825f4 100644 --- a/src/Jackett/Indexers/AlphaRatio.cs +++ b/src/Jackett/Indexers/AlphaRatio.cs @@ -56,7 +56,7 @@ namespace Jackett.Indexers AddCategoryMapping(23, TorznabCatType.Audio); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { var incomingConfig = new ConfigurationDataBasicLogin(); incomingConfig.LoadValuesFromJson(configJson); @@ -76,6 +76,7 @@ namespace Jackett.Indexers var errorMessage = dom["#loginform"].Text().Trim().Replace("\n\t", " "); throw new ExceptionWithConfigData(errorMessage, (ConfigurationData)incomingConfig); }); + return IndexerConfigurationStatus.RequiresTesting; } void FillReleaseInfoFromJson(ReleaseInfo release, JObject r) diff --git a/src/Jackett/Indexers/AnimeBytes.cs b/src/Jackett/Indexers/AnimeBytes.cs index 71bc47b5a..6875324da 100644 --- a/src/Jackett/Indexers/AnimeBytes.cs +++ b/src/Jackett/Indexers/AnimeBytes.cs @@ -49,10 +49,10 @@ namespace Jackett.Indexers p: ps, configData: new ConfigurationDataAnimeBytes()) { - + } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -99,6 +99,8 @@ namespace Jackett.Indexers // Their login page appears to be broken and just gives a 500 error. throw new ExceptionWithConfigData("Failed to login, 6 failed attempts will get you banned for 6 hours.", configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } // Override to load legacy config format diff --git a/src/Jackett/Indexers/Avistaz.cs b/src/Jackett/Indexers/Avistaz.cs new file mode 100644 index 000000000..ea99df3a0 --- /dev/null +++ b/src/Jackett/Indexers/Avistaz.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jackett.Models; +using Newtonsoft.Json.Linq; +using NLog; +using Jackett.Utils; +using System.Net; +using System.Net.Http; +using CsQuery; +using System.Web; +using Jackett.Services; +using Jackett.Utils.Clients; +using System.Text.RegularExpressions; +using Jackett.Models.IndexerConfig; + +namespace Jackett.Indexers +{ + public class Avistaz : AvistazTracker, IIndexer + { + public Avistaz(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService) + : base(name: "Avistaz", + desc: "Aka AsiaTorrents", + link: "https://avistaz.to/", + indexerManager: indexerManager, + logger: logger, + protectionService: protectionService, + webClient: webClient + ) + { + } + } +} \ No newline at end of file diff --git a/src/Jackett/Indexers/BB.cs b/src/Jackett/Indexers/BB.cs index 8a657bfe9..0f6b30549 100644 --- a/src/Jackett/Indexers/BB.cs +++ b/src/Jackett/Indexers/BB.cs @@ -37,7 +37,7 @@ namespace Jackett.Indexers : base(name: "bB", description: "bB", link: "http://www.reddit.com/r/baconbits/", - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + caps: new TorznabCapabilities(), manager: i, client: w, logger: l, @@ -57,7 +57,7 @@ namespace Jackett.Indexers AddCategoryMapping(11, TorznabCatType.PCGames); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -80,8 +80,9 @@ namespace Jackett.Indexers } var message = string.Join(" ", messages); throw new ExceptionWithConfigData(message, configData); - }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/BakaBT.cs b/src/Jackett/Indexers/BakaBT.cs index 4112dd18a..5c89fd31c 100644 --- a/src/Jackett/Indexers/BakaBT.cs +++ b/src/Jackett/Indexers/BakaBT.cs @@ -41,7 +41,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -66,6 +66,8 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/BaseIndexer.cs b/src/Jackett/Indexers/BaseIndexer.cs index dd528f4b6..71cf91979 100644 --- a/src/Jackett/Indexers/BaseIndexer.cs +++ b/src/Jackett/Indexers/BaseIndexer.cs @@ -38,7 +38,7 @@ namespace Jackett.Indexers set { configData.CookieHeader.Value = value; } } - + protected ConfigurationData configData; @@ -70,7 +70,7 @@ namespace Jackett.Indexers { if (string.IsNullOrEmpty(downloadUrlBase)) return releases; - foreach(var release in releases) + foreach (var release in releases) { if (release.Link.ToString().StartsWith(downloadUrlBase)) { @@ -91,7 +91,7 @@ namespace Jackett.Indexers if (null != input) { input = input.ToLowerInvariant(); - var mapping = categoryMapping.Where(m => m.TrackerCategory == input).FirstOrDefault(); + var mapping = categoryMapping.Where(m => m.TrackerCategory.ToLowerInvariant() == input.ToLowerInvariant()).FirstOrDefault(); if (mapping != null) { return mapping.NewzNabCategory; @@ -127,7 +127,6 @@ namespace Jackett.Indexers var spacing = string.Join("", Enumerable.Repeat(Environment.NewLine, 5)); var fileContents = string.Format("{0}{1}{2}", ex, spacing, results); logger.Error(fileName + fileContents); - throw ex; } protected void CleanCache() @@ -403,12 +402,44 @@ namespace Jackett.Indexers categoryMapping.Add(new CategoryMapping(trackerCategory.ToString(), newznabCategory)); } - protected List MapTorznabCapsToTrackers(TorznabQuery query) + protected void AddMultiCategoryMapping(TorznabCategory newznabCategory, params int[] trackerCategories) + { + foreach (var trackerCat in trackerCategories) + { + categoryMapping.Add(new CategoryMapping(trackerCat.ToString(), newznabCategory.ID)); + } + } + + protected void AddMultiCategoryMapping(int trackerCategory, params TorznabCategory[] newznabCategories) + { + foreach (var newznabCat in newznabCategories) + { + categoryMapping.Add(new CategoryMapping(trackerCategory.ToString(), newznabCat.ID)); + } + } + + protected List MapTorznabCapsToTrackers(TorznabQuery query, bool mapChildrenCatsToParent = false) { var result = new List(); foreach (var cat in query.Categories) { - foreach (var mapping in categoryMapping.Where(c => c.NewzNabCategory == cat)) + var queryCats = new List { cat }; + var newznabCat = TorznabCatType.AllCats.FirstOrDefault(c => c.ID == cat); + if (newznabCat != null) + { + queryCats.AddRange(newznabCat.SubCategories.Select(c => c.ID)); + } + + if (mapChildrenCatsToParent) + { + var parentNewznabCat = TorznabCatType.AllCats.FirstOrDefault(c => c.SubCategories.Contains(newznabCat)); + if (parentNewznabCat != null) + { + queryCats.Add(parentNewznabCat.ID); + } + } + + foreach (var mapping in categoryMapping.Where(c => queryCats.Contains(c.NewzNabCategory))) { result.Add(mapping.TrackerCategory); } diff --git a/src/Jackett/Indexers/BeyondHD.cs b/src/Jackett/Indexers/BeyondHD.cs index 81f472c19..f0f5a635b 100644 --- a/src/Jackett/Indexers/BeyondHD.cs +++ b/src/Jackett/Indexers/BeyondHD.cs @@ -33,36 +33,52 @@ namespace Jackett.Indexers : base(name: "BeyondHD", description: "Without BeyondHD, your HDTV is just a TV", link: "https://beyondhd.me/", - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + caps: new TorznabCapabilities(), manager: i, client: w, logger: l, p: ps, configData: new ConfigurationDataCookie()) { - AddCategoryMapping("40,44,48,89,46,45", TorznabCatType.TV); - AddCategoryMapping("40", TorznabCatType.TVHD); - AddCategoryMapping("44", TorznabCatType.TVHD); - AddCategoryMapping("48", TorznabCatType.TVHD); - AddCategoryMapping("46", TorznabCatType.TVHD); - AddCategoryMapping("45", TorznabCatType.TVHD); - AddCategoryMapping("44", TorznabCatType.TVSD); - AddCategoryMapping("46", TorznabCatType.TVSD); - AddCategoryMapping("45", TorznabCatType.TVSD); + AddCategoryMapping(37, TorznabCatType.MoviesBluRay); // Movie / Blu-ray + AddMultiCategoryMapping(TorznabCatType.Movies3D, + 71, // Movie / 3D + 83 // FraMeSToR 3D + ); + AddMultiCategoryMapping(TorznabCatType.MoviesHD, + 77, // Movie / 1080p/i + 94, // Movie / 4K + 78, // Movie / 720p + 54, // Movie / MP4 + 17, // Movie / Remux + 50, // Internal / FraMeSToR 1080p + 75, // Internal / FraMeSToR 720p + 49, // Internal / FraMeSToR REMUX + 61, // Internal / HDX REMUX + 86 // Internal / SC4R + ); + + AddMultiCategoryMapping(TorznabCatType.TVHD, + 40, // TV Show / Blu-ray + 44, // TV Show / Encodes + 48, // TV Show / HDTV + 89, // TV Show / Packs + 46, // TV Show / Remux + 45 // TV Show / WEB-DL + ); + + AddCategoryMapping(36, TorznabCatType.AudioLossless); // Music / Lossless + AddCategoryMapping(69, TorznabCatType.AudioMP3); // Music / MP3 + AddMultiCategoryMapping(TorznabCatType.AudioVideo, + 55, // Music / 1080p/i + 56, // Music / 720p + 42 // Music / Blu-ray + ); - AddCategoryMapping("41,77,71,94,78,37,54,17", TorznabCatType.Movies); - AddCategoryMapping("77", TorznabCatType.MoviesHD); - AddCategoryMapping("71", TorznabCatType.Movies3D); - AddCategoryMapping("78", TorznabCatType.MoviesHD); - AddCategoryMapping("37", TorznabCatType.MoviesBluRay); - AddCategoryMapping("54", TorznabCatType.MoviesHD); - AddCategoryMapping("55,56,42,36,69", TorznabCatType.Audio); - AddCategoryMapping("36", TorznabCatType.AudioLossless); - AddCategoryMapping("69", TorznabCatType.AudioMP3); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -72,18 +88,19 @@ namespace Jackett.Indexers Cookies = configData.Cookie.Value }); - await ConfigureIfOK(CookieHeader, response.Content.Contains("logout.php"), () => + await ConfigureIfOK(configData.Cookie.Value, response.Content.Contains("logout.php"), () => { CQ dom = response.Content; throw new ExceptionWithConfigData("Invalid cookie header", configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) { List releases = new List(); - var searchString = query.GetQueryString(); + var searchString = query.GetQueryString(); var searchUrl = SearchUrl; var queryCollection = new NameValueCollection(); @@ -92,12 +109,7 @@ namespace Jackett.Indexers queryCollection.Add("search", searchString); } - var cats = new List(); foreach (var cat in MapTorznabCapsToTrackers(query)) - { - cats.AddRange(cat.Split(',')); - } - foreach (var cat in cats.Distinct()) { queryCollection.Add("c" + cat, "1"); } diff --git a/src/Jackett/Indexers/BitHdtv.cs b/src/Jackett/Indexers/BitHdtv.cs index b01f3f37a..bdead64ec 100644 --- a/src/Jackett/Indexers/BitHdtv.cs +++ b/src/Jackett/Indexers/BitHdtv.cs @@ -15,13 +15,14 @@ using System.Text; using System.Threading.Tasks; using System.Web; using Jackett.Models.IndexerConfig; +using System.Collections.Specialized; namespace Jackett.Indexers { public class BitHdtv : BaseIndexer, IIndexer { private string LoginUrl { get { return SiteLink + "takelogin.php"; } } - private string SearchUrl { get { return SiteLink + "torrents.php?cat=0&search="; } } + private string SearchUrl { get { return SiteLink + "torrents.php?"; } } private string DownloadUrl { get { return SiteLink + "download.php?/{0}/dl.torrent"; } } new ConfigurationDataBasicLogin configData @@ -34,16 +35,26 @@ namespace Jackett.Indexers : base(name: "BIT-HDTV", description: "Home of high definition invites", link: "https://www.bit-hdtv.com/", - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + caps: new TorznabCapabilities(), manager: i, client: w, logger: l, p: ps, configData: new ConfigurationDataBasicLogin()) { + AddCategoryMapping(1, TorznabCatType.TVAnime); // Anime + AddCategoryMapping(2, TorznabCatType.MoviesBluRay); // Blu-ray + AddCategoryMapping(4, TorznabCatType.TVDocumentary); // Documentaries + AddCategoryMapping(6, TorznabCatType.AudioLossless); // HQ Audio + AddCategoryMapping(7, TorznabCatType.Movies); // Movies + AddCategoryMapping(8, TorznabCatType.AudioVideo); // Music Videos + AddCategoryMapping(5, TorznabCatType.TVSport); // Sports + AddCategoryMapping(10, TorznabCatType.TV); // TV + AddCategoryMapping(12, TorznabCatType.TV); // TV/Seasonpack + AddCategoryMapping(11, TorznabCatType.XXX); // XXX } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -62,13 +73,25 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) { var releases = new List(); - var episodeSearchUrl = SearchUrl + HttpUtility.UrlEncode(query.GetQueryString()); - var results = await RequestStringWithCookiesAndRetry(episodeSearchUrl); + var searchString = query.GetQueryString(); + var queryCollection = new NameValueCollection(); + + if (!string.IsNullOrWhiteSpace(searchString)) + { + queryCollection.Add("search", searchString); + } + + var searchUrl = SearchUrl + queryCollection.GetQueryString(); + + var trackerCats = MapTorznabCapsToTrackers(query, mapChildrenCatsToParent: true); + + var results = await RequestStringWithCookiesAndRetry(searchUrl); try { CQ dom = results.Content; @@ -86,10 +109,18 @@ namespace Jackett.Indexers release.MinimumSeedTime = 172800; release.Title = qLink.Attr("title"); release.Description = release.Title; - release.Guid = new Uri(SiteLink + qLink.Attr("href")); + release.Guid = new Uri(SiteLink + qLink.Attr("href").TrimStart('/')); release.Comments = release.Guid; release.Link = new Uri(string.Format(DownloadUrl, qLink.Attr("href").Split('=')[1])); + var catUrl = qRow.Children().ElementAt(1).FirstElementChild.Cq().Attr("href"); + var catNum = catUrl.Split(new char[] { '=', '&' })[1]; + release.Category = MapTrackerCatToNewznab(catNum); + + // This tracker cannot search multiple cats at a time, so search all cats then filter out results from different cats + if (trackerCats.Count > 0 && !trackerCats.Contains(catNum)) + continue; + var dateString = qRow.Children().ElementAt(5).Cq().Text().Trim(); var pubDate = DateTime.ParseExact(dateString, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); release.PublishDate = DateTime.SpecifyKind(pubDate, DateTimeKind.Local); diff --git a/src/Jackett/Indexers/BitMeTV.cs b/src/Jackett/Indexers/BitMeTV.cs index 8be342237..bc117d070 100644 --- a/src/Jackett/Indexers/BitMeTV.cs +++ b/src/Jackett/Indexers/BitMeTV.cs @@ -58,7 +58,7 @@ namespace Jackett.Indexers return configData; } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -80,6 +80,7 @@ namespace Jackett.Indexers configData.CaptchaCookie.Value = captchaImage.Cookies; throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/Demonoid.cs b/src/Jackett/Indexers/Demonoid.cs index c99fcbcbe..53400d26f 100644 --- a/src/Jackett/Indexers/Demonoid.cs +++ b/src/Jackett/Indexers/Demonoid.cs @@ -40,7 +40,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -58,6 +58,7 @@ namespace Jackett.Indexers var errorMessage = dom[".red"].ElementAt(1).Cq().Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/EuTorrents.cs b/src/Jackett/Indexers/EuTorrents.cs new file mode 100644 index 000000000..af0affe69 --- /dev/null +++ b/src/Jackett/Indexers/EuTorrents.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jackett.Models; +using Newtonsoft.Json.Linq; +using NLog; +using Jackett.Utils; +using System.Net; +using System.Net.Http; +using CsQuery; +using System.Web; +using Jackett.Services; +using Jackett.Utils.Clients; +using System.Text.RegularExpressions; +using Jackett.Models.IndexerConfig; + +namespace Jackett.Indexers +{ + public class EuTorrents : AvistazTracker, IIndexer + { + public EuTorrents(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService) + : base(name: "EuTorrents", + desc: "Part of the Avistaz network.", + link: "https://eutorrents.to/", + indexerManager: indexerManager, + logger: logger, + protectionService: protectionService, + webClient: webClient + ) + { + } + } +} \ No newline at end of file diff --git a/src/Jackett/Indexers/FileList.cs b/src/Jackett/Indexers/FileList.cs index 0b03887f5..9ca4e219f 100644 --- a/src/Jackett/Indexers/FileList.cs +++ b/src/Jackett/Indexers/FileList.cs @@ -66,7 +66,7 @@ namespace Jackett.Indexers AddCategoryMapping(7, TorznabCatType.XXX); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -81,6 +81,7 @@ namespace Jackett.Indexers var errorMessage = dom[".main"].Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) @@ -99,7 +100,7 @@ namespace Jackett.Indexers if (!string.IsNullOrWhiteSpace(searchString) || cat != "0") searchUrl += string.Format("?search={0}&cat={1}&searchin=0&sort=0", HttpUtility.UrlEncode(searchString), cat); - + var response = await RequestStringWithCookiesAndRetry(searchUrl, null, BrowseUrl); var results = response.Content; diff --git a/src/Jackett/Indexers/FrenchTorrentDb.cs b/src/Jackett/Indexers/FrenchTorrentDb.cs index 00d1e835a..da7538d93 100644 --- a/src/Jackett/Indexers/FrenchTorrentDb.cs +++ b/src/Jackett/Indexers/FrenchTorrentDb.cs @@ -37,7 +37,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var response = await webclient.GetString(new Utils.Clients.WebRequest() @@ -51,6 +51,7 @@ namespace Jackett.Indexers { throw new ExceptionWithConfigData("Failed to login", configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/Freshon.cs b/src/Jackett/Indexers/Freshon.cs index 0b298543b..62a4ee61f 100644 --- a/src/Jackett/Indexers/Freshon.cs +++ b/src/Jackett/Indexers/Freshon.cs @@ -46,7 +46,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -65,6 +65,7 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/HDSpace.cs b/src/Jackett/Indexers/HDSpace.cs index 0b455b78f..cfb4f57ee 100644 --- a/src/Jackett/Indexers/HDSpace.cs +++ b/src/Jackett/Indexers/HDSpace.cs @@ -41,7 +41,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -63,6 +63,7 @@ namespace Jackett.Indexers var errorMessage = string.Format(errorStr, attempts); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/HDTorrents.cs b/src/Jackett/Indexers/HDTorrents.cs index c4fa18507..294eafda4 100644 --- a/src/Jackett/Indexers/HDTorrents.cs +++ b/src/Jackett/Indexers/HDTorrents.cs @@ -15,12 +15,13 @@ using System.Text; using System.Threading.Tasks; using System.Web; using Jackett.Models.IndexerConfig; +using System.Collections.Specialized; namespace Jackett.Indexers { public class HDTorrents : BaseIndexer, IIndexer { - private string SearchUrl { get { return SiteLink + "torrents.php?search={0}&active=1&options=0&category%5B%5D=59&category%5B%5D=60&category%5B%5D=30&category%5B%5D=38&page=0"; } } + private string SearchUrl { get { return SiteLink + "torrents.php?"; } } private string LoginUrl { get { return SiteLink + "login.php"; } } private const int MAXPAGES = 3; @@ -34,16 +35,37 @@ namespace Jackett.Indexers : base(name: "HD-Torrents", description: "HD-Torrents is a private torrent website with HD torrents and strict rules on their content.", link: "http://hdts.ru/",// Of the accessible domains the .ru seems the most reliable. https://hdts.ru | https://hd-torrents.org | https://hd-torrents.net | https://hd-torrents.me - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), manager: i, client: w, logger: l, p: ps, configData: new ConfigurationDataBasicLogin()) { + TorznabCaps.Categories.Clear(); + + AddCategoryMapping("1", TorznabCatType.MoviesHD);// Movie/Blu-Ray + AddCategoryMapping("2", TorznabCatType.MoviesHD);// Movie/Remux + AddCategoryMapping("5", TorznabCatType.MoviesHD);//Movie/1080p/i + AddCategoryMapping("3", TorznabCatType.MoviesHD);//Movie/720p + AddCategoryMapping("63", TorznabCatType.Audio);//Movie/Audio Track + + AddCategoryMapping("59", TorznabCatType.TVHD);//TV Show/Blu-ray + AddCategoryMapping("60", TorznabCatType.TVHD);//TV Show/Remux + AddCategoryMapping("30", TorznabCatType.TVHD);//TV Show/1080p/i + AddCategoryMapping("38", TorznabCatType.TVHD);//TV Show/720p + + AddCategoryMapping("44", TorznabCatType.Audio);//Music/Album + AddCategoryMapping("61", TorznabCatType.AudioVideo);//Music/Blu-Ray + AddCategoryMapping("62", TorznabCatType.AudioVideo);//Music/Remux + AddCategoryMapping("57", TorznabCatType.AudioVideo);//Music/1080p/i + AddCategoryMapping("45", TorznabCatType.AudioVideo);//Music/720p + + AddCategoryMapping("58", TorznabCatType.XXX);//XXX/Blu-ray + AddCategoryMapping("48", TorznabCatType.XXX);//XXX/1080p/i + AddCategoryMapping("47", TorznabCatType.XXX);//XXX/720p } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var loginPage = await RequestStringWithCookies(LoginUrl, string.Empty); @@ -60,13 +82,37 @@ namespace Jackett.Indexers var errorMessage = "Couldn't login"; throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) { var releases = new List(); var searchurls = new List(); - var searchUrl = string.Format(SearchUrl, HttpUtility.UrlEncode(query.GetQueryString())); + var searchUrl = SearchUrl;// string.Format(SearchUrl, HttpUtility.UrlEncode())); + var queryCollection = new NameValueCollection(); + var searchString = query.GetQueryString(); + + + foreach (var cat in MapTorznabCapsToTrackers(query)) + { + searchUrl += "category%5B%5D=" + cat + "&"; + } + + + if (!string.IsNullOrWhiteSpace(searchString)) + { + queryCollection.Add("search", searchString); + } + + + + queryCollection.Add("active", "1"); + queryCollection.Add("options", "0"); + + searchUrl += queryCollection.GetQueryString(); + + var results = await RequestStringWithCookiesAndRetry(searchUrl); try { @@ -117,14 +163,17 @@ namespace Jackett.Indexers string fullSize = qRow.Find("td.mainblockcontent").Get(6).InnerText; release.Size = ReleaseInfo.GetBytes(fullSize); - release.Guid = new Uri(SiteLink + "/" + qRow.Find("td.mainblockcontent b a").Attr("href")); - release.Link = new Uri(SiteLink + "/" + qRow.Find("td.mainblockcontent").Get(3).FirstChild.GetAttribute("href")); - release.Comments = new Uri(SiteLink + "/" + qRow.Find("td.mainblockcontent b a").Attr("href") + "#comments"); + release.Guid = new Uri(SiteLink + qRow.Find("td.mainblockcontent b a").Attr("href")); + release.Link = new Uri(SiteLink + qRow.Find("td.mainblockcontent").Get(3).FirstChild.GetAttribute("href")); + release.Comments = new Uri(SiteLink + qRow.Find("td.mainblockcontent b a").Attr("href") + "#comments"); string[] dateSplit = qRow.Find("td.mainblockcontent").Get(5).InnerHTML.Split(','); string dateString = dateSplit[1].Substring(0, dateSplit[1].IndexOf('>')); release.PublishDate = DateTime.Parse(dateString, CultureInfo.InvariantCulture); + string category = qRow.Find("td:eq(0) a").Attr("href").Replace("torrents.php?category=", ""); + release.Category = MapTrackerCatToNewznab(category); + releases.Add(release); } } diff --git a/src/Jackett/Indexers/IIndexer.cs b/src/Jackett/Indexers/IIndexer.cs index e3dd527dd..c73588910 100644 --- a/src/Jackett/Indexers/IIndexer.cs +++ b/src/Jackett/Indexers/IIndexer.cs @@ -27,7 +27,7 @@ namespace Jackett.Indexers Task GetConfigurationForSetup(); // Called when web API wants to apply setup configuration via web API, usually this is where login and storing cookie happens - Task ApplyConfiguration(JToken configJson); + Task ApplyConfiguration(JToken configJson); // Called on startup when initializing indexers from saved configuration void LoadFromSavedConfiguration(JToken jsonConfig); diff --git a/src/Jackett/Indexers/IPTorrents.cs b/src/Jackett/Indexers/IPTorrents.cs index ebbef2c7d..705576168 100644 --- a/src/Jackett/Indexers/IPTorrents.cs +++ b/src/Jackett/Indexers/IPTorrents.cs @@ -83,7 +83,7 @@ namespace Jackett.Indexers AddCategoryMapping(94, TorznabCatType.BooksComics); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -109,6 +109,7 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/ImmortalSeed.cs b/src/Jackett/Indexers/ImmortalSeed.cs index e246b03b8..0bb675bf4 100644 --- a/src/Jackett/Indexers/ImmortalSeed.cs +++ b/src/Jackett/Indexers/ImmortalSeed.cs @@ -67,7 +67,7 @@ namespace Jackett.Indexers } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -93,6 +93,8 @@ namespace Jackett.Indexers var errorMessage = "Incorrect username or password! " + tries + " tries remaining."; throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/MoreThanTV.cs b/src/Jackett/Indexers/MoreThanTV.cs index 8a6111516..e8a809273 100644 --- a/src/Jackett/Indexers/MoreThanTV.cs +++ b/src/Jackett/Indexers/MoreThanTV.cs @@ -45,7 +45,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -63,6 +63,8 @@ namespace Jackett.Indexers var errorMessage = dom["#loginform"].Text().Trim().Replace("\n\t", " "); throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } private void FillReleaseInfoFromJson(ReleaseInfo release, JObject r) diff --git a/src/Jackett/Indexers/Pretome.cs b/src/Jackett/Indexers/Pretome.cs index b24cfc60d..9cd8c3c15 100644 --- a/src/Jackett/Indexers/Pretome.cs +++ b/src/Jackett/Indexers/Pretome.cs @@ -12,6 +12,7 @@ using Jackett.Utils; using CsQuery; using System.Web; using Jackett.Models.IndexerConfig; +using System.Collections.Specialized; namespace Jackett.Indexers { @@ -19,7 +20,9 @@ namespace Jackett.Indexers { private string LoginUrl { get { return SiteLink + "takelogin.php"; } } private string LoginReferer { get { return SiteLink + "index.php?cat=1"; } } - private string SearchUrl { get { return SiteLink + "browse.php?tags=&st=1&tf=all&cat%5B%5D=7&search={0}"; } } + private string SearchUrl { get { return SiteLink + "browse.php"; } } + + private List resultMapping = new List(); new ConfigurationDataPinNumber configData { @@ -38,9 +41,135 @@ namespace Jackett.Indexers p: ps, configData: new ConfigurationDataPinNumber()) { + + AddCategoryMapping("cat[]=22&tags=Windows", TorznabCatType.PC0day); + AddCategoryMapping("cat[]=22&tags=MAC", TorznabCatType.PCMac); + AddCategoryMapping("cat[]=22&tags=Linux", TorznabCatType.PC); + + AddCategoryMapping("cat[]=27", TorznabCatType.BooksEbook); + + AddCategoryMapping("cat[]=4&tags=PC", TorznabCatType.PCGames); + AddCategoryMapping("cat[]=4&tags=RIP", TorznabCatType.PCGames); + AddCategoryMapping("cat[]=4&tags=ISO", TorznabCatType.PCGames); + AddCategoryMapping("cat[]=4&tags=XBOX360", TorznabCatType.ConsoleXbox360); + AddCategoryMapping("cat[]=4&tags=PS3", TorznabCatType.ConsolePS3); + AddCategoryMapping("cat[]=4&tags=Wii", TorznabCatType.ConsoleWii); + AddCategoryMapping("cat[]=4&tags=PSP", TorznabCatType.ConsolePSP); + AddCategoryMapping("cat[]=4&tags=NSD", TorznabCatType.ConsoleNDS); + AddCategoryMapping("cat[]=4&tags=XBox", TorznabCatType.ConsoleXbox); + AddCategoryMapping("cat[]=4&tags=PS2", TorznabCatType.ConsoleOther); + + AddCategoryMapping("cat[]=31&tags=Ebook", TorznabCatType.BooksEbook); + AddCategoryMapping("cat[]=31&tags=RARFiX", TorznabCatType.Other); + + AddCategoryMapping("cat[]=19&tags=x264", TorznabCatType.Movies); + AddCategoryMapping("cat[]=19&tags=720p", TorznabCatType.MoviesHD); + AddCategoryMapping("cat[]=19&tags=XviD", TorznabCatType.MoviesSD); + AddCategoryMapping("cat[]=19&tags=BluRay", TorznabCatType.MoviesHD); + AddCategoryMapping("cat[]=19&tags=DVDRiP", TorznabCatType.MoviesSD); + AddCategoryMapping("cat[]=19&tags=1080p", TorznabCatType.MoviesHD); + AddCategoryMapping("cat[]=19&tags=DVD", TorznabCatType.MoviesSD); + AddCategoryMapping("cat[]=19&tags=DVDR", TorznabCatType.MoviesSD); + AddCategoryMapping("cat[]=19&tags=WMV", TorznabCatType.Movies); + AddCategoryMapping("cat[]=19&tags=CAM", TorznabCatType.Movies); + + AddCategoryMapping("cat[]=6&tags=MP3", TorznabCatType.AudioMP3); + AddCategoryMapping("cat[]=6&tags=V2", TorznabCatType.AudioMP3); + AddCategoryMapping("cat[]=6&tags=FLAC", TorznabCatType.AudioLossless); + AddCategoryMapping("cat[]=6&tags=320kbps", TorznabCatType.AudioMP3); + + AddCategoryMapping("cat[]=7&tags=x264", TorznabCatType.TVHD); + AddCategoryMapping("cat[]=7&tags=720p", TorznabCatType.TVHD); + AddCategoryMapping("cat[]=7&tags=HDTV", TorznabCatType.TVHD); + AddCategoryMapping("cat[]=7&tags=XviD", TorznabCatType.TVSD); + AddCategoryMapping("cat[]=7&BluRay", TorznabCatType.TVHD); + AddCategoryMapping("cat[]=7&tags=DVDRip", TorznabCatType.TVSD); + AddCategoryMapping("cat[]=7&tags=DVD", TorznabCatType.TVSD); + AddCategoryMapping("cat[]=7&tags=Documentary", TorznabCatType.TVDocumentary); + AddCategoryMapping("cat[]=7&tags=PDTV", TorznabCatType.TVSD); + AddCategoryMapping("cat[]=7&tags=HD-DVD", TorznabCatType.TVSD); + + + AddCategoryMapping("cat[]=51&tags=XviD", TorznabCatType.XXXXviD); + AddCategoryMapping("cat[]=51&tags=DVDRiP", TorznabCatType.XXXDVD); + + // Unfortunately they are tags not categories so return the results + // as the parent category so do not get results removed with the filtering. + + AddResultCategoryMapping("cat[]=22&tags=Windows", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=22&tags=MAC", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=22&tags=Linux", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=22&tags=All", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=22", TorznabCatType.PC); + + AddResultCategoryMapping("cat[]=27&tags=All", TorznabCatType.Books); + AddResultCategoryMapping("cat[]=27", TorznabCatType.Books); + + AddResultCategoryMapping("cat[]=4&tags=PC", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=4&tags=RIP", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=4&tags=ISO", TorznabCatType.PC); + AddResultCategoryMapping("cat[]=4&tags=XBOX360", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=PS3", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=Wii", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=PSP", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=NSD", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=XBox", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=PS2", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4&tags=All", TorznabCatType.Console); + AddResultCategoryMapping("cat[]=4", TorznabCatType.Console); + + AddResultCategoryMapping("cat[]=31&tags=Ebook", TorznabCatType.Books); + AddResultCategoryMapping("cat[]=31&tags=RARFiX", TorznabCatType.Other); + AddResultCategoryMapping("cat[]=31&tags=All", TorznabCatType.Other); + AddResultCategoryMapping("cat[]=31", TorznabCatType.Other); + + AddResultCategoryMapping("cat[]=19&tags=x264", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=720p", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=XviD", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=BluRay", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=DVDRiP", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=1080p", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=DVD", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=DVDR", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=WMV", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=CAM", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19&tags=All", TorznabCatType.Movies); + AddResultCategoryMapping("cat[]=19", TorznabCatType.Movies); + + AddResultCategoryMapping("cat[]=6&tags=MP3", TorznabCatType.Audio); + AddResultCategoryMapping("cat[]=6&tags=V2", TorznabCatType.Audio); + AddResultCategoryMapping("cat[]=6&tags=FLAC", TorznabCatType.Audio); + AddResultCategoryMapping("cat[]=6&tags=320kbps", TorznabCatType.Audio); + AddResultCategoryMapping("cat[]=6&tags=All", TorznabCatType.Audio); + AddResultCategoryMapping("cat[]=6", TorznabCatType.Audio); + + AddResultCategoryMapping("cat[]=7&tags=x264", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=720p", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=HDTV", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=XviD", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&BluRay", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=DVDRip", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=DVD", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=Documentary", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=PDTV", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=HD-DVD", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7&tags=All", TorznabCatType.TV); + AddResultCategoryMapping("cat[]=7", TorznabCatType.TV); + + AddResultCategoryMapping("cat[]=51&tags=XviD", TorznabCatType.XXX); + AddResultCategoryMapping("cat[]=51&tags=DVDRiP", TorznabCatType.XXX); + AddResultCategoryMapping("cat[]=51&tags=All", TorznabCatType.XXX); + AddResultCategoryMapping("cat[]=51", TorznabCatType.XXX); } - public async Task ApplyConfiguration(JToken configJson) + protected void AddResultCategoryMapping(string trackerCategory, TorznabCategory newznabCategory) + { + resultMapping.Add(new CategoryMapping(trackerCategory.ToString(), newznabCategory.ID)); + if (!TorznabCaps.Categories.Contains(newznabCategory)) + TorznabCaps.Categories.Add(newznabCategory); + } + + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -69,13 +198,67 @@ namespace Jackett.Indexers CookieHeader = string.Empty; throw new ExceptionWithConfigData("Failed", configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) { var releases = new List(); - var episodeSearchUrl = string.Format(SearchUrl, HttpUtility.UrlEncode(query.GetQueryString())); - var response = await RequestStringWithCookiesAndRetry(episodeSearchUrl); + var queryUrl = SearchUrl; + var queryCollection = new NameValueCollection(); + var cats = MapTorznabCapsToTrackers(query); + var tags = string.Empty; + var catGroups = new List(); + foreach (var cat in cats) + { + //"cat[]=7&tags=x264" + var cSplit = cat.Split('&'); + if (cSplit.Length > 0) + { + var gsplit = cSplit[0].Split('='); + if (gsplit.Length > 1) + { + catGroups.Add(gsplit[1]); + } + } + + if (cSplit.Length > 1) + { + var gsplit = cSplit[1].Split('='); + if (gsplit.Length > 1) + { + if (tags != string.Empty) + tags += ","; + tags += gsplit[1]; + } + } + } + + if (catGroups.Distinct().Count() == 1) + { + queryCollection.Add("cat[]", catGroups.First()); + } + + if (!string.IsNullOrWhiteSpace(query.GetQueryString())) + { + queryCollection.Add("st", "1"); + queryCollection.Add("search", query.GetQueryString()); + } + + // Do not include too many tags as it'll mess with their servers. + if (tags.Split(',').Length < 7) + { + queryCollection.Add("tags", tags); + queryCollection.Add("tf", "any"); + } + + if (queryCollection.Count > 0) + { + queryUrl += "?" + queryCollection.GetQueryString(); + } + + var response = await RequestStringWithCookiesAndRetry(queryUrl); try { @@ -111,6 +294,9 @@ namespace Jackett.Indexers release.Seeders = ParseUtil.CoerceInt(row.ChildElements.ElementAt(9).InnerText); release.Peers = ParseUtil.CoerceInt(row.ChildElements.ElementAt(10).InnerText) + release.Seeders; + var cat = row.ChildElements.ElementAt(0).ChildElements.ElementAt(0).GetAttribute("href").Replace("browse.php?", string.Empty); + release.Category = MapTrackerResultCatToNewznab(cat); + releases.Add(release); } } @@ -120,5 +306,19 @@ namespace Jackett.Indexers } return releases; } + + protected int MapTrackerResultCatToNewznab(string input) + { + if (null != input) + { + input = input.ToLowerInvariant(); + var mapping = resultMapping.Where(m => m.TrackerCategory.ToLowerInvariant() == input.ToLowerInvariant()).FirstOrDefault(); + if (mapping != null) + { + return mapping.NewzNabCategory; + } + } + return 0; + } } } diff --git a/src/Jackett/Indexers/PrivateHD.cs b/src/Jackett/Indexers/PrivateHD.cs index 99009c507..6bb387ca1 100644 --- a/src/Jackett/Indexers/PrivateHD.cs +++ b/src/Jackett/Indexers/PrivateHD.cs @@ -18,116 +18,18 @@ using Jackett.Models.IndexerConfig; namespace Jackett.Indexers { - public class PrivateHD : BaseIndexer, IIndexer + public class PrivateHD : AvistazTracker, IIndexer { - private string LoginUrl { get { return SiteLink + "auth/login"; } } - private string SearchUrl { get { return SiteLink + "torrents?in=1&type={0}&search={1}"; } } - - new ConfigurationDataBasicLogin configData - { - get { return (ConfigurationDataBasicLogin)base.configData; } - set { base.configData = value; } - } - - public PrivateHD(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps) + public PrivateHD(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService) : base(name: "PrivateHD", - description: "BitTorrent site for High Quality, High Definition (HD) movies and TV Shows", + desc: "BitTorrent site for High Quality, High Definition (HD) movies and TV Shows", link: "https://privatehd.to/", - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), - manager: i, - client: wc, - logger: l, - p: ps, - configData: new ConfigurationDataBasicLogin()) + indexerManager: indexerManager, + logger: logger, + protectionService: protectionService, + webClient: webClient + ) { - AddCategoryMapping(1, TorznabCatType.Movies); - AddCategoryMapping(1, TorznabCatType.MoviesForeign); - AddCategoryMapping(1, TorznabCatType.MoviesHD); - AddCategoryMapping(1, TorznabCatType.MoviesSD); - AddCategoryMapping(2, TorznabCatType.TV); - AddCategoryMapping(3, TorznabCatType.Audio); - } - - public async Task ApplyConfiguration(JToken configJson) - { - configData.LoadValuesFromJson(configJson); - var loginPage = await RequestStringWithCookies(LoginUrl, string.Empty); - var token = new Regex("Avz.CSRF_TOKEN = '(.*?)';").Match(loginPage.Content).Groups[1].ToString(); - var pairs = new Dictionary { - { "_token", token }, - { "username_email", configData.Username.Value }, - { "password", configData.Password.Value }, - { "remember", "on" } - }; - - var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, loginPage.Cookies, true, null, LoginUrl); - await ConfigureIfOK(result.Cookies, result.Content != null && result.Content.Contains("auth/logout"), () => - { - CQ dom = result.Content; - var messageEl = dom[".form-error"]; - var errorMessage = messageEl.Text().Trim(); - throw new ExceptionWithConfigData(errorMessage, configData); - }); - } - - public async Task> PerformQuery(TorznabQuery query) - { - var releases = new List(); - - var categoryMapping = MapTorznabCapsToTrackers(query).Distinct(); - string category = "0"; // Aka all - if (categoryMapping.Count() == 1) - { - category = categoryMapping.First(); - } - - - var episodeSearchUrl = string.Format(SearchUrl, category, HttpUtility.UrlEncode(query.GetQueryString())); - - var response = await RequestStringWithCookiesAndRetry(episodeSearchUrl); - - try - { - CQ dom = response.Content; - var rows = dom["table > tbody > tr"]; - foreach (var row in rows) - { - CQ qRow = row.Cq(); - var release = new ReleaseInfo(); - - release.MinimumRatio = 1; - release.MinimumSeedTime = 172800; - - var qLink = row.ChildElements.ElementAt(1).FirstElementChild.Cq(); - release.Title = qLink.Text().Trim(); - release.Comments = new Uri(qLink.Attr("href")); - release.Guid = release.Comments; - - var qDownload = row.ChildElements.ElementAt(3).FirstElementChild.Cq(); - release.Link = new Uri(qDownload.Attr("href")); - - var dateStr = row.ChildElements.ElementAt(5).Cq().Text().Trim(); - release.PublishDate = DateTimeUtil.FromTimeAgo(dateStr); - - var sizeStr = row.ChildElements.ElementAt(6).Cq().Text(); - release.Size = ReleaseInfo.GetBytes(sizeStr); - - release.Seeders = ParseUtil.CoerceInt(row.ChildElements.ElementAt(8).Cq().Text()); - release.Peers = ParseUtil.CoerceInt(row.ChildElements.ElementAt(9).Cq().Text()) + release.Seeders; - - var cat = row.Cq().Find("td:eq(0) i").First().Attr("class") - .Replace("gi gi-film", "1") - .Replace("gi gi-tv", "2") - .Replace("gi gi-music", "3"); - release.Category = MapTrackerCatToNewznab(cat); - releases.Add(release); - } - } - catch (Exception ex) - { - OnParseError(response.Content, ex); - } - return releases; } } } diff --git a/src/Jackett/Indexers/RUTor.cs b/src/Jackett/Indexers/RUTor.cs index 128847a52..91b136437 100644 --- a/src/Jackett/Indexers/RUTor.cs +++ b/src/Jackett/Indexers/RUTor.cs @@ -49,7 +49,7 @@ namespace Jackett.Indexers TorznabCaps.Categories.Add(TorznabCatType.Books); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var oldConfig = configData; @@ -60,6 +60,8 @@ namespace Jackett.Indexers configData = oldConfig; throw new Exception("Could not find releases from this URL"); }); + + return IndexerConfigurationStatus.RequiresTesting; } diff --git a/src/Jackett/Indexers/Rarbg.cs b/src/Jackett/Indexers/Rarbg.cs new file mode 100644 index 000000000..5e35e85bb --- /dev/null +++ b/src/Jackett/Indexers/Rarbg.cs @@ -0,0 +1,187 @@ +using Jackett.Models; +using Jackett.Models.IndexerConfig; +using Jackett.Services; +using Jackett.Utils.Clients; +using Newtonsoft.Json.Linq; +using NLog; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using System.Web; + +namespace Jackett.Indexers +{ + public class Rarbg : BaseIndexer, IIndexer + { + readonly static string defaultSiteLink = "https://torrentapi.org/"; + + private Uri BaseUri + { + get { return new Uri(configData.Url.Value); } + set { configData.Url.Value = value.ToString(); } + } + + private string ApiEndpoint { get { return BaseUri + "pubapi_v2.php"; } } + private string TokenUrl { get { return ApiEndpoint + "?get_token=get_token"; } } + private string SearchUrl { get { return ApiEndpoint + "?app_id=jackett_v{0}&mode={1}&format=json_extended&search_string={2}&token={3}"; } } + + + new ConfigurationDataUrl configData + { + get { return (ConfigurationDataUrl)base.configData; } + set { base.configData = value; } + } + + private DateTime lastTokenFetch; + private string token; + + readonly TimeSpan TOKEN_DURATION = TimeSpan.FromMinutes(10); + + private bool HasValidToken { get { return !string.IsNullOrEmpty(token) && lastTokenFetch > DateTime.Now - TOKEN_DURATION; } } + + Dictionary categoryLabels; + + public Rarbg(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps) + : base(name: "RARBG", + description: "RARBG", + link: defaultSiteLink, + caps: new TorznabCapabilities(), + manager: i, + client: wc, + logger: l, + p: ps, + configData: new ConfigurationDataUrl(defaultSiteLink)) + { + categoryLabels = new Dictionary(); + + AddCat(4, TorznabCatType.XXX, "XXX (18+)"); + AddCat(14, TorznabCatType.MoviesSD, "Movies/XVID"); + AddCat(48, TorznabCatType.MoviesHD, "Movies/XVID/720"); + AddCat(17, TorznabCatType.MoviesSD, "Movies/x264"); + AddCat(44, TorznabCatType.MoviesHD, "Movies/x264/1080"); + AddCat(45, TorznabCatType.MoviesHD, "Movies/x264/720"); + AddCat(47, TorznabCatType.Movies3D, "Movies/x264/3D"); + AddCat(42, TorznabCatType.MoviesBluRay, "Movies/Full BD"); + AddCat(46, TorznabCatType.MoviesBluRay, "Movies/BD Remux"); + AddCat(18, TorznabCatType.TVSD, "TV Episodes"); + AddCat(41, TorznabCatType.TVHD, "TV HD Episodes"); + AddCat(23, TorznabCatType.AudioMP3, "Music/MP3"); + AddCat(25, TorznabCatType.AudioLossless, "Music/FLAC"); + AddCat(27, TorznabCatType.PCGames, "Games/PC ISO"); + AddCat(28, TorznabCatType.PCGames, "Games/PC RIP"); + AddCat(40, TorznabCatType.ConsolePS3, "Games/PS3"); + AddCat(32, TorznabCatType.ConsoleXbox360, "Games/XBOX-360"); + AddCat(33, TorznabCatType.PCISO, "Software/PC ISO"); + AddCat(35, TorznabCatType.BooksEbook, "e-Books"); + } + + void AddCat(int cat, TorznabCategory catType, string label) + { + AddCategoryMapping(cat, catType); + categoryLabels.Add(label, cat); + } + + async Task CheckToken() + { + if (!HasValidToken) + { + var result = await RequestStringWithCookiesAndRetry(TokenUrl); + var json = JObject.Parse(result.Content); + token = json.Value("token"); + lastTokenFetch = DateTime.Now; + } + } + + public async Task ApplyConfiguration(JToken configJson) + { + configData.LoadValuesFromJson(configJson); + var releases = await PerformQuery(new TorznabQuery()); + + await ConfigureIfOK(string.Empty, releases.Count() > 0, () => + { + throw new Exception("Could not find releases from this URL"); + }); + + return IndexerConfigurationStatus.Completed; + } + + public Task> PerformQuery(TorznabQuery query) + { + return PerformQuery(query, 0); + } + + public async Task> PerformQuery(TorznabQuery query, int attempts = 0) + { + await CheckToken(); + var releases = new List(); + var queryStr = HttpUtility.UrlEncode(query.GetQueryString()); + + var mode = string.IsNullOrEmpty(queryStr) ? "list" : "search"; + var episodeSearchUrl = string.Format(SearchUrl, Engine.ConfigService.GetVersion(), mode, queryStr, token); + var cats = string.Join(";", MapTorznabCapsToTrackers(query)); + if (!string.IsNullOrEmpty(cats)) + { + episodeSearchUrl += "&category=" + cats; + } + + var response = await RequestStringWithCookiesAndRetry(episodeSearchUrl, string.Empty); + + try + { + var jsonContent = JObject.Parse(response.Content); + + int errorCode = jsonContent.Value("error_code"); + if (errorCode == 20) // no results found + { + return releases.ToArray(); + } + + if (errorCode > 0) // too many requests per second + { + if (attempts < 3) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return await PerformQuery(query, ++attempts); + } + else + { + throw new Exception(jsonContent.Value("error")); + } + } + + foreach (var item in jsonContent.Value("torrent_results")) + { + var release = new ReleaseInfo(); + release.Title = item.Value("title"); + release.Description = release.Title; + release.Category = MapTrackerCatToNewznab(categoryLabels[item.Value("category")].ToString()); + + release.MagnetUri = new Uri(item.Value("download")); + release.InfoHash = release.MagnetUri.ToString().Split(':')[3].Split('&')[0]; + + release.Comments = new Uri(item.Value("info_page")); + release.Guid = release.Comments; + + // ex: 2015-08-16 21:25:08 +0000 + var dateStr = item.Value("pubdate").Replace(" +0000", ""); + var dateTime = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + release.PublishDate = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc).ToLocalTime(); + + release.Seeders = item.Value("seeders"); + release.Peers = item.Value("leechers") + release.Seeders; + release.Size = item.Value("size"); + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(response.Content, ex); + } + + return releases.ToArray(); + } + + } +} diff --git a/src/Jackett/Indexers/SceneAccess.cs b/src/Jackett/Indexers/SceneAccess.cs index bdad51cf3..2c595e609 100644 --- a/src/Jackett/Indexers/SceneAccess.cs +++ b/src/Jackett/Indexers/SceneAccess.cs @@ -40,7 +40,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -60,6 +60,8 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/SceneTime.cs b/src/Jackett/Indexers/SceneTime.cs index d03e186fc..e517bd323 100644 --- a/src/Jackett/Indexers/SceneTime.cs +++ b/src/Jackett/Indexers/SceneTime.cs @@ -43,7 +43,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -58,6 +58,8 @@ namespace Jackett.Indexers var errorMessage = dom["td.text"].Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } private Dictionary GetSearchFormData(string searchString) diff --git a/src/Jackett/Indexers/ShowRSS.cs b/src/Jackett/Indexers/ShowRSS.cs index 3d29157ad..58d0b52d0 100644 --- a/src/Jackett/Indexers/ShowRSS.cs +++ b/src/Jackett/Indexers/ShowRSS.cs @@ -49,7 +49,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var releases = await PerformQuery(new TorznabQuery()); @@ -58,6 +58,8 @@ namespace Jackett.Indexers { throw new Exception("Could not find releases from this URL"); }); + + return IndexerConfigurationStatus.RequiresTesting; } // Override to load legacy config format diff --git a/src/Jackett/Indexers/SpeedCD.cs b/src/Jackett/Indexers/SpeedCD.cs index d6b7f67ca..8c7d456f1 100644 --- a/src/Jackett/Indexers/SpeedCD.cs +++ b/src/Jackett/Indexers/SpeedCD.cs @@ -46,7 +46,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -61,6 +61,8 @@ namespace Jackett.Indexers var errorMessage = dom["h5"].First().Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/Strike.cs b/src/Jackett/Indexers/Strike.cs index ff3817351..18986602c 100644 --- a/src/Jackett/Indexers/Strike.cs +++ b/src/Jackett/Indexers/Strike.cs @@ -14,6 +14,7 @@ using System.Text; using System.Threading.Tasks; using System.Web; using Jackett.Models.IndexerConfig; +using System.Collections.Specialized; namespace Jackett.Indexers { @@ -27,12 +28,12 @@ namespace Jackett.Indexers set { configData.Url.Value = value.ToString(); } } - private string SearchUrl { get { return BaseUri + "api/v2/torrents/search/?category=TV&phrase={0}"; } } + private string SearchUrl { get { return BaseUri + "api/v2/torrents/search/?phrase={0}"; } } private string DownloadUrl { get { return BaseUri + "torrents/api/download/{0}.torrent"; } } - new ConfigurationDataUrl configData + new ConfigurationDataStrike configData { - get { return (ConfigurationDataUrl)base.configData; } + get { return (ConfigurationDataStrike)base.configData; } set { base.configData = value; } } @@ -41,16 +42,37 @@ namespace Jackett.Indexers : base(name: "Strike", description: "Torrent search engine", link: defaultSiteLink, - caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + caps: new TorznabCapabilities(), manager: i, client: wc, logger: l, p: ps, - configData: new ConfigurationDataUrl(defaultSiteLink)) + configData: new ConfigurationDataStrike(defaultSiteLink)) { + AddCategoryMapping("Anime", TorznabCatType.TVAnime); + AddCategoryMapping("Applications", TorznabCatType.PC); + AddCategoryMapping("Books", TorznabCatType.Books); + AddCategoryMapping("Games", TorznabCatType.PCGames); + AddCategoryMapping("Movies", TorznabCatType.Movies); + AddCategoryMapping("TV", TorznabCatType.TV); + AddCategoryMapping("XXX", TorznabCatType.XXX); + AddCategoryMapping("Music", TorznabCatType.Audio); + + /*AddCategoryMapping("Movies:Highres Movies", TorznabCatType.MoviesHD); + AddCategoryMapping("Movies:3D Movies", TorznabCatType.Movies3D); + AddCategoryMapping("Books:Ebooks", TorznabCatType.BooksEbook); + AddCategoryMapping("Books:Comics", TorznabCatType.BooksComics); + AddCategoryMapping("Books:Audio Books", TorznabCatType.AudioAudiobook); + AddCategoryMapping("Games:XBOX360", TorznabCatType.ConsoleXbox360); + AddCategoryMapping("Games:Wii", TorznabCatType.ConsoleWii); + AddCategoryMapping("Games:PSP", TorznabCatType.ConsolePSP); + AddCategoryMapping("Games:PS3", TorznabCatType.ConsolePS3); + AddCategoryMapping("Games:PC", TorznabCatType.PCGames); + AddCategoryMapping("Games:Android", TorznabCatType.PCPhoneAndroid); + AddCategoryMapping("Music:Mp3", TorznabCatType.AudioMP3);*/ } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var releases = await PerformQuery(new TorznabQuery()); @@ -59,6 +81,8 @@ namespace Jackett.Indexers { throw new Exception("Could not find releases from this URL"); }); + + return IndexerConfigurationStatus.Completed; } // Override to load legacy config format @@ -78,9 +102,18 @@ namespace Jackett.Indexers public async Task> PerformQuery(TorznabQuery query) { List releases = new List(); + var queryString = query.GetQueryString(); + var searchTerm = string.IsNullOrEmpty(queryString) ? DateTime.Now.Year.ToString() : queryString; + var episodeSearchUrl = string.Format(SearchUrl, HttpUtility.UrlEncode(searchTerm)); + + var trackerCategories = MapTorznabCapsToTrackers(query, mapChildrenCatsToParent: true); + + // This tracker can only search one cat at a time, otherwise search all and filter results + if (trackerCategories.Count == 1) + { + episodeSearchUrl += "&category=" + trackerCategories[0]; + } - var searchTerm = string.IsNullOrEmpty(query.SanitizedSearchTerm) ? "2015" : query.SanitizedSearchTerm; - var episodeSearchUrl = string.Format(SearchUrl, HttpUtility.UrlEncode(query.GetQueryString())); var results = await RequestStringWithCookiesAndRetry(episodeSearchUrl, string.Empty); try { @@ -92,6 +125,12 @@ namespace Jackett.Indexers release.MinimumRatio = 1; release.MinimumSeedTime = 172800; + if (trackerCategories.Count > 0 && !trackerCategories.Contains((string)result["torrent_category"])) + { + continue; + } + release.Category = MapTrackerCatToNewznab((string)result["torrent_category"]); + release.Title = (string)result["torrent_title"]; release.Description = release.Title; release.Seeders = (int)result["seeds"]; diff --git a/src/Jackett/Indexers/T411.cs b/src/Jackett/Indexers/T411.cs index a0fc25573..887ad221e 100644 --- a/src/Jackett/Indexers/T411.cs +++ b/src/Jackett/Indexers/T411.cs @@ -81,7 +81,7 @@ namespace Jackett.Indexers return configData.ApiToken.Value; } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -99,6 +99,8 @@ namespace Jackett.Indexers { throw tokenFetchEx; }); + + return IndexerConfigurationStatus.RequiresTesting; } // Override to load legacy config format diff --git a/src/Jackett/Indexers/TVChaosUK.cs b/src/Jackett/Indexers/TVChaosUK.cs index 3d42a570b..0d439592a 100644 --- a/src/Jackett/Indexers/TVChaosUK.cs +++ b/src/Jackett/Indexers/TVChaosUK.cs @@ -103,7 +103,7 @@ namespace Jackett.Indexers AddCategoryMapping("HD Factual/Reality", TorznabCatType.TVHD); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -139,6 +139,7 @@ namespace Jackett.Indexers IsConfigured = false; throw e; } + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/ThePirateBay.cs b/src/Jackett/Indexers/ThePirateBay.cs index a4ad92616..2135442cd 100644 --- a/src/Jackett/Indexers/ThePirateBay.cs +++ b/src/Jackett/Indexers/ThePirateBay.cs @@ -30,6 +30,7 @@ namespace Jackett.Indexers } private string SearchUrl { get { return BaseUri + "search/{0}/0/99/208,205"; } } + private string RecentUrl { get { return BaseUri + "recent"; } } new ConfigurationDataUrl configData { @@ -50,7 +51,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var releases = await PerformQuery(new TorznabQuery()); @@ -59,6 +60,8 @@ namespace Jackett.Indexers { throw new Exception("Could not find releases from this URL"); }); + + return IndexerConfigurationStatus.Completed; } // Override to load legacy config format @@ -79,7 +82,7 @@ namespace Jackett.Indexers { var releases = new List(); var queryStr = HttpUtility.UrlEncode(query.GetQueryString()); - var episodeSearchUrl = string.Format(SearchUrl, queryStr); + var episodeSearchUrl = string.IsNullOrWhiteSpace(queryStr) ? RecentUrl : string.Format(SearchUrl, queryStr); var response = await RequestStringWithCookiesAndRetry(episodeSearchUrl, string.Empty); try @@ -89,8 +92,10 @@ namespace Jackett.Indexers var rows = dom["#searchResult > tbody > tr"]; foreach (var row in rows) { - var release = new ReleaseInfo(); + if (row.ChildElements.Count() < 2) + continue; + var release = new ReleaseInfo(); CQ qRow = row.Cq(); CQ qLink = qRow.Find(".detName > .detLink").First(); @@ -110,7 +115,7 @@ namespace Jackett.Indexers var timeString = descParts[0].Split(' ')[1]; - if (timeString.Contains("mins ago")) + if (timeString.Contains(" ago")) { release.PublishDate = (DateTime.Now - TimeSpan.FromMinutes(ParseUtil.CoerceInt(timeString.Split(' ')[0]))); } diff --git a/src/Jackett/Indexers/TorrentBytes.cs b/src/Jackett/Indexers/TorrentBytes.cs index 4e161ac4e..7ee384627 100644 --- a/src/Jackett/Indexers/TorrentBytes.cs +++ b/src/Jackett/Indexers/TorrentBytes.cs @@ -71,7 +71,7 @@ namespace Jackett.Indexers AddCategoryMapping(24, TorznabCatType.XXXImageset); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -91,6 +91,7 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/TorrentDay.cs b/src/Jackett/Indexers/TorrentDay.cs index c41511770..c67779d83 100644 --- a/src/Jackett/Indexers/TorrentDay.cs +++ b/src/Jackett/Indexers/TorrentDay.cs @@ -94,7 +94,7 @@ namespace Jackett.Indexers return result; } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -140,6 +140,7 @@ namespace Jackett.Indexers throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/TorrentLeech.cs b/src/Jackett/Indexers/TorrentLeech.cs index b1edcd967..10171ecb3 100644 --- a/src/Jackett/Indexers/TorrentLeech.cs +++ b/src/Jackett/Indexers/TorrentLeech.cs @@ -79,7 +79,7 @@ namespace Jackett.Indexers AddCategoryMapping(33, TorznabCatType.PC0day); } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var pairs = new Dictionary { @@ -97,6 +97,7 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) @@ -107,7 +108,7 @@ namespace Jackett.Indexers if (!string.IsNullOrWhiteSpace(searchString)) { - searchUrl += "query/" + HttpUtility.UrlEncode(searchString) + "/"; + searchUrl += "query/" + HttpUtility.UrlEncode(searchString) + "/"; } string.Format(SearchUrl, HttpUtility.UrlEncode(searchString)); @@ -160,7 +161,7 @@ namespace Jackett.Indexers release.Seeders = ParseUtil.CoerceInt(qRow.Find(".seeders").Text()); release.Peers = release.Seeders + ParseUtil.CoerceInt(qRow.Find(".leechers").Text()); - var category = qRow.Find(".category a").Attr("href").Replace("/torrents/browse/index/categories/",string.Empty); + var category = qRow.Find(".category a").Attr("href").Replace("/torrents/browse/index/categories/", string.Empty); release.Category = MapTrackerCatToNewznab(category); releases.Add(release); diff --git a/src/Jackett/Indexers/TorrentShack.cs b/src/Jackett/Indexers/TorrentShack.cs index e79b7cae6..b34daa839 100644 --- a/src/Jackett/Indexers/TorrentShack.cs +++ b/src/Jackett/Indexers/TorrentShack.cs @@ -42,7 +42,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -62,6 +62,7 @@ namespace Jackett.Indexers var errorMessage = messageEl.Text().Trim(); throw new ExceptionWithConfigData(errorMessage, configData); }); + return IndexerConfigurationStatus.RequiresTesting; } public async Task> PerformQuery(TorznabQuery query) diff --git a/src/Jackett/Indexers/Torrentz.cs b/src/Jackett/Indexers/Torrentz.cs index e68b5efef..ed7640726 100644 --- a/src/Jackett/Indexers/Torrentz.cs +++ b/src/Jackett/Indexers/Torrentz.cs @@ -50,7 +50,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); var releases = await PerformQuery(new TorznabQuery()); @@ -59,6 +59,7 @@ namespace Jackett.Indexers { throw new Exception("Could not find releases from this URL"); }); + return IndexerConfigurationStatus.Completed; } // Override to load legacy config format diff --git a/src/Jackett/Indexers/nCore.cs b/src/Jackett/Indexers/nCore.cs index b86176e30..8f4979896 100644 --- a/src/Jackett/Indexers/nCore.cs +++ b/src/Jackett/Indexers/nCore.cs @@ -43,7 +43,7 @@ namespace Jackett.Indexers { } - public async Task ApplyConfiguration(JToken configJson) + public async Task ApplyConfiguration(JToken configJson) { configData.LoadValuesFromJson(configJson); @@ -69,6 +69,8 @@ namespace Jackett.Indexers var errorMessage = msgContainer != null ? msgContainer.InnerText : "Error while trying to login."; throw new ExceptionWithConfigData(errorMessage, configData); }); + + return IndexerConfigurationStatus.RequiresTesting; } List> CreateKeyValueList(params string[][] keyValues) diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 3bb428742..b84a20994 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -175,6 +175,8 @@ + + @@ -189,7 +191,9 @@ - + + + @@ -214,6 +218,7 @@ + @@ -221,6 +226,7 @@ + @@ -402,6 +408,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -414,6 +423,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -438,6 +450,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Jackett/Models/CategoryMapping.cs b/src/Jackett/Models/CategoryMapping.cs index 6ab314c48..3fd22a3e7 100644 --- a/src/Jackett/Models/CategoryMapping.cs +++ b/src/Jackett/Models/CategoryMapping.cs @@ -10,7 +10,7 @@ namespace Jackett.Models { public CategoryMapping(string trackerCat, int newzCat) { - TrackerCategory = trackerCat.ToLowerInvariant(); + TrackerCategory = trackerCat; NewzNabCategory = newzCat; } diff --git a/src/Jackett/Models/IndexerConfig/ConfigurationDataStrike.cs b/src/Jackett/Models/IndexerConfig/ConfigurationDataStrike.cs new file mode 100644 index 000000000..124ed0c2c --- /dev/null +++ b/src/Jackett/Models/IndexerConfig/ConfigurationDataStrike.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett.Models.IndexerConfig +{ + public class ConfigurationDataStrike : ConfigurationDataUrl + { + public DisplayItem StrikeWarning { get; private set; } + + public ConfigurationDataStrike(string url) : base(url) + { + StrikeWarning = new DisplayItem("This indexer does not support RSS Sync, only Search") { Name = "Warning" }; + } + } +} diff --git a/src/Jackett/Models/IndexerConfigurationStatus.cs b/src/Jackett/Models/IndexerConfigurationStatus.cs new file mode 100644 index 000000000..2c232f7c9 --- /dev/null +++ b/src/Jackett/Models/IndexerConfigurationStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett.Models +{ + public enum IndexerConfigurationStatus + { + Completed, + RequiresTesting, + Failed + } +} diff --git a/src/Jackett/Indexers/ManualSearchResult.cs b/src/Jackett/Models/ManualSearchResult.cs similarity index 91% rename from src/Jackett/Indexers/ManualSearchResult.cs rename to src/Jackett/Models/ManualSearchResult.cs index 0831640bf..59e1a35bd 100644 --- a/src/Jackett/Indexers/ManualSearchResult.cs +++ b/src/Jackett/Models/ManualSearchResult.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Jackett.Indexers +namespace Jackett { public class ManualSearchResult { diff --git a/src/Jackett/Properties/AssemblyInfo.cs b/src/Jackett/Properties/AssemblyInfo.cs index 6a6c92c39..6e876de18 100644 --- a/src/Jackett/Properties/AssemblyInfo.cs +++ b/src/Jackett/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.6.2.0")] -[assembly: AssemblyFileVersion("0.6.2.0")] +[assembly: AssemblyVersion("0.6.3.0")] +[assembly: AssemblyFileVersion("0.6.3.0")] diff --git a/src/Jackett/Utils/BrowserUtil.cs b/src/Jackett/Utils/BrowserUtil.cs index cc23d2baa..326a6c7da 100644 --- a/src/Jackett/Utils/BrowserUtil.cs +++ b/src/Jackett/Utils/BrowserUtil.cs @@ -10,7 +10,16 @@ namespace Jackett.Utils { public static string ChromeUserAgent { - get { return "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36"; } + get { + if (System.Environment.OSVersion.Platform == PlatformID.Unix) + { + return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chrome/44.0.2403.155 Safari/537.36"; + } + else + { + return "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36"; + } + } } } } diff --git a/src/Website/App_Data/web.db b/src/Website/App_Data/web.db new file mode 100644 index 000000000..ef741b127 Binary files /dev/null and b/src/Website/App_Data/web.db differ diff --git a/src/Website/Content/libs/jquery.timeago.js b/src/Website/Content/libs/jquery.timeago.js new file mode 100644 index 000000000..15805a619 --- /dev/null +++ b/src/Website/Content/libs/jquery.timeago.js @@ -0,0 +1,221 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.4.1 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowPast: true, + allowFuture: false, + localeTitle: false, + cutoff: 0, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + inPast: 'any moment now', + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + + inWords: function(distanceMillis) { + if(!this.settings.allowPast && ! this.settings.allowFuture) { + throw 'timeago allowPast and allowFuture settings can not both be set to false.'; + } + + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + if(!this.settings.allowPast && distanceMillis >= 0) { + return this.settings.strings.inPast; + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + var parsedTime = $t.parse(time); + $(this).data('timeago', { datetime: parsedTime }); + if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString()); + refresh.apply(this); + }, + updateFromDOM: function(){ + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + }, + dispose: function () { + if (this._timeagoInterval) { + window.clearInterval(this._timeagoInterval); + this._timeagoInterval = null; + } + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + //check if it's still visible + if(!$.contains(document.documentElement,this)){ + //stop if it has been removed + $(this).timeago("dispose"); + return this; + } + + var data = prepareData(this); + var $s = $t.settings; + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/src/Website/Scripts/jquery.timeago.js b/src/Website/Scripts/jquery.timeago.js new file mode 100644 index 000000000..15805a619 --- /dev/null +++ b/src/Website/Scripts/jquery.timeago.js @@ -0,0 +1,221 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.4.1 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowPast: true, + allowFuture: false, + localeTitle: false, + cutoff: 0, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + inPast: 'any moment now', + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + + inWords: function(distanceMillis) { + if(!this.settings.allowPast && ! this.settings.allowFuture) { + throw 'timeago allowPast and allowFuture settings can not both be set to false.'; + } + + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + if(!this.settings.allowPast && distanceMillis >= 0) { + return this.settings.strings.inPast; + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + var parsedTime = $t.parse(time); + $(this).data('timeago', { datetime: parsedTime }); + if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString()); + refresh.apply(this); + }, + updateFromDOM: function(){ + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + }, + dispose: function () { + if (this._timeagoInterval) { + window.clearInterval(this._timeagoInterval); + this._timeagoInterval = null; + } + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + //check if it's still visible + if(!$.contains(document.documentElement,this)){ + //stop if it has been removed + $(this).timeago("dispose"); + return this; + } + + var data = prepareData(this); + var $s = $t.settings; + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/src/Website/Views/Download/Index.cshtml b/src/Website/Views/Download/Index.cshtml index a3cb09d12..51018c5de 100644 --- a/src/Website/Views/Download/Index.cshtml +++ b/src/Website/Views/Download/Index.cshtml @@ -9,6 +9,7 @@ {

@release.Title - @release.Version

+
Published
var files = Website.Services.FileService.FindFilesForRelease(release.Version); diff --git a/src/Website/Views/Shared/_Layout.cshtml b/src/Website/Views/Shared/_Layout.cshtml index 58497ea4d..d7373becc 100644 --- a/src/Website/Views/Shared/_Layout.cshtml +++ b/src/Website/Views/Shared/_Layout.cshtml @@ -8,6 +8,7 @@ + @@ -36,5 +37,12 @@ + + + \ No newline at end of file diff --git a/src/Website/Web.config b/src/Website/Web.config index 449a64236..f150b78f1 100644 --- a/src/Website/Web.config +++ b/src/Website/Web.config @@ -31,7 +31,7 @@ - + diff --git a/src/Website/Website.csproj b/src/Website/Website.csproj index d0872dadc..0a14a684e 100644 --- a/src/Website/Website.csproj +++ b/src/Website/Website.csproj @@ -293,6 +293,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -345,6 +348,7 @@ + diff --git a/src/Website/packages.config b/src/Website/packages.config index 4215a3659..92b6ef170 100644 --- a/src/Website/packages.config +++ b/src/Website/packages.config @@ -4,6 +4,7 @@ +