diff --git a/README.md b/README.md index 280838dd8..36da92d81 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/ * NetHD * NextGen * Norbits + * PassTheHeadphones * PassThePopcorn * PirateTheNet * Pretome diff --git a/src/Jackett/Indexers/Abstract/GazelleTracker.cs b/src/Jackett/Indexers/Abstract/GazelleTracker.cs new file mode 100644 index 000000000..d41a1810b --- /dev/null +++ b/src/Jackett/Indexers/Abstract/GazelleTracker.cs @@ -0,0 +1,234 @@ +using AngleSharp.Parser.Html; +using Jackett.Models; +using Jackett.Models.IndexerConfig; +using Jackett.Services; +using Jackett.Utils; +using Jackett.Utils.Clients; +using Newtonsoft.Json.Linq; +using NLog; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Jackett.Indexers.Abstract +{ + public abstract class GazelleTracker : BaseIndexer + { + protected string LoginUrl { get { return SiteLink + "login.php"; } } + protected string APIUrl { get { return SiteLink + "ajax.php"; } } + protected string DownloadUrl { get { return SiteLink + "torrents.php?action=download&id="; } } + protected string DetailsUrl { get { return SiteLink + "torrents.php?torrentid="; } } + + new ConfigurationDataBasicLogin configData + { + get { return (ConfigurationDataBasicLogin)base.configData; } + set { base.configData = value; } + } + + public GazelleTracker(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService, string name, string desc, string link) + : base(name: name, + description: desc, + link: link, + caps: TorznabUtil.CreateDefaultTorznabTVCaps(), + manager: indexerManager, + client: webClient, + logger: logger, + p: protectionService, + configData: new ConfigurationDataBasicLogin()) + { + Encoding = Encoding.GetEncoding("UTF-8"); + } + + public async Task ApplyConfiguration(JToken configJson) + { + configData.LoadValuesFromJson(configJson); + var pairs = new Dictionary { + { "username", configData.Username.Value }, + { "password", configData.Password.Value }, + }; + + var response = await RequestLoginAndFollowRedirect(LoginUrl, pairs, string.Empty, true, SiteLink); + await ConfigureIfOK(response.Cookies, response.Content != null && response.Content.Contains("logout.php"), () => + { + var loginResultParser = new HtmlParser(); + var loginResultDocument = loginResultParser.Parse(response.Content); + var loginform = loginResultDocument.QuerySelector("#loginform"); + if (loginform == null) + throw new ExceptionWithConfigData(response.Content, configData); + + loginform.QuerySelector("table").Remove(); + var errorMessage = loginform.TextContent.Replace("\n\t", " ").Trim(); + throw new ExceptionWithConfigData(errorMessage, configData); + }); + return IndexerConfigurationStatus.RequiresTesting; + } + + public async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + var searchString = query.GetQueryString(); + + var searchUrl = APIUrl; + var queryCollection = new NameValueCollection(); + + queryCollection.Add("action", "browse"); + //queryCollection.Add("group_results", "0"); # results won't include all information + queryCollection.Add("order_by", "time"); + queryCollection.Add("order_way", "desc"); + + if (!string.IsNullOrWhiteSpace(searchString)) + { + queryCollection.Add("searchstr", searchString); + } + + foreach (var cat in MapTorznabCapsToTrackers(query)) + { + queryCollection.Add("filter_cat[" + cat + "]", "1"); + } + + searchUrl += "?" + queryCollection.GetQueryString(); + + var response = await RequestStringWithCookiesAndRetry(searchUrl); + + if (response.IsRedirect || query.IsTest) + { + // re-login + await ApplyConfiguration(null); + response = await RequestStringWithCookiesAndRetry(searchUrl); + } + + try + { + var json = JObject.Parse(response.Content); + foreach (JObject r in json["response"]["results"]) + { + var groupTime = DateTimeUtil.UnixTimestampToDateTime(long.Parse((string)r["groupTime"])); + var groupName = HttpUtility.HtmlDecode((string)r["groupName"]); + var artist = (string)r["artist"]; + var cover = (string)r["cover"]; + var tags = r["tags"].ToList(); + var groupYear = (string)r["groupYear"]; + var releaseType = (string)r["releaseType"]; + + var release = new ReleaseInfo(); + + release.PublishDate = groupTime; + + if (!string.IsNullOrEmpty(cover)) + release.BannerUrl = new Uri(cover); + + release.Title = ""; + if (!string.IsNullOrEmpty(artist)) + release.Title += artist + " - "; + release.Title += groupName; + if (!string.IsNullOrEmpty(groupYear)) + release.Title += " [" + groupYear + "]"; + if (!string.IsNullOrEmpty(releaseType)) + release.Title += " [" + releaseType + "]"; + + release.Description = ""; + if (tags != null) + release.Description += "Tags: " + string.Join(", ", tags) + "\n"; + + if (r["torrents"] is JArray) + { + foreach (JObject torrent in r["torrents"]) + { + ReleaseInfo release2 = (ReleaseInfo)release.Clone(); + FillReleaseInfoFromJson(release2, torrent); + releases.Add(release2); + } + } + else + { + FillReleaseInfoFromJson(release, r); + releases.Add(release); + } + } + } + catch (Exception ex) + { + OnParseError(response.Content, ex); + } + + return releases; + } + + void FillReleaseInfoFromJson(ReleaseInfo release, JObject torrent) + { + var torrentId = torrent["torrentId"]; + + var time = (string)torrent["time"]; + if (!string.IsNullOrEmpty(time)) { + release.PublishDate = DateTime.ParseExact(time+" +0000", "yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture); + } + + var flags = new List(); + + var format = (string)torrent["format"]; + if (!string.IsNullOrEmpty(format)) + flags.Add(format); + + var encoding = (string)torrent["encoding"]; + if (!string.IsNullOrEmpty(encoding)) + flags.Add(encoding); + + if(torrent["hasLog"] != null && (bool)torrent["hasLog"]) + { + var logScore = (string)torrent["logScore"]; + flags.Add("Log (" + logScore + "%)"); + } + + if (torrent["hasCue"] != null && (bool)torrent["hasCue"]) + flags.Add("Cue"); + + var media = (string)torrent["media"]; + if (!string.IsNullOrEmpty(media)) + flags.Add(media); + + if (torrent["remastered"] != null && (bool)torrent["remastered"]) + { + var remasterYear = (string)torrent["remasterYear"]; + var remasterTitle = HttpUtility.HtmlDecode((string)torrent["remasterTitle"]); + flags.Add(remasterYear + (!string.IsNullOrEmpty(remasterTitle) ? " " + remasterTitle : "")); + } + + if (flags.Count > 0) + release.Title += " " + string.Join(" / ", flags); + + release.Size = (long)torrent["size"]; + release.Seeders = (int)torrent["seeders"]; + release.Peers = (int)torrent["leechers"] + release.Seeders; + release.Comments = new Uri(DetailsUrl + torrentId); + release.Guid = release.Comments; + release.Link = new Uri(DownloadUrl + torrentId); + var category = (string)torrent["category"]; + if (category == null) + release.Category = MapTrackerCatToNewznab("1"); + else + release.Category = MapTrackerCatDescToNewznab(category); + release.Files = (int)torrent["fileCount"]; + release.Grabs = (int)torrent["snatches"]; + release.DownloadVolumeFactor = 1; + release.UploadVolumeFactor = 1; + if ((bool)torrent["isFreeleech"]) + { + release.DownloadVolumeFactor = 0; + } + if ((bool)torrent["isPersonalFreeleech"]) + { + release.DownloadVolumeFactor = 0; + } + if ((bool)torrent["isNeutralLeech"]) + { + release.DownloadVolumeFactor = 0; + release.UploadVolumeFactor = 0; + } + } + } +} diff --git a/src/Jackett/Indexers/PassTheHeadphones.cs b/src/Jackett/Indexers/PassTheHeadphones.cs new file mode 100644 index 000000000..240b4d108 --- /dev/null +++ b/src/Jackett/Indexers/PassTheHeadphones.cs @@ -0,0 +1,32 @@ +using Jackett.Models; +using NLog; +using Jackett.Services; +using Jackett.Utils.Clients; +using Jackett.Indexers.Abstract; + +namespace Jackett.Indexers +{ + public class PassTheHeadphones : GazelleTracker, IIndexer + { + public PassTheHeadphones(IIndexerManagerService indexerManager, IWebClient webClient, Logger logger, IProtectionService protectionService) + : base(name: "PassTheHeadphones", + desc: "A music tracker", + link: "https://passtheheadphones.me/", + indexerManager: indexerManager, + logger: logger, + protectionService: protectionService, + webClient: webClient + ) + { + Language = "en-us"; + + AddCategoryMapping(1, TorznabCatType.Audio, "Music"); + AddCategoryMapping(2, TorznabCatType.PC, "Applications"); + AddCategoryMapping(3, TorznabCatType.Books, "E-Books"); + AddCategoryMapping(4, TorznabCatType.AudioAudiobook, "Audiobooks"); + AddCategoryMapping(5, TorznabCatType.Movies, "E-Learning Videos"); + AddCategoryMapping(6, TorznabCatType.TV, "Comedy"); + AddCategoryMapping(7, TorznabCatType.Books, "Comics"); + } + } +} \ No newline at end of file diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 7cdf24dc4..1ca5a80cf 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -165,7 +165,9 @@ + +