From 4545e384450ba82716a68a436404fc89b82918fc Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 10 May 2021 22:55:58 -0400 Subject: [PATCH] New: (Indexer) SubsPlease --- .../Indexers/Definitions/SubsPlease.cs | 285 ++++++++++++++++++ src/NzbDrone.Core/Indexers/HttpIndexerBase.cs | 12 +- 2 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs diff --git a/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs b/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs new file mode 100644 index 000000000..06579773e --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/SubsPlease.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using FluentValidation; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Definitions +{ + public class SubsPlease : HttpIndexerBase + { + public override string Name => "SubsPlease"; + public override string BaseUrl => "https://subsplease.org/"; + public override string Language => "en-us"; + public override string Description => "SubsPlease - A better HorribleSubs/Erai replacement"; + public override Encoding Encoding => Encoding.UTF8; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override IndexerPrivacy Privacy => IndexerPrivacy.Public; + public override IndexerCapabilities Capabilities => SetCapabilities(); + + public SubsPlease(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger) + : base(httpClient, eventAggregator, indexerStatusService, configService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new SubsPleaseRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl }; + } + + public override IParseIndexerResponse GetParser() + { + return new SubsPleaseParser(Settings, Capabilities.Categories, BaseUrl); + } + + private IndexerCapabilities SetCapabilities() + { + var caps = new IndexerCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + }; + + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "Anime"); + + return caps; + } + } + + public class SubsPleaseRequestGenerator : IIndexerRequestGenerator + { + public SubsPleaseSettings Settings { get; set; } + public IndexerCapabilities Capabilities { get; set; } + public string BaseUrl { get; set; } + + public SubsPleaseRequestGenerator() + { + } + + private IEnumerable GetSearchRequests(string term) + { + var searchUrl = string.Format("{0}/api/?", BaseUrl.TrimEnd('/')); + + string searchTerm = Regex.Replace(term, "\\[?SubsPlease\\]?\\s*", string.Empty, RegexOptions.IgnoreCase).Trim(); + + // If the search terms contain a resolution, remove it from the query sent to the API + Match resMatch = Regex.Match(searchTerm, "\\d{3,4}[p|P]"); + if (resMatch.Success) + { + searchTerm = searchTerm.Replace(resMatch.Value, string.Empty); + } + + var queryParameters = new NameValueCollection + { + { "f", "search" }, + { "tz", "America/New_York" }, + { "s", searchTerm } + }; + + var request = new IndexerRequest(searchUrl + queryParameters.GetQueryString(), HttpAccept.Json); + + yield return request; + } + + private IEnumerable GetRssRequest() + { + var searchUrl = string.Format("{0}/api/?", BaseUrl.TrimEnd('/')); + + var queryParameters = new NameValueCollection + { + { "f", "latest" }, + { "tz", "America/New_York" } + }; + + var request = new IndexerRequest(searchUrl + queryParameters.GetQueryString(), HttpAccept.Json); + + yield return request; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + if (searchCriteria.RssSearch) + { + pageableRequests.Add(GetRssRequest()); + } + else + { + pageableRequests.Add(GetSearchRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString))); + } + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + if (searchCriteria.RssSearch) + { + pageableRequests.Add(GetRssRequest()); + } + else + { + pageableRequests.Add(GetSearchRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm))); + } + + return pageableRequests; + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class SubsPleaseParser : IParseIndexerResponse + { + private readonly SubsPleaseSettings _settings; + private readonly IndexerCapabilitiesCategories _categories; + private readonly string _baseUrl; + + public SubsPleaseParser(SubsPleaseSettings settings, IndexerCapabilitiesCategories categories, string baseurl) + { + _settings = settings; + _categories = categories; + _baseUrl = baseurl; + } + + 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"); + } + + // When there are no results, the API returns an empty array or empty response instead of an object + if (string.IsNullOrWhiteSpace(indexerResponse.Content) || indexerResponse.Content == "[]") + { + return torrentInfos; + } + + var jsonResponse = new HttpResponse>(indexerResponse.HttpResponse); + + foreach (var keyValue in jsonResponse.Resource) + { + SubPleaseRelease r = keyValue.Value; + + foreach (var d in r.Downloads) + { + var release = new TorrentInfo + { + InfoUrl = _baseUrl + $"shows/{r.Page}/", + PublishDate = r.Release_Date.DateTime, + Files = 1, + Category = new List { NewznabStandardCategory.TVAnime }, + Seeders = 1, + Peers = 2, + MinimumRatio = 1, + MinimumSeedTime = 172800, // 48 hours + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1 + }; + + // Ex: [SubsPlease] Shingeki no Kyojin (The Final Season) - 64 (1080p) + release.Title += $"[SubsPlease] {r.Show} - {r.Episode} ({d.Res}p)"; + release.MagnetUrl = d.Magnet; + release.DownloadUrl = null; + release.Guid = d.Magnet; + + // The API doesn't tell us file size, so give an estimate based on resolution + if (string.Equals(d.Res, "1080")) + { + release.Size = 1395864371; // 1.3GB + } + else if (string.Equals(d.Res, "720")) + { + release.Size = 734003200; // 700MB + } + else if (string.Equals(d.Res, "480")) + { + release.Size = 367001600; // 350MB + } + else + { + release.Size = 1073741824; // 1GB + } + + torrentInfos.Add(release); + } + } + + return torrentInfos.ToArray(); + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } + + public class SubsPleaseSettingsValidator : AbstractValidator + { + } + + public class SubsPleaseSettings : IProviderConfig + { + private static readonly SubsPleaseSettingsValidator Validator = new SubsPleaseSettingsValidator(); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } + + public class SubPleaseRelease + { + public string Time { get; set; } + public DateTimeOffset Release_Date { get; set; } + public string Show { get; set; } + public string Episode { get; set; } + public SubPleaseDownloadInfo[] Downloads { get; set; } + public string Xdcc { get; set; } + public string ImageUrl { get; set; } + public string Page { get; set; } + } + + public class SubPleaseDownloadInfo + { + public string Res { get; set; } + public string Magnet { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 725f422c7..18e8c965c 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -252,7 +252,7 @@ namespace NzbDrone.Core.Indexers } } - releases.AddRange(pagedReleases.Where(IsValidRelease)); + releases.AddRange(pagedReleases); } if (releases.Any()) @@ -363,16 +363,6 @@ namespace NzbDrone.Core.Indexers return Capabilities ?? ((IndexerDefinition)Definition).Capabilities; } - protected virtual bool IsValidRelease(ReleaseInfo release) - { - if (release.DownloadUrl == null) - { - return false; - } - - return true; - } - protected virtual bool IsFullPage(IList page) { return PageSize != 0 && page.Count >= PageSize;