From bf0c095d558aab1f94324a4046ff67f5039f1303 Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 9 Mar 2021 21:15:11 -0500 Subject: [PATCH] New: Indexer - PrivateHD --- .../Indexers/Definitions/Avistaz/Avistaz.cs | 117 +++++++++++++++ .../Definitions/Avistaz/AvistazApi.cs | 72 +++++++++ .../Definitions/Avistaz/AvistazParser.cs | 127 ++++++++++++++++ .../Avistaz/AvistazRequestGenerator.cs | 139 ++++++++++++++++++ .../Definitions/Avistaz/AvistazSettings.cs | 42 ++++++ .../Indexers/Definitions/PrivateHD.cs | 63 ++++++++ .../Indexers/IndexerRepository.cs | 6 + 7 files changed, 566 insertions(+) create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Avistaz/Avistaz.cs create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParser.cs create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs create mode 100644 src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs create mode 100644 src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/Avistaz.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/Avistaz.cs new file mode 100644 index 000000000..afecb2393 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/Avistaz.cs @@ -0,0 +1,117 @@ +using System; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Indexers.Definitions.Avistaz +{ + public abstract class Avistaz : HttpIndexerBase + { + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override string BaseUrl => ""; + protected virtual string LoginUrl => BaseUrl + "api/v1/jackett/auth"; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + public override IndexerCapabilities Capabilities => SetCapabilities(); + private IIndexerRepository _indexerRepository; + + public Avistaz(IIndexerRepository indexerRepository, + IHttpClient httpClient, + IIndexerStatusService indexerStatusService, + IConfigService configService, + Logger logger) + : base(httpClient, indexerStatusService, configService, logger) + { + _indexerRepository = indexerRepository; + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new AvistazRequestGenerator() + { + Settings = Settings, + HttpClient = _httpClient, + Logger = _logger, + Capabilities = Capabilities, + BaseUrl = BaseUrl + }; + } + + public override IParseIndexerResponse GetParser() + { + return new AvistazParser(Settings, Capabilities, BaseUrl); + } + + protected virtual IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities(); + + return caps; + } + + protected override void DoLogin() + { + Settings.Token = GetToken(); + + if (Definition.Id > 0) + { + _indexerRepository.UpdateSettings((IndexerDefinition)Definition); + } + + _logger.Debug("Avistaz authentication succeeded."); + } + + protected override bool CheckIfLoginNeeded(HttpResponse response) + { + if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed) + { + return true; + } + + return false; + } + + protected override ValidationFailure TestConnection() + { + try + { + GetToken(); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to indexer"); + + return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details"); + } + + return null; + } + + private string GetToken() + { + var requestBuilder = new HttpRequestBuilder(LoginUrl) + { + LogResponseContent = true + }; + + requestBuilder.Method = HttpMethod.POST; + requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15); + + var authLoginRequest = requestBuilder + .AddFormParameter("username", Settings.Username) + .AddFormParameter("password", Settings.Password) + .AddFormParameter("pid", Settings.Pid.Trim()) + .SetHeader("Content-Type", "application/json") + .Accept(HttpAccept.Json) + .Build(); + + var response = _httpClient.Post(authLoginRequest); + var token = response.Resource.Token; + + return token; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs new file mode 100644 index 000000000..dfce16ed8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Indexers.Definitions.Avistaz +{ + public class AvistazRelease + { + public string Url { get; set; } + public string Download { get; set; } + + [JsonProperty(PropertyName = "movie_tv")] + public AvistazIdInfo MovieTvinfo { get; set; } + + [JsonProperty(PropertyName = "created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty(PropertyName = "file_name")] + public string FileName { get; set; } + + [JsonProperty(PropertyName = "info_hash")] + public string InfoHash { get; set; } + public int Leech { get; set; } + public int Completed { get; set; } + public int Seed { get; set; } + + [JsonProperty(PropertyName = "file_size")] + public int FileSize { get; set; } + + [JsonProperty(PropertyName = "file_count")] + public int FileCount { get; set; } + + [JsonProperty(PropertyName = "download_multiply")] + public double? DownloadMultiply { get; set; } + + [JsonProperty(PropertyName = "upload_multiply")] + public double? UploadMultiply { get; set; } + + [JsonProperty(PropertyName = "video_quality")] + public string VideoQuality { get; set; } + public string Type { get; set; } + } + + public class AvistazResponse + { + public string Status { get; set; } + public List Data { get; set; } + } + + public class AvistazIdInfo + { + public int Tmdb { get; set; } + public int Tvdb { get; set; } + public string Imdb { get; set; } + public string Title { get; set; } + + [JsonProperty(PropertyName = "tv_episode")] + public string TvEpisode { get; set; } + + [JsonProperty(PropertyName = "tv_season")] + public string TVSeason { get; set; } + + [JsonProperty(PropertyName = "tv_full_season")] + public bool TVFullSeason { get; set; } + } + + public class AvistazAuthResponse + { + public string Token { get; set; } + public string Expiry { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParser.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParser.cs new file mode 100644 index 000000000..a49cf9daa --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParser.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Indexers.Definitions.Avistaz +{ + public class AvistazParser : IParseIndexerResponse + { + private readonly AvistazSettings _settings; + private readonly IndexerCapabilities _capabilities; + private readonly string _baseUrl; + private readonly HashSet _hdResolutions = new HashSet { "1080p", "1080i", "720p" }; + + public AvistazParser(AvistazSettings settings, IndexerCapabilities capabilities, string baseUrl) + { + _settings = settings; + _capabilities = capabilities; + _baseUrl = baseUrl; + } + + public Action, DateTime?> CookiesUpdater { get; set; } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + // Remove cookie cache + CookiesUpdater(null, null); + + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request"); + } + + if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + // Remove cookie cache + CookiesUpdater(null, null); + + throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}"); + } + + var jsonResponse = new HttpResponse(indexerResponse.HttpResponse); + if (jsonResponse.Resource.Status != "success" || + jsonResponse.Resource.Status.IsNullOrWhiteSpace()) + { + return torrentInfos; + } + + foreach (var row in jsonResponse.Resource.Data) + { + var details = row.Url; + var link = row.Download; + + var cats = ParseCategories(row); + + var release = new TorrentInfo + { + Title = row.FileName, + DownloadUrl = link, + InfoHash = row.InfoHash, + InfoUrl = details, + Guid = details, + Category = cats, + PublishDate = row.CreatedAt, + Size = row.FileSize, + Files = row.FileCount, + Grabs = row.Completed, + Seeders = row.Seed, + Peers = row.Leech + row.Seed, + ImdbId = ParseUtil.GetImdbID(row.MovieTvinfo.Imdb).Value, + TvdbId = row.MovieTvinfo.Tvdb, + TmdbId = row.MovieTvinfo.Tmdb, + DownloadVolumeFactor = row.DownloadMultiply, + UploadVolumeFactor = row.UploadMultiply, + MinimumRatio = 1, + MinimumSeedTime = 172800 // 48 hours + }; + + torrentInfos.Add(release); + } + + // order by date + return torrentInfos.OrderByDescending(o => o.PublishDate).ToArray(); + } + + // hook to adjust category parsing + protected virtual List ParseCategories(AvistazRelease row) + { + var cats = new List(); + var resolution = row.VideoQuality; + + switch (row.Type) + { + case "Movie": + cats.Add(resolution switch + { + var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD, + "2160p" => NewznabStandardCategory.MoviesUHD, + _ => NewznabStandardCategory.MoviesSD + }); + break; + case "TV-Show": + cats.Add(resolution switch + { + var res when _hdResolutions.Contains(res) => NewznabStandardCategory.TVHD, + "2160p" => NewznabStandardCategory.TVUHD, + _ => NewznabStandardCategory.TVSD + }); + break; + case "Music": + cats.Add(NewznabStandardCategory.Audio); + break; + default: + throw new Exception("Error parsing category!"); + } + + return cats; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs new file mode 100644 index 000000000..ef54c66d8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.Definitions.Avistaz +{ + public class AvistazRequestGenerator : IIndexerRequestGenerator + { + public AvistazSettings Settings { get; set; } + public string BaseUrl { get; set; } + + public IDictionary AuthCookieCache { get; set; } + public IHttpClient HttpClient { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public Logger Logger { get; set; } + + protected virtual string SearchUrl => BaseUrl + "api/v1/jackett/torrents"; + protected virtual bool ImdbInTags => false; + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + + // hook to adjust the search category + protected virtual List> GetBasicSearchParameters(int[] categories) + { + var categoryMapping = Capabilities.Categories.MapTorznabCapsToTrackers(categories).Distinct().ToList(); + var qc = new List> // NameValueCollection don't support cat[]=19&cat[]=6 + { + { "in", "1" }, + { "type", categoryMapping.Any() ? categoryMapping.First() : "0" } + }; + + // resolution filter to improve the search + if (!categories.Contains(NewznabStandardCategory.Movies.Id) && !categories.Contains(NewznabStandardCategory.TV.Id) && + !categories.Contains(NewznabStandardCategory.Audio.Id)) + { + if (categories.Contains(NewznabStandardCategory.MoviesUHD.Id) || categories.Contains(NewznabStandardCategory.TVUHD.Id)) + { + qc.Add("video_quality[]", "6"); // 2160p + } + + if (categories.Contains(NewznabStandardCategory.MoviesHD.Id) || categories.Contains(NewznabStandardCategory.TVHD.Id)) + { + qc.Add("video_quality[]", "2"); // 720p + qc.Add("video_quality[]", "7"); // 1080i + qc.Add("video_quality[]", "3"); // 1080p + } + + if (categories.Contains(NewznabStandardCategory.MoviesSD.Id) || categories.Contains(NewznabStandardCategory.TVSD.Id)) + { + qc.Add("video_quality[]", "1"); // SD + } + } + + return qc; + } + + private IEnumerable GetRequest(List> searchParameters) + { + var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString(); + + var request = new IndexerRequest(searchUrl, HttpAccept.Json); + request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}"); + + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.Categories); + + if (searchCriteria.ImdbId != null) + { + parameters.Add("imdb", searchCriteria.ImdbId); + } + else + { + parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim()); + } + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.Categories); + + parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim()); + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.Categories); + + if (searchCriteria.ImdbId != null) + { + parameters.Add("imdb", searchCriteria.ImdbId); + } + else + { + parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedTvSearchString).Trim()); + } + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + throw new NotImplementedException(); + } + + // hook to adjust the search term + protected virtual string GetSearchTerm(string term) => term; + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.Categories); + + parameters.Add("search", GetSearchTerm(searchCriteria.SanitizedSearchTerm).Trim()); + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs new file mode 100644 index 000000000..e02530492 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs @@ -0,0 +1,42 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions.Avistaz +{ + public class AvistazSettingsValidator : AbstractValidator + { + public AvistazSettingsValidator() + { + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class AvistazSettings : IProviderConfig + { + private static readonly AvistazSettingsValidator Validator = new AvistazSettingsValidator(); + + public AvistazSettings() + { + Token = ""; + } + + public string Token { get; set; } + + [FieldDefinition(1, Label = "Username", HelpText = "Username", Privacy = PrivacyLevel.UserName)] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Password", Privacy = PrivacyLevel.Password)] + public string Password { get; set; } + + [FieldDefinition(3, Label = "PID", HelpText = "PID from My Account or My Profile page")] + public string Pid { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs b/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs new file mode 100644 index 000000000..0eb30fd12 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Definitions.Avistaz; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class PrivateHD : Avistaz.Avistaz + { + public override string Name => "PrivateHD"; + public override string BaseUrl => "https://privatehd.to/"; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + + public PrivateHD(IIndexerRepository indexerRepository, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(indexerRepository, httpClient, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new AvistazRequestGenerator() + { + Settings = Settings, + HttpClient = _httpClient, + Logger = _logger, + Capabilities = Capabilities, + BaseUrl = BaseUrl + }; + } + + protected override 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 + } + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies); + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesUHD); + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesHD); + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVUHD); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVHD); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVSD); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Audio); + + return caps; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/IndexerRepository.cs b/src/NzbDrone.Core/Indexers/IndexerRepository.cs index ace52631d..e9544810e 100644 --- a/src/NzbDrone.Core/Indexers/IndexerRepository.cs +++ b/src/NzbDrone.Core/Indexers/IndexerRepository.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Core.Indexers { public interface IIndexerRepository : IProviderRepository { + void UpdateSettings(IndexerDefinition model); } public class IndexerRepository : ProviderRepository, IIndexerRepository @@ -14,5 +15,10 @@ namespace NzbDrone.Core.Indexers : base(database, eventAggregator) { } + + public void UpdateSettings(IndexerDefinition model) + { + SetFields(model, m => m.Settings); + } } }