diff --git a/src/NzbDrone.Core/Indexers/Definitions/Blutopia.cs b/src/NzbDrone.Core/Indexers/Definitions/Blutopia.cs new file mode 100644 index 000000000..11e67d878 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Blutopia.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Definitions.UNIT3D; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Blutopia : Unit3dBase + { + public override string Name => "Blutopia"; + public override string BaseUrl => "https://blutopia.xyz/"; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + + public Blutopia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + protected override IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvdbId + }, + MovieSearchParams = new List + { + MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies, "Movie"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TV, "TV Show"); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.MoviesOther, "FANRES"); + caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.MoviesOther, "Trailer"); + + return caps; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dApi.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dApi.cs new file mode 100644 index 000000000..310a42109 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dApi.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Indexers.Definitions.UNIT3D +{ + public class Unit3dResponse + { + public List Data { get; set; } + public Unit3dLinks Links { get; set; } + } + + public class Unit3dTorrent + { + public string Type { get; set; } + public string Id { get; set; } + public Unit3dTorrentAttributes Attributes { get; set; } + } + + public class Unit3dTorrentAttributes + { + public string Name { get; set; } + + [JsonProperty(PropertyName = "release_year")] + public int ReleaseYear { get; set; } + public string Category { get; set; } + public string Encode { get; set; } + public string Resolution { get; set; } + public long Size { get; set; } + + [JsonProperty(PropertyName = "num_file")] + public int Files { get; set; } + + [JsonProperty(PropertyName = "times_completed")] + public int Grabs { get; set; } + public int Seeders { get; set; } + public int Leechers { get; set; } + + [JsonProperty(PropertyName = "created_at")] + public string CreatedAt { get; set; } + + [JsonProperty(PropertyName = "download_link")] + public string DownloadLink { get; set; } + + [JsonProperty(PropertyName = "details_link")] + public string DetailsLink { get; set; } + + [JsonProperty(PropertyName = "imdb_id")] + public string ImdbId { get; set; } + + [JsonProperty(PropertyName = "tmdb_id")] + public string TmdbId { get; set; } + + [JsonProperty(PropertyName = "tvdb_id")] + public string TvdbId { get; set; } + + [JsonProperty(PropertyName = "igdb_id")] + public string IgdbId { get; set; } + + [JsonProperty(PropertyName = "mal_id")] + public string MalId { get; set; } + + [JsonProperty(PropertyName = "double_upload")] + public bool DoubleUpload { get; set; } + public bool Freeleech { get; set; } + public string Uploader { get; set; } + } + + public class Unit3dLinks + { + public string first { get; set; } + public string last { get; set; } + public string prev { get; set; } + public string next { get; set; } + public string self { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dBase.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dBase.cs new file mode 100644 index 000000000..285406f85 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Indexers.Definitions.UNIT3D +{ + public abstract class Unit3dBase : HttpIndexerBase + { + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override string BaseUrl => ""; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Unit3dBase(IHttpClient httpClient, + IEventAggregator eventAggregator, + IIndexerStatusService indexerStatusService, + IConfigService configService, + Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new Unit3dRequestGenerator() + { + Settings = Settings, + HttpClient = _httpClient, + Logger = _logger, + Capabilities = Capabilities, + BaseUrl = BaseUrl + }; + } + + public override IParseIndexerResponse GetParser() + { + return new Unit3dParser(Capabilities.Categories, BaseUrl); + } + + protected virtual IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities(); + + return caps; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dParser.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dParser.cs new file mode 100644 index 000000000..88e37d043 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dParser.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +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.UNIT3D +{ + public class Unit3dParser : IParseIndexerResponse + { + private readonly string _baseUrl; + private readonly IndexerCapabilitiesCategories _categories; + + protected virtual string TorrentUrl => _baseUrl + "torrents"; + + public Unit3dParser(IndexerCapabilitiesCategories categories, string baseUrl) + { + _categories = categories; + _baseUrl = baseUrl; + } + + public Action, DateTime?> CookiesUpdater { get; set; } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request"); + } + + if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value)) + { + throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}"); + } + + var jsonResponse = new HttpResponse(indexerResponse.HttpResponse); + + foreach (var row in jsonResponse.Resource.Data) + { + var details = row.Attributes.DetailsLink; + var link = row.Attributes.DownloadLink; + + var release = new TorrentInfo + { + Title = row.Attributes.Name, + DownloadUrl = link, + InfoHash = row.Id, + InfoUrl = details, + Guid = details, + Category = _categories.MapTrackerCatDescToNewznab(row.Attributes.Category), + PublishDate = DateTime.Parse(row.Attributes.CreatedAt, CultureInfo.InvariantCulture), + Size = row.Attributes.Size, + Files = row.Attributes.Files, + Grabs = row.Attributes.Grabs, + Seeders = row.Attributes.Seeders, + ImdbId = ParseUtil.GetImdbID(row.Attributes.ImdbId).GetValueOrDefault(), + TmdbId = row.Attributes.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.Attributes.TmdbId), + TvdbId = row.Attributes.TvdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.Attributes.TvdbId), + Peers = row.Attributes.Leechers + row.Attributes.Seeders, + DownloadVolumeFactor = row.Attributes.Freeleech ? 0 : 1, + UploadVolumeFactor = row.Attributes.DoubleUpload ? 2 : 1, + MinimumRatio = 1, + MinimumSeedTime = 172800, // 48 hours + }; + + torrentInfos.Add(release); + } + + // order by date + return torrentInfos.OrderByDescending(o => o.PublishDate).ToArray(); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dRequestGenerator.cs new file mode 100644 index 000000000..d49cfc335 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dRequestGenerator.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.Definitions.UNIT3D +{ + public class Unit3dRequestGenerator : IIndexerRequestGenerator + { + public Unit3dSettings Settings { get; set; } + public string BaseUrl { get; set; } + + public IHttpClient HttpClient { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public Logger Logger { get; set; } + + protected virtual string SearchUrl => BaseUrl + "api/torrents/filter"; + protected virtual bool ImdbInTags => false; + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + + if (searchCriteria.ImdbId != null) + { + parameters.Add("imdb", searchCriteria.ImdbId); + } + + if (searchCriteria.TmdbId > 0) + { + parameters.Add("tmdb", searchCriteria.TmdbId.ToString()); + } + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories); + + if (searchCriteria.ImdbId != null) + { + parameters.Add("imdb", searchCriteria.ImdbId); + } + + if (searchCriteria.TvdbId > 0) + { + parameters.Add("tmdb", searchCriteria.TvdbId.ToString()); + } + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var parameters = GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); + + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(parameters)); + return pageableRequests; + } + + private IEnumerable GetRequest(List> searchParameters) + { + var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString(); + + var request = new IndexerRequest(searchUrl, HttpAccept.Json); + + yield return request; + } + + private List> GetBasicSearchParameters(string searchTerm, int[] categories) + { + var searchString = searchTerm; + + var qc = new List> + { + { "api_token", Settings.ApiKey } + }; + + if (!string.IsNullOrWhiteSpace(searchString)) + { + qc.Add("name", searchString); + } + + if (categories != null) + { + foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories)) + { + qc.Add("categories[]", cat); + } + } + + return qc; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs new file mode 100644 index 000000000..75f6fb25e --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/UNIT3D/Unit3dSettings.cs @@ -0,0 +1,32 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions.UNIT3D +{ + public class Unit3dSettingsValidator : AbstractValidator + { + public Unit3dSettingsValidator() + { + RuleFor(c => c.ApiKey).NotEmpty(); + } + } + + public class Unit3dSettings : IProviderConfig + { + private static readonly Unit3dSettingsValidator Validator = new Unit3dSettingsValidator(); + + public Unit3dSettings() + { + } + + [FieldDefinition(1, Label = "Api Key", HelpText = "Api key generated in My Security", Privacy = PrivacyLevel.ApiKey)] + public string ApiKey { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}