parent
49b120ba55
commit
41a9d2d732
@ -0,0 +1,400 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions;
|
||||
|
||||
public class Shazbat : TorrentIndexerBase<ShazbatSettings>
|
||||
{
|
||||
public override string Name => "Shazbat";
|
||||
public override string[] IndexerUrls => new[] { "https://www.shazbat.tv/" };
|
||||
public override string Description => "Shazbat is a PRIVATE Torrent Tracker with highly curated TV content";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(5.1);
|
||||
|
||||
public Shazbat(IIndexerHttpClient httpClient,
|
||||
IEventAggregator eventAggregator,
|
||||
IIndexerStatusService indexerStatusService,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new ShazbatRequestGenerator(Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new ShazbatParser(Settings, RateLimit, _httpClient, _logger);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var loginUrl = Settings.BaseUrl + "login";
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(loginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
var authLoginRequest = requestBuilder.Post()
|
||||
.AddFormParameter("referer", "")
|
||||
.AddFormParameter("query", "")
|
||||
.AddFormParameter("tv_timezone", "0")
|
||||
.AddFormParameter("tv_login", Settings.Username)
|
||||
.AddFormParameter("tv_password", Settings.Password)
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("Referer", loginUrl)
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("div#fail .modal-body")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
|
||||
}
|
||||
|
||||
var cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("Authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse response)
|
||||
{
|
||||
return !response.Content.Contains("onclick=\"document.location='logout'\"") &&
|
||||
!response.Content.Contains("show_id") && !response.Content.Contains("Filename") &&
|
||||
!response.Content.Contains("Peers") && !response.Content.Contains("Download");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.TV);
|
||||
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVSD);
|
||||
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TVHD);
|
||||
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.TVUHD);
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShazbatRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
private readonly ShazbatSettings _settings;
|
||||
|
||||
public ShazbatRequestGenerator(ShazbatSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}"));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
term = FixSearchTerm(term);
|
||||
|
||||
if (term.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var request = new HttpRequestBuilder(_settings.BaseUrl + "search").Post()
|
||||
.AddFormParameter("search", term)
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("X-Requested-With", "XMLHttpRequest")
|
||||
.SetHeader("Referer", _settings.BaseUrl)
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new HttpRequestBuilder(_settings.BaseUrl + "torrents")
|
||||
.SetHeader("Referer", _settings.BaseUrl)
|
||||
.Accept(HttpAccept.Html)
|
||||
.Build();
|
||||
|
||||
yield return new IndexerRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixSearchTerm(string term)
|
||||
{
|
||||
term = Regex.Replace(term, @"\b[S|E]\d+\b", string.Empty, RegexOptions.IgnoreCase);
|
||||
term = Regex.Replace(term, @"(.+)\b\d{4}(\.\d{2}\.\d{2})?\b", "$1");
|
||||
term = Regex.Replace(term, @"[\.\s\(\)\[\]]+", " ");
|
||||
|
||||
return term.ToLower().Trim();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class ShazbatParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly ShazbatSettings _settings;
|
||||
private readonly TimeSpan _rateLimit;
|
||||
private readonly IIndexerHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly Regex _torrentInfoRegex = new (@"\((?<size>\d+)\):(?<seeders>\d+) \/ :(?<leechers>\d+)$", RegexOptions.Compiled);
|
||||
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
|
||||
|
||||
public ShazbatParser(ShazbatSettings settings, TimeSpan rateLimit, IIndexerHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_rateLimit = rateLimit;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
var hasGlobalFreeleech = dom.QuerySelector("span:contains(\"Freeleech until:\"):has(span.datetime)") != null;
|
||||
|
||||
releaseInfos.AddRange(ParseResults(indexerResponse, hasGlobalFreeleech));
|
||||
|
||||
var shows = dom.QuerySelectorAll("div.show[data-id]");
|
||||
if (shows.Any())
|
||||
{
|
||||
var showPagesFetchLimit = _settings.ShowPagesFetchLimit ?? 2;
|
||||
|
||||
if (showPagesFetchLimit is < 1 or > 5)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Value for Show Pages Fetch Limit should be between 1 and 5. Current value: {0}.", showPagesFetchLimit);
|
||||
}
|
||||
|
||||
if (shows.Length > showPagesFetchLimit)
|
||||
{
|
||||
_logger.Debug($"Your search returned {shows.Length} shows. Use a more specific search term for more relevant results.");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.GetCookies() == null || !indexerResponse.HttpResponse.GetCookies().Any())
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Invalid cookies. Most likely your session expired or was killed.");
|
||||
}
|
||||
|
||||
foreach (var show in shows.Take(showPagesFetchLimit))
|
||||
{
|
||||
var showPageUrl = new HttpRequestBuilder(_settings.BaseUrl + "show")
|
||||
.AddQueryParam("id", show.GetAttribute("data-id"))
|
||||
.Build()
|
||||
.Url.FullUri;
|
||||
|
||||
var showRequest = new HttpRequestBuilder(_settings.BaseUrl + "show").Post()
|
||||
.SetCookies(indexerResponse.HttpResponse.GetCookies() ?? new Dictionary<string, string>())
|
||||
.AddQueryParam("id", show.GetAttribute("data-id"))
|
||||
.AddQueryParam("show_mode", "torrents")
|
||||
.AddFormParameter("portlet", "true")
|
||||
.AddFormParameter("tab", "true")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.SetHeader("X-Requested-With", "XMLHttpRequest")
|
||||
.SetHeader("Referer", showPageUrl)
|
||||
.Accept(HttpAccept.Html)
|
||||
.WithRateLimit(_rateLimit.TotalSeconds)
|
||||
.Build();
|
||||
|
||||
_logger.Debug("Downloading Feed " + showRequest.ToString());
|
||||
|
||||
var releaseRequest = new IndexerRequest(showRequest);
|
||||
var releaseResponse = new IndexerResponse(releaseRequest, _httpClient.Execute(releaseRequest.HttpRequest));
|
||||
|
||||
if (releaseResponse.HttpResponse.Content.ContainsIgnoreCase("sign in now"))
|
||||
{
|
||||
// Remove cookie cache
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerAuthException("We are being redirected to the Shazbat login page. Most likely your session expired or was killed.");
|
||||
}
|
||||
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
{
|
||||
if (releaseResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
|
||||
throw new IndexerException(releaseResponse, $"HTTP Error - {releaseResponse.HttpResponse.StatusCode}. {showRequest.Url.FullUri}");
|
||||
}
|
||||
|
||||
releaseInfos.AddRange(ParseResults(releaseResponse, hasGlobalFreeleech));
|
||||
}
|
||||
}
|
||||
|
||||
return releaseInfos.ToArray();
|
||||
}
|
||||
|
||||
private IList<ReleaseInfo> ParseResults(IndexerResponse indexerResponse, bool hasGlobalFreeleech = false)
|
||||
{
|
||||
var releaseInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
if (!hasGlobalFreeleech)
|
||||
{
|
||||
hasGlobalFreeleech = dom.QuerySelector("span:contains(\"Freeleech until:\"):has(span.datetime)") != null;
|
||||
}
|
||||
|
||||
var publishDate = DateTime.Now;
|
||||
|
||||
var rows = dom.QuerySelectorAll("#torrent-table tr.eprow, table tr.eprow");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var downloadUrl = row.QuerySelector("td:nth-of-type(5) a[href^=\"load_torrent?\"]")?.GetAttribute("href");
|
||||
var infoUrl = row.QuerySelector("td:nth-of-type(5) [href^=\"torrent_info?\"]")?.GetAttribute("href");
|
||||
var title = ParseTitle(row.QuerySelector("td:nth-of-type(3)"));
|
||||
|
||||
var infoString = row.QuerySelector("td:nth-of-type(4)")?.TextContent.Trim() ?? string.Empty;
|
||||
var matchInfo = _torrentInfoRegex.Match(infoString);
|
||||
var size = matchInfo.Groups["size"].Success && long.TryParse(matchInfo.Groups["size"].Value, out var outSize) ? outSize : 0;
|
||||
var seeders = matchInfo.Groups["seeders"].Success && int.TryParse(matchInfo.Groups["seeders"].Value, out var outSeeders) ? outSeeders : 0;
|
||||
var leechers = matchInfo.Groups["leechers"].Success && int.TryParse(matchInfo.Groups["leechers"].Value, out var outLeechers) ? outLeechers : 0;
|
||||
|
||||
var dateTimestamp = row.QuerySelector(".datetime[data-timestamp]")?.GetAttribute("data-timestamp");
|
||||
publishDate = dateTimestamp != null && ParseUtil.TryCoerceDouble(dateTimestamp, out var timestamp) ? DateTimeUtil.UnixTimestampToDateTime(timestamp) : publishDate.AddMinutes(-1);
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Guid = infoUrl,
|
||||
InfoUrl = _settings.BaseUrl + infoUrl,
|
||||
DownloadUrl = _settings.BaseUrl + downloadUrl,
|
||||
Title = title,
|
||||
Categories = ParseCategories(title),
|
||||
Size = size,
|
||||
Seeders = seeders,
|
||||
Peers = seeders + leechers,
|
||||
PublishDate = publishDate,
|
||||
Genres = row.QuerySelectorAll("label.label-tag").Select(t => t.TextContent.Trim()).ToList(),
|
||||
DownloadVolumeFactor = hasGlobalFreeleech ? 0 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800, // 48 hours
|
||||
};
|
||||
|
||||
releaseInfos.Add(release);
|
||||
}
|
||||
|
||||
return releaseInfos;
|
||||
}
|
||||
|
||||
private static string ParseTitle(IElement titleRow)
|
||||
{
|
||||
var title = titleRow?.ChildNodes.First(n => n.NodeType == NodeType.Text && n.TextContent.Trim() != string.Empty);
|
||||
|
||||
return title?.TextContent.Trim();
|
||||
}
|
||||
|
||||
protected virtual List<IndexerCategory> ParseCategories(string title)
|
||||
{
|
||||
var categories = new List<IndexerCategory>
|
||||
{
|
||||
NewznabStandardCategory.TV,
|
||||
title switch
|
||||
{
|
||||
_ when _hdResolutions.Any(title.Contains) => NewznabStandardCategory.TVHD,
|
||||
_ when title.Contains("2160p") => NewznabStandardCategory.TVUHD,
|
||||
_ => NewznabStandardCategory.TVSD
|
||||
}
|
||||
};
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class ShazbatSettingsValidator : UserPassBaseSettingsValidator<ShazbatSettings>
|
||||
{
|
||||
public ShazbatSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.ShowPagesFetchLimit).GreaterThan(0).When(c => c.ShowPagesFetchLimit.HasValue).WithMessage("Should be greater than zero");
|
||||
RuleFor(c => c.ShowPagesFetchLimit).LessThanOrEqualTo(5).When(c => c.ShowPagesFetchLimit.HasValue).WithMessage("Should be less than or equal to 5");
|
||||
}
|
||||
}
|
||||
|
||||
public class ShazbatSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
private static readonly ShazbatSettingsValidator Validator = new ();
|
||||
|
||||
[FieldDefinition(4, Type = FieldType.Number, Label = "Show Pages Fetch Limit", HelpText = "The number of show pages should Prowlarr fetch when searching. Default: 2.")]
|
||||
public int? ShowPagesFetchLimit { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
Loading…
Reference in new issue