using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using AngleSharp.Html.Parser; using Jackett.Common.Models; using Jackett.Common.Models.IndexerConfig; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using Newtonsoft.Json.Linq; using NLog; using static Jackett.Common.Models.IndexerConfig.ConfigurationData; namespace Jackett.Common.Indexers { [ExcludeFromCodeCoverage] public class XSpeeds : BaseWebIndexer { private string LandingUrl => SiteLink + "login.php"; private string LoginUrl => SiteLink + "takelogin.php"; private string GetRSSKeyUrl => SiteLink + "getrss.php"; private string SearchUrl => SiteLink + "browse.php"; private new ConfigurationDataBasicLoginWithRSSAndDisplay configData => (ConfigurationDataBasicLoginWithRSSAndDisplay)base.configData; public XSpeeds(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) : base(id: "xspeeds", name: "XSpeeds", description: "XSpeeds (XS) is a Private Torrent Tracker for MOVIES / TV / GENERAL", link: "https://www.xspeeds.eu/", caps: new TorznabCapabilities { SupportsImdbMovieSearch = true // SupportsImdbTVSearch = true (supported by the site but disabled due to #8107) }, configService: configService, client: wc, logger: l, p: ps, configData: new ConfigurationDataBasicLoginWithRSSAndDisplay()) { Encoding = Encoding.UTF8; Language = "en-us"; Type = "private"; AddCategoryMapping(92, TorznabCatType.MoviesUHD, "4K Movies"); AddCategoryMapping(91, TorznabCatType.TVUHD, "4K TV"); AddCategoryMapping(94, TorznabCatType.TVUHD, "4K TV Boxsets"); AddCategoryMapping(70, TorznabCatType.TVAnime, "Anime"); AddCategoryMapping(4, TorznabCatType.PC, "Apps"); AddCategoryMapping(82, TorznabCatType.PCMac, "Mac"); AddCategoryMapping(80, TorznabCatType.AudioAudiobook, "Audiobooks"); AddCategoryMapping(66, TorznabCatType.MoviesBluRay, "Blu-Ray"); AddCategoryMapping(48, TorznabCatType.Books, "Books Magazines"); AddCategoryMapping(68, TorznabCatType.MoviesOther, "Cams/TS"); AddCategoryMapping(65, TorznabCatType.TVDocumentary, "Documentaries"); AddCategoryMapping(10, TorznabCatType.MoviesDVD, "DVDR"); AddCategoryMapping(72, TorznabCatType.MoviesForeign, "Foreign"); AddCategoryMapping(74, TorznabCatType.TVOTHER, "Kids"); AddCategoryMapping(44, TorznabCatType.TVSport, "MMA"); AddCategoryMapping(11, TorznabCatType.Movies, "Movie Boxsets"); AddCategoryMapping(12, TorznabCatType.Movies, "Movies"); AddCategoryMapping(13, TorznabCatType.Audio, "Music"); AddCategoryMapping(15, TorznabCatType.AudioVideo, "Music Videos"); AddCategoryMapping(32, TorznabCatType.ConsoleNDS, "NDS Games"); AddCategoryMapping(9, TorznabCatType.Other, "Other"); AddCategoryMapping(95, TorznabCatType.PCMac, "Mac Games"); AddCategoryMapping(6, TorznabCatType.PCGames, "PC Games"); AddCategoryMapping(45, TorznabCatType.Other, "Pictures"); AddCategoryMapping(31, TorznabCatType.ConsolePS4, "Playstation"); AddCategoryMapping(71, TorznabCatType.TV, "PPV"); AddCategoryMapping(54, TorznabCatType.TV, "Soaps"); AddCategoryMapping(20, TorznabCatType.TVSport, "Sports"); AddCategoryMapping(86, TorznabCatType.TVSport, "MotorSports"); AddCategoryMapping(89, TorznabCatType.TVSport, "Olympics 2016"); AddCategoryMapping(88, TorznabCatType.TVSport, "World Cup"); AddCategoryMapping(83, TorznabCatType.Movies, "TOTM"); AddCategoryMapping(21, TorznabCatType.TVSD, "TV Boxsets"); AddCategoryMapping(76, TorznabCatType.TVHD, "HD Boxsets"); AddCategoryMapping(47, TorznabCatType.TVHD, "TV-HD"); AddCategoryMapping(16, TorznabCatType.TVSD, "TV-SD"); AddCategoryMapping(7, TorznabCatType.ConsoleWii, "Wii Games"); AddCategoryMapping(43, TorznabCatType.TVSport, "Wrestling"); AddCategoryMapping(8, TorznabCatType.ConsoleXbox, "Xbox Games"); // RSS Textual categories AddCategoryMapping("4K Movies", TorznabCatType.MoviesUHD); AddCategoryMapping("4K TV", TorznabCatType.TVUHD); AddCategoryMapping("4K TV Boxsets", TorznabCatType.TVUHD); AddCategoryMapping("Anime", TorznabCatType.TVAnime); AddCategoryMapping("Apps", TorznabCatType.PC); AddCategoryMapping("Mac", TorznabCatType.PCMac); AddCategoryMapping("Audiobooks", TorznabCatType.AudioAudiobook); AddCategoryMapping("Blu-Ray", TorznabCatType.MoviesBluRay); AddCategoryMapping("Books Magazines", TorznabCatType.Books); AddCategoryMapping("Cams/TS", TorznabCatType.MoviesOther); AddCategoryMapping("Documentaries", TorznabCatType.TVDocumentary); AddCategoryMapping("DVDR", TorznabCatType.MoviesDVD); AddCategoryMapping("Foreign", TorznabCatType.MoviesForeign); AddCategoryMapping("Kids", TorznabCatType.TVOTHER); AddCategoryMapping("MMA", TorznabCatType.TVSport); AddCategoryMapping("Movie Boxsets", TorznabCatType.Movies); AddCategoryMapping("Movies", TorznabCatType.Movies); AddCategoryMapping("Music", TorznabCatType.Audio); AddCategoryMapping("Music Videos", TorznabCatType.AudioVideo); AddCategoryMapping("NDS Games", TorznabCatType.ConsoleNDS); AddCategoryMapping("Other", TorznabCatType.Other); AddCategoryMapping("PC Games", TorznabCatType.PCGames); AddCategoryMapping("Pictures", TorznabCatType.Other); AddCategoryMapping("Playstation", TorznabCatType.ConsolePS4); AddCategoryMapping("PPV", TorznabCatType.TV); AddCategoryMapping("Soaps", TorznabCatType.TV); AddCategoryMapping("Sports", TorznabCatType.TVSport); AddCategoryMapping("MotorSports", TorznabCatType.TVSport); AddCategoryMapping("Olympics 2016", TorznabCatType.TVSport); AddCategoryMapping("World Cup", TorznabCatType.TVSport); AddCategoryMapping("TOTM", TorznabCatType.Movies); AddCategoryMapping("TV Boxsets", TorznabCatType.TVSD); AddCategoryMapping("HD Boxsets", TorznabCatType.TVHD); AddCategoryMapping("TV-HD", TorznabCatType.TVHD); AddCategoryMapping("TV-SD", TorznabCatType.TVSD); AddCategoryMapping("Wii Games", TorznabCatType.ConsoleWii); AddCategoryMapping("Wrestling", TorznabCatType.TVSport); AddCategoryMapping("Xbox Games", TorznabCatType.ConsoleXbox); } public override async Task GetConfigurationForSetup() { var loginPage = await RequestWithCookiesAsync(LandingUrl); var parser = new HtmlParser(); var dom = parser.ParseDocument(loginPage.ContentString); var qCaptchaImg = dom.QuerySelector("img#regimage"); if (qCaptchaImg != null) { var captchaUrl = qCaptchaImg.GetAttribute("src"); var captchaImageResponse = await RequestWithCookiesAsync(captchaUrl, loginPage.Cookies, RequestType.GET, LandingUrl); var captchaText = new StringItem { Name = "Captcha Text" }; var captchaImage = new ImageItem {Name = "Captcha Image", Value = captchaImageResponse.ContentBytes}; configData.AddDynamic("CaptchaText", captchaText); configData.AddDynamic("CaptchaImage", captchaImage); } else logger.Debug($"{Id}: No captcha image found"); return configData; } public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); var pairs = new Dictionary { {"username", configData.Username.Value}, {"password", configData.Password.Value} }; var captchaText = (StringItem)configData.GetDynamic("CaptchaText"); if (captchaText != null) pairs.Add("imagestring", captchaText.Value); //var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, null, SiteLink, true); var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, SearchUrl, LandingUrl, true); await ConfigureIfOK(result.Cookies, result.ContentString?.Contains("logout.php") == true, () => { var parser = new HtmlParser(); var dom = parser.ParseDocument(result.ContentString); var errorMessage = dom.QuerySelector(".left_side table:nth-of-type(1) tr:nth-of-type(2)")?.TextContent.Trim().Replace("\n\t", " "); if (string.IsNullOrWhiteSpace(errorMessage)) errorMessage = dom.QuerySelector("div.notification-body").TextContent.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 RequestWithCookiesAsync( GetRSSKeyUrl, result.Cookies, RequestType.POST, data: rssParams); var match = Regex.Match(rssPage.ContentString, "(?<=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 { IsConfigured = false; throw; } return IndexerConfigurationStatus.RequiresTesting; } protected override async Task> PerformQuery(TorznabQuery query) { var releases = new List(); var searchString = query.GetQueryString(); var prevCook = CookieHeader + ""; var searchParams = new Dictionary { { "do", "search" }, { "category", "0" }, { "include_dead_torrents", "no" } }; if (query.IsImdbQuery) { searchParams.Add("keywords", query.ImdbID); searchParams.Add("search_type", "t_both"); } else { searchParams.Add("keywords", searchString); searchParams.Add("search_type", "t_name"); } var searchPage = await RequestWithCookiesAndRetryAsync( SearchUrl, CookieHeader, RequestType.POST, null, searchParams); // Occasionally the cookies become invalid, login again if that happens if (searchPage.IsRedirect) { await ApplyConfiguration(null); searchPage = await RequestWithCookiesAndRetryAsync( SearchUrl, CookieHeader, RequestType.POST, null, searchParams); } try { var parser = new HtmlParser(); var dom = parser.ParseDocument(searchPage.ContentString); var rows = dom.QuerySelectorAll("table#sortabletable > tbody > tr:has(div > a[href*=\"details.php?id=\"])"); foreach (var row in rows) { var release = new ReleaseInfo(); var qDetails = row.QuerySelector("div > a[href*=\"details.php?id=\"]"); var qTitle = qDetails; // #7975 release.Title = qTitle.TextContent; release.Guid = new Uri(row.QuerySelector("td:nth-of-type(3) a").GetAttribute("href")); release.Link = release.Guid; release.Comments = new Uri(qDetails.GetAttribute("href")); //08-08-2015 12:51 release.PublishDate = DateTime.ParseExact( row.QuerySelectorAll("td:nth-of-type(2) div").Last().TextContent.Trim(), "dd-MM-yyyy H:mm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); release.Seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(7)").TextContent); release.Peers = release.Seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(8)").TextContent.Trim()); release.Size = ReleaseInfo.GetBytes(row.QuerySelector("td:nth-of-type(5)").TextContent.Trim()); var qBanner = row.QuerySelector("td:nth-of-type(2) .tooltip-content img"); if (qBanner != null) release.BannerUrl = new Uri(qBanner.GetAttribute("src")); var cat = row.QuerySelector("td:nth-of-type(1) a").GetAttribute("href"); var catSplit = cat.LastIndexOf('='); if (catSplit > -1) cat = cat.Substring(catSplit + 1); release.Category = MapTrackerCatToNewznab(cat); var grabs = row.QuerySelector("td:nth-child(6)").TextContent; release.Grabs = ParseUtil.CoerceInt(grabs); if (row.QuerySelector("img[alt^=\"Free Torrent\"]") != null) release.DownloadVolumeFactor = 0; else if (row.QuerySelector("img[alt^=\"Silver Torrent\"]") != null) release.DownloadVolumeFactor = 0.5; else release.DownloadVolumeFactor = 1; if (row.QuerySelector("img[alt^=\"x2 Torrent\"]") != null) release.UploadVolumeFactor = 2; else release.UploadVolumeFactor = 1; releases.Add(release); } } catch (Exception ex) { OnParseError(searchPage.ContentString, ex); } if (!CookieHeader.Trim().Equals(prevCook.Trim())) SaveConfig(); return releases; } } }