diff --git a/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs b/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs new file mode 100644 index 000000000..db6101f04 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AngleSharp.Html.Parser; +using FluentValidation; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Anthelion : TorrentIndexerBase + { + public override string Name => "Anthelion"; + public override string BaseUrl => "https://anthelion.me/"; + private string LoginUrl => BaseUrl + "login.php"; + public override string Description => "A movies tracker"; + public override string Language => "en-us"; + public override Encoding Encoding => Encoding.UTF8; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Anthelion(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new AnthelionRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl }; + } + + public override IParseIndexerResponse GetParser() + { + return new AnthelionParser(Settings, Capabilities.Categories, BaseUrl); + } + + protected override async Task 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) + .AddFormParameter("keeplogged", "1") + .AddFormParameter("login", "Log+In!") + .SetHeader("Content-Type", "multipart/form-data") + .Build(); + + var response = await ExecuteAuth(authLoginRequest); + + if (!response.Content.Contains("logout.php")) + { + throw new IndexerAuthException("Anthelion Auth Failed"); + } + + cookies = response.GetCookies(); + UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30)); + + _logger.Debug("Anthelion authentication succeeded."); + } + + protected override bool CheckIfLoginNeeded(HttpResponse httpResponse) + { + if (!httpResponse.Content.Contains("logout.php")) + { + return true; + } + + return false; + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature"); + caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.Movies, "Film/Short"); + caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TV, "TV/Miniseries"); + caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Other, "Other"); + + return caps; + } + } + + public class AnthelionRequestGenerator : IIndexerRequestGenerator + { + public AnthelionSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public string BaseUrl { get; set; } + + public AnthelionRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, int[] categories, string imdbId = null) + { + var searchUrl = string.Format("{0}/torrents.php", BaseUrl.TrimEnd('/')); + + // TODO: IMDB search is available but it requires to parse the details page + var qc = new NameValueCollection + { + { "order_by", "time" }, + { "order_way", "desc" }, + { "action", "basic" }, + { "searchsubmit", "1" }, + { "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term } + }; + + var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories); + + foreach (var cat in catList) + { + qc.Add($"filter_cat[{cat}]", "1"); + } + + searchUrl = searchUrl + "?" + qc.GetQueryString(); + + var request = new IndexerRequest(searchUrl, HttpAccept.Html); + + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories)); + + return pageableRequests; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class AnthelionParser : IParseIndexerResponse + { + private readonly AnthelionSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + private readonly string _baseUrl; + + public AnthelionParser(AnthelionSettings settings, IndexerCapabilitiesCategories categories, string baseurl) + { + _settings = settings; + _categories = categories; + _baseUrl = baseurl; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + var parser = new HtmlParser(); + var doc = parser.ParseDocument(indexerResponse.Content); + var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent"); + foreach (var row in rows) + { + var qDetailsLink = row.QuerySelector("a.torrent_name"); + var year = qDetailsLink.NextSibling.TextContent.Replace("[", "").Replace("]", "").Trim(); + var tags = row.QuerySelector("div.torrent_info").FirstChild.TextContent.Replace(" / ", " ").Trim(); + var title = $"{qDetailsLink.TextContent} {year} {tags}"; + var description = row.QuerySelector("div.tags").TextContent.Trim(); + var details = _baseUrl + qDetailsLink.GetAttribute("href"); + var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last(); + var link = _baseUrl + "torrents.php?action=download&id=" + torrentId; + var posterStr = qDetailsLink.GetAttribute("data-cover"); + var poster = !string.IsNullOrWhiteSpace(posterStr) ? new Uri(qDetailsLink.GetAttribute("data-cover")) : null; + + var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent); + var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent); + var size = ReleaseInfo.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent); + var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent); + var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent); + var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent); + + var dlVolumeFactor = row.QuerySelector("strong.tl_free") != null ? 0 : 1; + + var cat = row.QuerySelector("td.cats_col > div").GetAttribute("class").Replace("tooltip cats_", ""); + var category = new List { + cat switch + { + "featurefilm" => NewznabStandardCategory.Movies, + "shortfilm" => NewznabStandardCategory.Movies, + "miniseries" => NewznabStandardCategory.TV, + "other" => NewznabStandardCategory.Other, + _ => throw new Exception($"Unknown category: {cat}") + } + }; + + // TODO: TMDb is also available + var qImdb = row.QuerySelector("a[href^=\"https://www.imdb.com\"]"); + var imdb = qImdb != null ? ParseUtil.GetImdbID(qImdb.GetAttribute("href").Split('/').Last()) : null; + + var release = new TorrentInfo + { + MinimumRatio = 1, + MinimumSeedTime = 259200, + Description = description, + Title = title, + PublishDate = publishDate, + Categories = category, + DownloadUrl = link, + InfoUrl = details, + Guid = link, + ImdbId = imdb.GetValueOrDefault(), + Seeders = seeders, + Peers = leechers + seeders, + Size = size, + Grabs = grabs, + Files = files, + DownloadVolumeFactor = dlVolumeFactor, + UploadVolumeFactor = 1 + }; + + torrentInfos.Add(release); + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class AnthelionSettingsValidator : AbstractValidator + { + public AnthelionSettingsValidator() + { + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class AnthelionSettings : IProviderConfig + { + private static readonly AnthelionSettingsValidator Validator = new AnthelionSettingsValidator(); + + public AnthelionSettings() + { + Username = ""; + Password = ""; + } + + [FieldDefinition(1, Label = "Username", Advanced = false, HelpText = "Site Username")] + public string Username { get; set; } + + [FieldDefinition(1, Label = "Password", Advanced = false, HelpText = "Site Password")] + public string Password { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}