From 2ed0738b301467b23b42c46a32a5062dd132c5fe Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Mon, 9 Jan 2017 08:30:15 -0500 Subject: [PATCH] Added PassThePopcorn indexer (#64) * Added PassThePopcorn indexe * Added checkboxes for Gold and Checked torrents thanks @evulhotdog * Sorted movie releases by golden -> checked -> upload date * Refactored logic. * Added flags at the end of torrent name for golden / approved * Updated PTP GOlden to be the popcorn emoji * Revert "Updated PTP GOlden to be the popcorn emoji" This reverts commit c6cf0f5fc520b84f43548afe558c0797e0866fbf. * Opps, hopefully build will pass now. * Move PTP props to new subclass. --- .../Indexers/PassThePopcorn/PassThePopcorn.cs | 30 ++++ .../PassThePopcorn/PassThePopcornApi.cs | 59 +++++++ .../PassThePopcorn/PassThePopcornInfo.cs | 15 ++ .../PassThePopcorn/PassThePopcornParser.cs | 144 ++++++++++++++++++ .../PassThePopcornRequestGenerator.cs | 67 ++++++++ .../PassThePopcorn/PassThePopcornSettings.cs | 52 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 6 + 7 files changed, 373 insertions(+) create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs create mode 100644 src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs new file mode 100644 index 000000000..301894f57 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcorn : HttpIndexerBase + { + public override string Name => "PassThePopcorn"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + + public PassThePopcorn(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new PassThePopcornRequestGenerator() { Settings = Settings }; + } + + public override IParseIndexerResponse GetParser() + { + return new PassThePopcornParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs new file mode 100644 index 000000000..9d7c93ea8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs @@ -0,0 +1,59 @@ +using System; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class Director + { + public string Name { get; set; } + public string Id { get; set; } + } + + public class Torrent + { + public int Id { get; set; } + public string Quality { get; set; } + public string Source { get; set; } + public string Container { get; set; } + public string Codec { get; set; } + public string Resolution { get; set; } + public bool Scene { get; set; } + public string Size { get; set; } + public DateTime UploadTime { get; set; } + public string RemasterTitle { get; set; } + public string Snatched { get; set; } + public string Seeders { get; set; } + public string Leechers { get; set; } + public string ReleaseName { get; set; } + public bool Checked { get; set; } + public bool GoldenPopcorn { get; set; } + } + + public class Movie + { + public string GroupId { get; set; } + public string Title { get; set; } + public string Year { get; set; } + public string Cover { get; set; } + public List Tags { get; set; } + public List Directors { get; set; } + public string ImdbId { get; set; } + public int TotalLeechers { get; set; } + public int TotalSeeders { get; set; } + public int TotalSnatched { get; set; } + public long MaxSize { get; set; } + public string LastUploadTime { get; set; } + public List Torrents { get; set; } + } + + public class PassThePopcornResponse + { + public string TotalResults { get; set; } + public List Movies { get; set; } + public string Page { get; set; } + public string AuthKey { get; set; } + public string PassKey { get; set; } + } + +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs new file mode 100644 index 000000000..3fec7eaff --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs @@ -0,0 +1,15 @@ +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornInfo : TorrentInfo + { + public bool? Golden { get; set; } + public bool? Scene { get; set; } + public bool? Approved { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs new file mode 100644 index 000000000..ef1ad3909 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; +using System; +using System.Linq; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornParser : IParseIndexerResponse + { + private readonly PassThePopcornSettings _settings; + + public PassThePopcornParser(PassThePopcornSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, + "Unexpected response status {0} code from API request", + indexerResponse.HttpResponse.StatusCode); + } + + var jsonResponse = JsonConvert.DeserializeObject(indexerResponse.Content); + + var responseData = jsonResponse.Movies; + if (responseData == null) + { + throw new IndexerException(indexerResponse, + "Indexer API call response missing result data"); + } + + foreach (var result in responseData) + { + foreach (var torrent in result.Torrents) + { + var id = torrent.Id; + var title = torrent.ReleaseName; + + if (torrent.GoldenPopcorn) + { + title = $"{title} 🍿"; + } + + if (torrent.Checked) + { + title = $"{title} ✔"; + } + + //if (IsPropertyExist(torrent, "RemasterTitle")) + //{ + // if (torrent.RemasterTitle != null) + // { + // title = $"{title} - {torrent.RemasterTitle}"; + // } + //} + + // Only add approved torrents + if (_settings.Approved && torrent.Checked) + { + torrentInfos.Add(new PassThePopcornInfo() + { + Guid = string.Format("PassThePopcorn-{0}", id), + Title = title, + Size = long.Parse(torrent.Size), + DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey), + InfoUrl = GetInfoUrl(result.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.UploadTime.ToUniversalTime(), + Golden = torrent.GoldenPopcorn, + Scene = torrent.Scene, + Approved = torrent.Checked + }); + } + // Add all torrents + else if (!_settings.Approved) + { + torrentInfos.Add(new PassThePopcornInfo() + { + Guid = string.Format("PassThePopcorn-{0}", id), + Title = title, + Size = long.Parse(torrent.Size), + DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey), + InfoUrl = GetInfoUrl(result.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.UploadTime.ToUniversalTime(), + Golden = torrent.GoldenPopcorn, + Scene = torrent.Scene, + Approved = torrent.Checked + }); + } + // Don't add any torrents + else if (_settings.Approved && !torrent.Checked) + { + continue; + } + } + } + + // prefer golden + // prefer scene + // require approval + return torrentInfos.OrderBy(o => ((dynamic)o).Golden ? 0 : 1).ThenBy(o => ((dynamic)o).Scene ? 0 : 1).ThenByDescending(o => ((dynamic)o).PublishDate).ToArray(); + } + + private string GetDownloadUrl(int torrentId, string authKey, string passKey) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("action", "download") + .AddQueryParam("id", torrentId) + .AddQueryParam("authkey", authKey) + .AddQueryParam("torrent_pass", passKey); + + return url.FullUri; + } + + private string GetInfoUrl(string groupId, int torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("id", groupId) + .AddQueryParam("torrentid", torrentId); + + return url.FullUri; + } + + //public static bool IsPropertyExist(dynamic torrents, string name) + //{ + // return torrents.GetType().GetProperty(name) != null; + //} + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs new file mode 100644 index 000000000..773f18b79 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornRequestGenerator : IIndexerRequestGenerator + { + public PassThePopcornSettings Settings { get; set; } + + public virtual IndexerPageableRequestChain GetRecentRequests() + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetRequest(null)); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(searchCriteria.Movie.ImdbId)); + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + private IEnumerable GetRequest(string searchParameters) + { + var request = new IndexerRequest(string.Format("{0}/torrents.php?json=noredirect&searchstr={1}", Settings.BaseUrl.Trim().TrimEnd('/'), searchParameters), HttpAccept.Json); + + foreach (var cookie in HttpHeader.ParseCookies(Settings.Cookie)) + { + request.HttpRequest.Cookies[cookie.Key] = cookie.Value; + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs new file mode 100644 index 000000000..88d7e15b2 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs @@ -0,0 +1,52 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornSettingsValidator : AbstractValidator + { + public PassThePopcornSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.Cookie).NotEmpty(); + + RuleFor(c => c.Cookie) + .Matches(@"__cfduid=[0-9a-f]{43}", RegexOptions.IgnoreCase) + .WithMessage("Wrong pattern") + .AsWarning(); + } + } + + public class PassThePopcornSettings : IProviderConfig + { + private static readonly PassThePopcornSettingsValidator Validator = new PassThePopcornSettingsValidator(); + + public PassThePopcornSettings() + { + BaseUrl = "https://passthepopcorn.me"; + } + + [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your cookie will be sent to that host.")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Cookie", HelpText = "PassThePopcorn uses a login cookie needed to access the API, you'll have to retrieve it via a browser.")] + public string Cookie { get; set; } + + [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Prefer Golden", HelpText = "Favors Golden Popcorn-releases over all other releases.")] + public bool Golden { get; set; } + + [FieldDefinition(3, Type = FieldType.Checkbox, Label = "Prefer Scene", HelpText = "Favors scene-releases over non-scene releases.")] + public bool Scene { get; set; } + + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Require Approved", HelpText = "Require staff-approval for releases to be accepted.")] + public bool Approved { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a4a24ed0e..65b1a764a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -593,6 +593,12 @@ + + + + + +