diff --git a/src/Jackett/Content/logos/xspeeds.png b/src/Jackett/Content/logos/xspeeds.png new file mode 100644 index 000000000..81098b346 Binary files /dev/null and b/src/Jackett/Content/logos/xspeeds.png differ diff --git a/src/Jackett/Indexers/XSpeeds.cs b/src/Jackett/Indexers/XSpeeds.cs new file mode 100644 index 000000000..fd72894c8 --- /dev/null +++ b/src/Jackett/Indexers/XSpeeds.cs @@ -0,0 +1,226 @@ +using CsQuery; +using Jackett.Models; +using Jackett.Services; +using Jackett.Utils; +using Jackett.Utils.Clients; +using Newtonsoft.Json.Linq; +using NLog; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Jackett.Models.IndexerConfig; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Jackett.Indexers +{ + public class XSpeeds : BaseIndexer, IIndexer + { + string LoginUrl { get { return SiteLink + "takelogin.php"; } } + string GetRSSKeyUrl { get { return SiteLink + "getrss.php"; } } + string SearchUrl { get { return SiteLink + "browse.php"; } } + string RSSUrl { get { return SiteLink + "rss.php?secret_key={0}&feedtype=download&timezone=0&showrows=50&categories=all"; } } + string CommentUrl { get { return SiteLink + "details.php?id={0}"; } } + string DownloadUrl { get { return SiteLink + "download.php?id={0}"; } } + + new ConfigurationDataBasicLoginWithRSS configData + { + get { return (ConfigurationDataBasicLoginWithRSS)base.configData; } + set { base.configData = value; } + } + + public XSpeeds(IIndexerManagerService i, IWebClient wc, Logger l, IProtectionService ps) + : base(name: "XSpeeds", + description: "XSpeeds", + link: "https://www.xspeeds.eu/", + caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + manager: i, + client: wc, + logger: l, + p: ps, + configData: new ConfigurationDataBasicLoginWithRSS()) + { + AddCategoryMapping(70, TorznabCatType.TVAnime); + AddCategoryMapping(80, TorznabCatType.AudioAudiobook); + AddCategoryMapping(66, TorznabCatType.MoviesBluRay); + AddCategoryMapping(48, TorznabCatType.Books); + AddCategoryMapping(68, TorznabCatType.MoviesOther); + AddCategoryMapping(65, TorznabCatType.TVDocumentary); + AddCategoryMapping(10, TorznabCatType.MoviesDVD); + AddCategoryMapping(74, TorznabCatType.TVOTHER); + AddCategoryMapping(44, TorznabCatType.TVSport); + AddCategoryMapping(12, TorznabCatType.Movies); + AddCategoryMapping(13, TorznabCatType.Audio); + AddCategoryMapping(6, TorznabCatType.PC); + AddCategoryMapping(4, TorznabCatType.PC); + AddCategoryMapping(31, TorznabCatType.ConsolePS3); + AddCategoryMapping(31, TorznabCatType.ConsolePS4); + AddCategoryMapping(20, TorznabCatType.TVSport); + AddCategoryMapping(86, TorznabCatType.TVSport); + AddCategoryMapping(47, TorznabCatType.TVHD); + AddCategoryMapping(16, TorznabCatType.TVSD); + AddCategoryMapping(7, TorznabCatType.ConsoleWii); + AddCategoryMapping(8, TorznabCatType.ConsoleXbox); + + // RSS Textual categories + AddCategoryMapping("Apps", TorznabCatType.PC); + AddCategoryMapping("Music", TorznabCatType.Audio); + AddCategoryMapping("Audiobooks", TorznabCatType.AudioAudiobook); + + } + + public async Task ApplyConfiguration(JToken configJson) + { + configData.LoadValuesFromJson(configJson); + var pairs = new Dictionary { + { "username", configData.Username.Value }, + { "password", configData.Password.Value } + }; + + var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, SiteLink, true); + result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, result.Cookies, true, SearchUrl, SiteLink, true); + await ConfigureIfOK(result.Cookies, result.Content != null && result.Content.Contains("logout.php"), () => + { + CQ dom = result.Content; + var errorMessage = dom[".left_side table:eq(0) tr:eq(1)"].Text().Trim().Replace("\n\t", " "); + throw new ExceptionWithConfigData(errorMessage, configData); + }); + + try + { + // Get RSS key + var rssParams = new Dictionary { + { "feedtype", "download" }, + { "timezone", "0" }, + { "showrows", "50" } + }; + var rssPage = await PostDataWithCookies(GetRSSKeyUrl, rssParams, result.Cookies); + var match = Regex.Match(rssPage.Content, "(?<=secret_key\\=)([a-zA-z0-9]*)"); + configData.RSSKey.Value = match.Success ? match.Value : string.Empty; + if (string.IsNullOrWhiteSpace(configData.RSSKey.Value)) + throw new Exception("Failed to get RSS Key"); + SaveConfig(); + } + catch (Exception e) + { + IsConfigured = false; + throw e; + } + return IndexerConfigurationStatus.RequiresTesting; + } + + public async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + var searchString = query.GetQueryString(); + + // If we have no query use the RSS Page as their server is slow enough at times! + if (string.IsNullOrWhiteSpace(searchString)) + { + var rssPage = await RequestStringWithCookiesAndRetry(string.Format(RSSUrl, configData.RSSKey.Value)); + var rssDoc = XDocument.Parse(rssPage.Content); + + foreach (var item in rssDoc.Descendants("item")) + { + var title = item.Descendants("title").First().Value; + var description = item.Descendants("description").First().Value; + var link = item.Descendants("link").First().Value; + var category = item.Descendants("category").First().Value; + var date = item.Descendants("pubDate").First().Value; + + var torrentIdMatch = Regex.Match(link, "(?<=id=)(\\d)*"); + var torrentId = torrentIdMatch.Success ? torrentIdMatch.Value : string.Empty; + if (string.IsNullOrWhiteSpace(torrentId)) + throw new Exception("Missing torrent id"); + + var infoMatch = Regex.Match(description, @"Category:\W(?.*)\W\/\WSeeders:\W(?\d*)\W\/\WLeechers:\W(?\d*)\W\/\WSize:\W(?[\d\.]*\W\S*)"); + if (!infoMatch.Success) + throw new Exception("Unable to find info"); + + var release = new ReleaseInfo() + { + Title = title, + Description = title, + Guid = new Uri(string.Format(DownloadUrl, torrentId)), + Comments = new Uri(string.Format(CommentUrl, torrentId)), + PublishDate = DateTime.ParseExact(date, "yyyy-MM-dd H:mm:ss", CultureInfo.InvariantCulture), //2015-08-08 21:20:31 + Link = new Uri(string.Format(DownloadUrl, torrentId)), + Seeders = ParseUtil.CoerceInt(infoMatch.Groups["seeders"].Value), + Peers = ParseUtil.CoerceInt(infoMatch.Groups["leechers"].Value), + Size = ReleaseInfo.GetBytes(infoMatch.Groups["size"].Value), + Category = MapTrackerCatToNewznab(infoMatch.Groups["cat"].Value) + }; + + // If its not apps or audio we can only mark as general TV + if (release.Category == 0) + release.Category = 5030; + + release.Peers += release.Seeders; + releases.Add(release); + } + } + else + { + var searchParams = new Dictionary { + { "do", "search" }, + { "keywords", searchString }, + { "search_type", "t_name" }, + { "category", "0" }, + { "include_dead_torrents", "no" } + }; + + var searchPage = await PostDataWithCookiesAndRetry(SearchUrl, searchParams); + try + { + CQ dom = searchPage.Content; + var rows = dom["#listtorrents tbody tr"]; + foreach (var row in rows.Skip(1)) + { + var release = new ReleaseInfo(); + var qRow = row.Cq(); + + release.Title = qRow.Find("td:eq(1) .tooltip-content div:eq(0)").Text(); + + if (string.IsNullOrWhiteSpace(release.Title)) + continue; + + release.Description = release.Title; + release.Guid = new Uri(qRow.Find("td:eq(2) a").Attr("href")); + release.Link = release.Guid; + release.Comments = new Uri(qRow.Find("td:eq(1) .tooltip-target a").Attr("href")); + release.PublishDate = DateTime.ParseExact(qRow.Find("td:eq(1) div").Last().Text().Trim(), "dd-MM-yyyy H:mm", CultureInfo.InvariantCulture); //08-08-2015 12:51 + release.Seeders = ParseUtil.CoerceInt(qRow.Find("td:eq(6)").Text()); + release.Peers = release.Seeders + ParseUtil.CoerceInt(qRow.Find("td:eq(7)").Text().Trim()); + release.Size = ReleaseInfo.GetBytes(qRow.Find("td:eq(4)").Text().Trim()); + + + var cat = row.Cq().Find("td:eq(0) a").First().Attr("href"); + var catSplit = cat.LastIndexOf('='); + if (catSplit > -1) + cat = cat.Substring(catSplit + 1); + release.Category = MapTrackerCatToNewznab(cat); + + // If its not apps or audio we can only mark as general TV + if (release.Category == 0) + release.Category = 5030; + + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(searchPage.Content, ex); + } + } + + return releases; + } + } +} diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 08e3f039a..ec2350c60 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -209,6 +209,7 @@ + @@ -556,6 +557,7 @@ PreserveNewest + PreserveNewest