diff --git a/src/NzbDrone.Core/Indexers/Definitions/TorrentLeech.cs b/src/NzbDrone.Core/Indexers/Definitions/TorrentLeech.cs new file mode 100644 index 000000000..d6bd53ad4 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/TorrentLeech.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using FluentValidation; +using Newtonsoft.Json; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class TorrentLeech : HttpIndexerBase + { + public override string Name => "TorrentLeech"; + + public override string BaseUrl => "https://www.torrentleech.org/"; + private string LoginUrl => BaseUrl + "user/account/login/"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public TorrentLeech(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new TorrentLeechRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl }; + } + + public override IParseIndexerResponse GetParser() + { + return new TorrentLeechParser(Settings, Capabilities.Categories, BaseUrl); + } + + protected override void DoLogin() + { + var requestBuilder = new HttpRequestBuilder(LoginUrl) + { + LogResponseContent = true + }; + + requestBuilder.Method = HttpMethod.POST; + requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15); + + var cookies = Cookies; + + Cookies = null; + var authLoginRequest = requestBuilder + .AddFormParameter("username", Settings.Username) + .AddFormParameter("password", Settings.Password) + .SetHeader("Content-Type", "multipart/form-data") + .Build(); + + var response = _httpClient.Execute(authLoginRequest); + + cookies = response.GetCookies(); + Cookies = cookies; + UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30)); + + _logger.Debug("TorrentLeech authentication succeeded."); + } + + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) + { + if (httpResponse.Content.Contains("/user/account/login")) + { + return true; + } + + return false; + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId + }, + MovieSearchParams = new List + { + MovieSearchParam.Q, MovieSearchParam.ImdbId + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Movies"); + caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.MoviesSD, "Movies Cam"); + caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.MoviesSD, "Movies TS/TC"); + caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.MoviesSD, "Movies DVDRip/DVDScreener"); + caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.MoviesDVD, "Movies DVD-R"); + caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.MoviesBluRay, "Movies Bluray"); + caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.MoviesHD, "Movies BlurayRip"); + caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.Movies, "Movies Boxsets"); + caps.Categories.AddCategoryMapping(29, NewznabStandardCategory.TVDocumentary, "Documentaries"); + caps.Categories.AddCategoryMapping(47, NewznabStandardCategory.MoviesUHD, "Movies 4K"); + caps.Categories.AddCategoryMapping(36, NewznabStandardCategory.MoviesForeign, "Movies Foreign"); + caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.MoviesWEBDL, "Movies WEBRip"); + caps.Categories.AddCategoryMapping(43, NewznabStandardCategory.MoviesHD, "Movies HDRip"); + + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping(26, NewznabStandardCategory.TVSD, "TV Episodes"); + caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TV, "TV Boxsets"); + caps.Categories.AddCategoryMapping(32, NewznabStandardCategory.TVHD, "TV Episodes HD"); + caps.Categories.AddCategoryMapping(44, NewznabStandardCategory.TVForeign, "TV Foreign"); + + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.PCGames, "Games PC"); + caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.ConsoleXBox, "Games XBOX"); + caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.ConsoleXBox360, "Games XBOX360"); + caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.ConsoleXBoxOne, "Games XBOXONE"); + caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.ConsolePS3, "Games PS2"); + caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.ConsolePS3, "Games Mac"); + caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.ConsolePSP, "Games PSP"); + caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.ConsoleWii, "Games Wii"); + caps.Categories.AddCategoryMapping(30, NewznabStandardCategory.ConsoleNDS, "Games Nintendo DS"); + caps.Categories.AddCategoryMapping(39, NewznabStandardCategory.ConsolePS4, "Games PS4"); + caps.Categories.AddCategoryMapping(42, NewznabStandardCategory.PCMac, "Games Mac"); + caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.ConsoleOther, "Games Nintendo Switch"); + + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Audio, "Music"); + caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.AudioVideo, "Music videos"); + caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.Audio, "Audio"); + + caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TV, "Animation"); + caps.Categories.AddCategoryMapping(34, NewznabStandardCategory.TVAnime, "TV Anime"); + caps.Categories.AddCategoryMapping(35, NewznabStandardCategory.TV, "TV Cartoons"); + + caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Books, "Books"); + caps.Categories.AddCategoryMapping(45, NewznabStandardCategory.BooksEBook, "Books EBooks"); + caps.Categories.AddCategoryMapping(46, NewznabStandardCategory.BooksComics, "Books Comics"); + + caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.PC, "Apps"); + caps.Categories.AddCategoryMapping(23, NewznabStandardCategory.PCISO, "PC ISO"); + caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.PCMac, "PC Mac"); + caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.PCMobileOther, "PC Mobile"); + caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.PC0day, "PC 0-day"); + caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.Other, "Education"); + + return caps; + } + } + + public class TorrentLeechRequestGenerator : IIndexerRequestGenerator + { + public TorrentLeechSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public string BaseUrl { get; set; } + + public TorrentLeechRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, int[] categories, string imdbId = null) + { + var searchString = Regex.Replace(term, @"(^|\s)-", " "); + + var searchUrl = BaseUrl + "torrents/browse/list/"; + + if (Settings.FreeLeechOnly) + { + searchUrl += "facets/tags%3AFREELEECH/"; + } + + if (imdbId.IsNotNullOrWhiteSpace()) + { + searchUrl += "imdbID/" + imdbId + "/"; + } + else if (!string.IsNullOrWhiteSpace(searchString)) + { + searchUrl += "exact/1/query/" + WebUtility.UrlEncode(searchString) + "/"; + } + + var cats = Capabilities.Categories.MapTorznabCapsToTrackers(categories); + if (cats.Count > 0) + { + searchUrl += "categories/" + string.Join(",", cats); + } + else + { + searchUrl += "newfilter/2"; // include 0day and music + } + + var request = new IndexerRequest(searchUrl, HttpAccept.Rss); + + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SearchTerm), searchCriteria.Categories, searchCriteria.ImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SearchTerm), searchCriteria.Categories, searchCriteria.ImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class TorrentLeechParser : IParseIndexerResponse + { + private readonly TorrentLeechSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + private readonly string _baseUrl; + + public TorrentLeechParser(TorrentLeechSettings settings, IndexerCapabilitiesCategories categories, string baseUrl) + { + _settings = settings; + _categories = categories; + _baseUrl = baseUrl; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + var rows = JsonConvert.DeserializeObject(indexerResponse.Content).torrentList; + foreach (var row in rows ?? Enumerable.Empty()) + { + var title = row.name.ToString(); + + var torrentId = row.fid.ToString(); + var details = new Uri(_baseUrl + "torrent/" + torrentId); + var link = new Uri(_baseUrl + "download/" + torrentId + "/" + row.filename); + var publishDate = DateTime.ParseExact(row.addedTimestamp.ToString(), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + var seeders = (int)row.seeders; + var leechers = (int)row.leechers; + var grabs = (int)row.completed; + var size = (long)row.size; + var cats = _categories.MapTrackerCatToNewznab(((int)row.categoryID).ToString()); + var imdb = (string)row.imdbID; + var imdbId = 0; + if (imdb.Length > 2) + { + imdbId = int.Parse(imdb.Substring(2)); + } + + var release = new TorrentInfo + { + Title = title, + InfoUrl = details.AbsoluteUri, + Guid = details.AbsoluteUri, + DownloadUrl = link.AbsoluteUri, + PublishDate = publishDate, + Category = cats, + Size = size, + Grabs = grabs, + Seeders = seeders, + Peers = seeders + leechers, + ImdbId = imdbId + }; + + torrentInfos.Add(release); + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class TorrentLeechSettingsValidator : AbstractValidator + { + public TorrentLeechSettingsValidator() + { + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class TorrentLeechSettings : IProviderConfig + { + private static readonly TorrentLeechSettingsValidator Validator = new TorrentLeechSettingsValidator(); + + public TorrentLeechSettings() + { + Username = ""; + Password = ""; + } + + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Username", Advanced = true, HelpText = "Site username")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Site password", Privacy = PrivacyLevel.Password)] + public string Password { get; set; } + + [FieldDefinition(3, Label = "FreeLeech Only", Type = FieldType.Checkbox, Advanced = true, HelpText = "Search Freeleech torrents only")] + public bool FreeLeechOnly { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}