diff --git a/README.md b/README.md
index c97d90749..3f60351d9 100644
--- a/README.md
+++ b/README.md
@@ -465,6 +465,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht
* P2PBG
* Panda
* Party-Tracker
+ * PassThePopcorn (PTP)
* Peeratiko
* Peers.FM
* PigNetwork
diff --git a/src/Jackett.Common/Indexers/PassThePopcorn.cs b/src/Jackett.Common/Indexers/PassThePopcorn.cs
new file mode 100644
index 000000000..ec00fee2a
--- /dev/null
+++ b/src/Jackett.Common/Indexers/PassThePopcorn.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Jackett.Common.Models;
+using Jackett.Common.Models.IndexerConfig;
+using Jackett.Common.Services.Interfaces;
+using Jackett.Common.Utils;
+using Newtonsoft.Json.Linq;
+using NLog;
+
+namespace Jackett.Common.Indexers
+{
+ [ExcludeFromCodeCoverage]
+ public class PassThePopcorn : IndexerBase
+ {
+ public override string Id => "passthepopcorn";
+ public override string Name => "PassThePopcorn";
+ public override string Description => "PassThePopcorn is a Private site for MOVIES / TV";
+ public override string SiteLink { get; protected set; } = "https://passthepopcorn.me/";
+ public override string Language => "en-US";
+ public override string Type => "private";
+
+ public override TorznabCapabilities TorznabCaps => SetCapabilities();
+
+ private static string SearchUrl => "https://passthepopcorn.me/torrents.php";
+ private string AuthKey { get; set; }
+ private string PassKey { get; set; }
+
+ // TODO: merge ConfigurationDataAPILoginWithUserAndPasskeyAndFilter class with with ConfigurationDataUserPasskey
+ private new ConfigurationDataAPILoginWithUserAndPasskeyAndFilter configData
+ {
+ get => (ConfigurationDataAPILoginWithUserAndPasskeyAndFilter)base.configData;
+ set => base.configData = value;
+ }
+
+ public PassThePopcorn(IIndexerConfigurationService configService, Utils.Clients.WebClient c, Logger l,
+ IProtectionService ps, ICacheService cs)
+ : base(configService: configService,
+ client: c,
+ logger: l,
+ p: ps,
+ cacheService: cs,
+ configData: new ConfigurationDataAPILoginWithUserAndPasskeyAndFilter(@"Enter filter options below to restrict search results.
+ Separate options with a space if using more than one option.
Filter options available:
+
GoldenPopcorn
Scene
Checked
Free
"))
+ {
+ webclient.requestDelay = 2; // 0.5 requests per second
+ }
+
+ private TorznabCapabilities SetCapabilities()
+ {
+ var caps = new TorznabCapabilities
+ {
+ TvSearchParams = new List
+ {
+ TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
+ },
+ MovieSearchParams = new List
+ {
+ MovieSearchParam.Q, MovieSearchParam.ImdbId
+ }
+ };
+
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.Movies, "Feature Film");
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesForeign);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesOther);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesSD);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesHD);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.Movies3D);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesBluRay);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesDVD);
+ caps.Categories.AddCategoryMapping(1, TorznabCatType.MoviesWEBDL);
+ caps.Categories.AddCategoryMapping(2, TorznabCatType.Movies, "Short Film");
+ caps.Categories.AddCategoryMapping(3, TorznabCatType.TV, "Miniseries");
+ caps.Categories.AddCategoryMapping(4, TorznabCatType.TV, "Stand-up Comedy");
+ caps.Categories.AddCategoryMapping(5, TorznabCatType.TV, "Live Performance");
+
+ return caps;
+ }
+
+ public override async Task ApplyConfiguration(JToken configJson)
+ {
+ LoadValuesFromJson(configJson);
+
+ IsConfigured = false;
+ try
+ {
+ var results = await PerformQuery(new TorznabQuery());
+ if (!results.Any())
+ throw new Exception("Testing returned no results!");
+ IsConfigured = true;
+ SaveConfig();
+ }
+ catch (Exception e)
+ {
+ throw new ExceptionWithConfigData(e.Message, configData);
+ }
+
+ return IndexerConfigurationStatus.Completed;
+ }
+
+ protected override async Task> PerformQuery(TorznabQuery query)
+ {
+ var releases = new List();
+ var configGoldenPopcornOnly = configData.FilterString.Value.ToLowerInvariant().Contains("goldenpopcorn");
+ var configSceneOnly = configData.FilterString.Value.ToLowerInvariant().Contains("scene");
+ var configCheckedOnly = configData.FilterString.Value.ToLowerInvariant().Contains("checked");
+ var configFreeOnly = configData.FilterString.Value.ToLowerInvariant().Contains("free");
+
+
+ var movieListSearchUrl = SearchUrl;
+ var queryCollection = new NameValueCollection { { "json", "noredirect" } };
+
+ if (!string.IsNullOrEmpty(query.ImdbID))
+ queryCollection.Add("searchstr", query.ImdbID);
+ else if (!string.IsNullOrEmpty(query.GetQueryString()))
+ queryCollection.Add("searchstr", query.GetQueryString());
+ if (configFreeOnly)
+ queryCollection.Add("freetorrent", "1");
+
+ movieListSearchUrl += "?" + queryCollection.GetQueryString();
+
+ var authHeaders = new Dictionary
+ {
+ { "ApiUser", configData.User.Value },
+ { "ApiKey", configData.Key.Value }
+ };
+
+ var results = await RequestWithCookiesAndRetryAsync(movieListSearchUrl, headers: authHeaders);
+ if (results.IsRedirect) // untested
+ results = await RequestWithCookiesAndRetryAsync(movieListSearchUrl, headers: authHeaders);
+ try
+ {
+ //Iterate over the releases for each movie
+ var jsResults = JObject.Parse(results.ContentString);
+
+ AuthKey = (string)jsResults["AuthKey"];
+ PassKey = (string)jsResults["PassKey"];
+
+ foreach (var movie in jsResults["Movies"])
+ {
+ var movieTitle = (string)movie["Title"];
+ var year = (string)movie["Year"];
+ var movieImdbIdStr = (string)movie["ImdbId"];
+ var posterStr = (string)movie["Cover"];
+ var poster = !string.IsNullOrEmpty(posterStr) ? new Uri(posterStr) : null;
+ var movieImdbId = !string.IsNullOrEmpty(movieImdbIdStr) ? (long?)long.Parse(movieImdbIdStr) : null;
+ var movieGroupId = (string)movie["GroupId"];
+ foreach (var torrent in movie["Torrents"])
+ {
+ var releaseName = (string)torrent["ReleaseName"];
+ var torrentId = (string)torrent["Id"];
+
+ var releaseLinkQuery = new NameValueCollection
+ {
+ {"action", "download"},
+ {"id", torrentId},
+ {"authkey", AuthKey},
+ {"torrent_pass", PassKey}
+ };
+ var free = !(torrent["FreeleechType"] is null);
+
+ bool.TryParse((string)torrent["GoldenPopcorn"], out var golden);
+ bool.TryParse((string)torrent["Scene"], out var scene);
+ bool.TryParse((string)torrent["Checked"], out var check);
+
+ if (configGoldenPopcornOnly && !golden)
+ continue; //Skip release if user only wants GoldenPopcorn
+ if (configSceneOnly && !scene)
+ continue; //Skip release if user only wants Scene
+ if (configCheckedOnly && !check)
+ continue; //Skip release if user only wants Checked
+ if (configFreeOnly && !free)
+ continue;
+ var link = new Uri($"{SearchUrl}?{releaseLinkQuery.GetQueryString()}");
+ var seeders = int.Parse((string)torrent["Seeders"]);
+ var details = new Uri($"{SearchUrl}?id={WebUtility.UrlEncode(movieGroupId)}&torrentid={WebUtility.UrlEncode(torrentId)}");
+ var size = long.Parse((string)torrent["Size"]);
+ var grabs = long.Parse((string)torrent["Snatched"]);
+ var publishDate = DateTime.ParseExact((string)torrent["UploadTime"],
+ "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)
+ .ToLocalTime();
+ var leechers = int.Parse((string)torrent["Leechers"]);
+ var release = new ReleaseInfo
+ {
+ Title = releaseName,
+ Description = $"Title: {movieTitle}",
+ Poster = poster,
+ Imdb = movieImdbId,
+ Details = details,
+ Size = size,
+ Grabs = grabs,
+ Seeders = seeders,
+ Peers = seeders + leechers,
+ PublishDate = publishDate,
+ Link = link,
+ Guid = link,
+ MinimumRatio = 1,
+ MinimumSeedTime = 345600,
+ DownloadVolumeFactor = free ? 0 : 1,
+ UploadVolumeFactor = 1,
+ Category = new List { TorznabCatType.Movies.ID }
+ };
+
+
+ var titleTags = new List();
+ var quality = (string)torrent["Quality"];
+ var container = (string)torrent["Container"];
+ var codec = (string)torrent["Codec"];
+ var resolution = (string)torrent["Resolution"];
+ var source = (string)torrent["Source"];
+ var otherTags = (string)torrent["RemasterTitle"];
+
+ if (year != null)
+ release.Description += $"
\nYear: {year}";
+ if (quality != null)
+ release.Description += $"
\nQuality: {quality}";
+ if (resolution != null)
+ {
+ titleTags.Add(resolution);
+ release.Description += $"
\nResolution: {resolution}";
+ }
+ if (source != null)
+ {
+ titleTags.Add(source);
+ release.Description += $"
\nSource: {source}";
+ }
+ if (codec != null)
+ {
+ titleTags.Add(codec);
+ release.Description += $"
\nCodec: {codec}";
+ }
+ if (container != null)
+ {
+ titleTags.Add(container);
+ release.Description += $"
\nContainer: {container}";
+ }
+ if (scene)
+ {
+ titleTags.Add("Scene");
+ release.Description += "
\nScene";
+ }
+ if (check)
+ {
+ titleTags.Add("Checked");
+ release.Description += "
\nChecked";
+ }
+ if (golden)
+ {
+ titleTags.Add("Golden Popcorn");
+ release.Description += "
\nGolden Popcorn";
+ }
+
+ if (otherTags != null)
+ titleTags.Add(otherTags);
+
+ if (titleTags.Any())
+ release.Title += " [" + string.Join(" / ", titleTags) + "]";
+
+ releases.Add(release);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ OnParseError(results.ContentString, ex);
+ }
+
+ return releases;
+ }
+ }
+}