diff --git a/src/NzbDrone.Core/Indexers/Definitions/Milkie.cs b/src/NzbDrone.Core/Indexers/Definitions/Milkie.cs new file mode 100644 index 000000000..420396218 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/Milkie.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using FluentValidation; +using Newtonsoft.Json; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class Milkie : HttpIndexerBase + { + public override string Name => "Milkie"; + + public override string BaseUrl => "https://milkie.cc/"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Private; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public Milkie(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new MilkieRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl }; + } + + public override IParseIndexerResponse GetParser() + { + return new MilkieParser(Settings, Capabilities.Categories, BaseUrl); + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Movies"); + caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TV, "TV"); + caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.Audio, "Music"); + caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.Books, "Ebook"); + caps.Categories.AddCategoryMapping("6", NewznabStandardCategory.PC, "Apps"); + caps.Categories.AddCategoryMapping("7", NewznabStandardCategory.XXX, "Adult"); + return caps; + } + } + + public class MilkieRequestGenerator : IIndexerRequestGenerator + { + public MilkieSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public string BaseUrl { get; set; } + + public MilkieRequestGenerator() + { + } + + private IEnumerable GetPagedRequests(string term, int[] categories, string imdbId = null) + { + var searchUrl = BaseUrl + "api/v1/torrents"; + + var qc = new NameValueCollection + { + { "ps", "100" } + }; + + if (!string.IsNullOrWhiteSpace(term)) + { + qc.Add("query", term); + } + + if (categories != null && categories.Length > 0) + { + qc.Add("categories", string.Join(",", Capabilities.Categories.MapTorznabCapsToTrackers(categories))); + } + + searchUrl = searchUrl + "?" + qc.GetQueryString(); + + var request = new IndexerRequest(searchUrl, HttpAccept.Json); + + request.HttpRequest.Headers.Add("x-milkie-auth", Settings.ApiKey); + + 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 MilkieParser : IParseIndexerResponse + { + private readonly MilkieSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + private readonly string _baseUrl; + + public MilkieParser(MilkieSettings settings, IndexerCapabilitiesCategories categories, string baseUrl) + { + _settings = settings; + _categories = categories; + _baseUrl = baseUrl; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + var response = JsonConvert.DeserializeObject(indexerResponse.Content); + + var dlQueryParams = new NameValueCollection + { + { "key", _settings.ApiKey } + }; + + foreach (var torrent in response.Torrents) + { + var torrentUrl = _baseUrl + "api/v1/torrents"; + var link = $"{torrentUrl}/{torrent.Id}/torrent?{dlQueryParams.GetQueryString()}"; + var details = $"{_baseUrl}browse/{torrent.Id}"; + var publishDate = DateTimeUtil.FromUnknown(torrent.CreatedAt); + + var release = new TorrentInfo + { + Title = torrent.ReleaseName, + DownloadUrl = link, + InfoUrl = details, + Guid = details, + PublishDate = publishDate, + Category = _categories.MapTrackerCatToNewznab(torrent.Category.ToString()), + Size = torrent.Size, + Seeders = torrent.Seeders, + Peers = torrent.Seeders + torrent.PartialSeeders + torrent.Leechers, + Grabs = torrent.Downloaded, + UploadVolumeFactor = 1, + DownloadVolumeFactor = 0, + MinimumRatio = 1, + MinimumSeedTime = 172800 // 48 hours + }; + + torrentInfos.Add(release); + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class MilkieSettingsValidator : AbstractValidator + { + public MilkieSettingsValidator() + { + RuleFor(c => c.ApiKey).NotEmpty(); + } + } + + public class MilkieSettings : IProviderConfig + { + private static readonly MilkieSettingsValidator Validator = new MilkieSettingsValidator(); + + public MilkieSettings() + { + ApiKey = ""; + } + + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Apikey", HelpText = "Site ApiKey", Privacy = PrivacyLevel.ApiKey)] + public string ApiKey { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } + + public class MilkieResponse + { + public int Hits { get; set; } + public int Took { get; set; } + public MilkieTorrent[] Torrents { get; set; } + } + + public class MilkieTorrent + { + public string Id { get; set; } + public string ReleaseName { get; set; } + public int Category { get; set; } + public int Downloaded { get; set; } + public int Seeders { get; set; } + public int PartialSeeders { get; set; } + public int Leechers { get; set; } + public long Size { get; set; } + public string CreatedAt { get; set; } + } +}