diff --git a/README.md b/README.md index a659c27f5..e93f13f75 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ We were previously focused on TV but are working on extending searches to allow * [ShowRSS](https://showrss.info/) * [Strike](https://getstrike.net/) * [T411](http://www.t411.io/) + * [TehConnection](https://tehconnection.eu/) * [The Pirate Bay](https://thepiratebay.se/) * [TorrentBytes](https://www.torrentbytes.net/) * [TorrentDay](https://torrentday.eu/) diff --git a/src/Jackett/Content/logos/tehconnection.png b/src/Jackett/Content/logos/tehconnection.png new file mode 100644 index 000000000..4a859cec9 Binary files /dev/null and b/src/Jackett/Content/logos/tehconnection.png differ diff --git a/src/Jackett/Indexers/BaseIndexer.cs b/src/Jackett/Indexers/BaseIndexer.cs index 4ae334dca..404f55044 100644 --- a/src/Jackett/Indexers/BaseIndexer.cs +++ b/src/Jackett/Indexers/BaseIndexer.cs @@ -364,7 +364,7 @@ protected async Task RequestLoginAndFollowRedirect(string if (response.IsRedirect) { - await FollowIfRedirect(response, request.Url, null, response.Cookies); + await FollowIfRedirect(response, request.Url, redirectUrlOverride, response.Cookies); } if (returnCookiesFromFirstCall) diff --git a/src/Jackett/Indexers/TehConnection.cs b/src/Jackett/Indexers/TehConnection.cs new file mode 100644 index 000000000..308e31665 --- /dev/null +++ b/src/Jackett/Indexers/TehConnection.cs @@ -0,0 +1,183 @@ +using CsQuery; +using Jackett.Indexers; +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.Globalization; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Jackett.Models.IndexerConfig; + +namespace Jackett.Indexers +{ + public class TehConnection : BaseIndexer, IIndexer + { + private string LoginUrl { get { return SiteLink + "login.php"; } } + private string indexUrl { get { return SiteLink + "index.php"; } } + private string SearchUrl { get { return SiteLink + "torrents.php"; } } + + new ConfigurationDataBasicLoginWithFilter configData + { + get { return (ConfigurationDataBasicLoginWithFilter)base.configData; } + set { base.configData = value; } + } + + public TehConnection(IIndexerManagerService i, Logger l, IWebClient c, IProtectionService ps) + : base(name: "TehConnection", + description: "Working towards providing a well-seeded archive of all available digital forms of cinema and film in their highest possible quality", + link: "https://tehconnection.eu/", + caps: new TorznabCapabilities(), + manager: i, + client: c, + logger: l, + p: ps, + configData: new ConfigurationDataBasicLoginWithFilter(@"Enter filter options below to restrict search results. + Separate options with a space if using more than one option.
Filter options available: +
QualityEncodeOnly
FreeLeechOnly")) + { + AddCategoryMapping(7, TorznabCatType.Movies); + AddCategoryMapping(7, TorznabCatType.MoviesForeign); + AddCategoryMapping(7, TorznabCatType.MoviesOther); + AddCategoryMapping(7, TorznabCatType.MoviesSD); + AddCategoryMapping(7, TorznabCatType.MoviesHD); + AddCategoryMapping(7, TorznabCatType.Movies3D); + AddCategoryMapping(7, TorznabCatType.MoviesBluRay); + AddCategoryMapping(7, TorznabCatType.MoviesDVD); + AddCategoryMapping(7, TorznabCatType.MoviesWEBDL); + } + + public async Task ApplyConfiguration(JToken configJson) + { + configData.LoadValuesFromJson(configJson); + var pairs = new Dictionary { + { "username", configData.Username.Value }, + { "password", configData.Password.Value }, + { "keeplogged", "1" }, + { "login", "Log In!" } + }; + + var response = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, indexUrl, SiteLink); + + await ConfigureIfOK(response.Cookies, response.Content != null && response.Content.Contains("/logout.php"), () => + { + CQ dom = response.Content; + string errorMessage = "Unable to login to TehConnection"; + throw new ExceptionWithConfigData(errorMessage, configData); + }); + return IndexerConfigurationStatus.RequiresTesting; + } + + public async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + bool configFreeLeechOnly = configData.FilterString.Value.ToLowerInvariant().Contains("freeleechonly"); + bool configQualityEncodeOnly = configData.FilterString.Value.ToLowerInvariant().Contains("qualityencodeonly"); + string movieListSearchUrl; + + if (string.IsNullOrEmpty(query.GetQueryString())) + movieListSearchUrl = SearchUrl; + else + { + movieListSearchUrl = string.Format("{0}?action=basic&searchstr={1}", SearchUrl, HttpUtility.UrlEncode(query.GetQueryString())); + } + + var results = await RequestStringWithCookiesAndRetry(movieListSearchUrl); + try + { + CQ mdom = results.Content; + + var mrows = mdom[".torrent_title_box"]; + foreach (var mrow in mrows.Take(5)) + { + var mqRow = mrow.Cq(); + + Uri movieReleasesLink = new Uri(SiteLink.TrimEnd('/') + mqRow.Find("a[title='View Torrent']").First().Attr("href").Trim()); + bool commentsPresent = !mqRow.Find("font[class='tags'] > strong > a:contains('Comment')").First().Text().Trim().Contains("0 Comments"); + Uri commentsLink = new Uri(SiteLink.TrimEnd('/') + mqRow.Find("font[class='tags'] > strong > a:contains('Comment')").First().Attr("href").Trim()); + + string imdblink = mqRow.Find("span[class='imdb-number-rating']").Length > 0 ? mqRow.Find("span[class='imdb-number-rating'] > a").First().Attr("href").Trim() : ""; + long imdb_id = 0; + try + { + if (!string.IsNullOrWhiteSpace(imdblink) && imdblink.ToLowerInvariant().Contains("tt")) + { + imdb_id = long.Parse(imdblink.Substring(imdblink.LastIndexOf('t') + 1).Replace("/", "")); + } + } + catch { imdb_id = 0; } + + var release_results = await RequestStringWithCookiesAndRetry(movieReleasesLink.ToString()); + + //Iterate over the releases for each movie + + CQ dom = release_results.Content; + + var rows = dom[".torrent_widget.box.pad"]; + foreach (var row in rows) + { + var qRow = row.Cq(); + + string title = qRow.Find("[id^=desc_] > h2 > strong").First().Text().Trim(); + Uri link = new Uri(SiteLink.TrimEnd('/') + qRow.Find("a[title='Download']").First().Attr("href").Trim()); + Uri guid = new Uri(SiteLink.TrimEnd('/') + qRow.Find("a[title='Permalink']").First().Attr("href").Trim()); + string pubDateStr = qRow.Find("div[class='box pad'] > p:contains('Uploaded by') > span").First().Attr("title").Trim(); + DateTime pubDate = DateTime.ParseExact(pubDateStr, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime(); + string sizeStr = qRow.Find("[id^=desc_] > div > table > tbody > tr > td > strong:contains('Size:')").First().Parent().Parent().Find("td").Last().Text().Trim(); + Int32 seeders = Convert.ToInt32(qRow.Find("img[title='Seeders']").First().Parent().Text().Trim()); + Int32 peers = Convert.ToInt32(qRow.Find("img[title='Leechers']").First().Parent().Text().Trim()) + seeders; + Uri CoverUrl = new Uri(SiteLink.TrimEnd('/') + dom.Find("div[id='poster'] > a > img").First().Attr("src").Trim()); + bool freeleech = qRow.Find("span[class='freeleech']").Length == 1 ? true : false; + bool qualityEncode = qRow.Find("img[class='approved']").Length == 1 ? true : false; + string grabs = qRow.Find("img[title='Snatches']").First().Parent().Text().Trim(); + if (String.IsNullOrWhiteSpace(sizeStr)) + { + string secondSizeStr = qRow.Find("div[class='details_title'] > strong:contains('(')").Last().Text().Trim(); + if (secondSizeStr.Length > 3 && secondSizeStr.Contains("(") && secondSizeStr.Contains(")")) + { sizeStr = secondSizeStr.Replace("(", "").Replace(")", "").Trim(); } + } + + var release = new ReleaseInfo(); + + release.Title = title; + release.Guid = guid; + release.Link = link; + release.PublishDate = pubDate; + release.Size = ReleaseInfo.GetBytes(sizeStr); + release.Description = release.Title; + release.Seeders = seeders; + release.Peers = peers; + release.MinimumRatio = 1; + release.MinimumSeedTime = 345600; + release.Category = 2000; + if (commentsPresent) { release.Comments = commentsLink; } + if (imdb_id > 0) { release.Imdb = imdb_id; } + + if (configFreeLeechOnly && !freeleech) + { + continue; //Skip release if user only wants FreeLeech + } + if (configQualityEncodeOnly && !qualityEncode) + { + continue; //Skip release if user only wants Quality Encode + } + + releases.Add(release); + } + + } + } + catch (Exception ex) + { + OnParseError(results.Content, ex); + } + + return releases; + } + } +} diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 7dec323e1..23b020ffe 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -255,6 +255,7 @@ + @@ -300,6 +301,7 @@ + diff --git a/src/Jackett/Models/IndexerConfig/ConfigurationDataBasicLoginWithFilter.cs b/src/Jackett/Models/IndexerConfig/ConfigurationDataBasicLoginWithFilter.cs new file mode 100644 index 000000000..10a029db2 --- /dev/null +++ b/src/Jackett/Models/IndexerConfig/ConfigurationDataBasicLoginWithFilter.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett.Models.IndexerConfig +{ + public class ConfigurationDataBasicLoginWithFilter : ConfigurationData + { + public StringItem Username { get; private set; } + public StringItem Password { get; private set; } + public DisplayItem FilterExample { get; private set; } + public StringItem FilterString { get; private set; } + + public ConfigurationDataBasicLoginWithFilter(string FilterInstructions) + { + Username = new StringItem { Name = "Username" }; + Password = new StringItem { Name = "Password" }; + FilterExample = new DisplayItem(FilterInstructions) + { + Name = "" + }; + FilterString = new StringItem { Name = "Filters (optional)" }; + } + + + } +}