Merge branch 'develop' into fix/flaresolverr-use-response-body

This commit is contained in:
Eric Quaintance 2026-03-19 17:21:12 -04:00
commit eb2277a33e
21 changed files with 400 additions and 600 deletions

8
SECURITY.md Normal file
View file

@ -0,0 +1,8 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.

View file

@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '2.3.2'
majorVersion: '2.3.4'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View file

@ -6,13 +6,14 @@
namespace NzbDrone.Common.Http
{
public class HttpUri : IEquatable<HttpUri>
public partial class HttpUri : IEquatable<HttpUri>
{
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _uri;
public string FullUri => _uri;
[GeneratedRegex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex UriRegex();
public HttpUri(string uri)
{
_uri = uri ?? string.Empty;
@ -70,9 +71,9 @@ public HttpUri(string scheme, string host, int? port, string path, string query,
private void Parse()
{
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out _);
var match = RegexUri.Match(_uri);
var match = UriRegex().Match(_uri);
var scheme = match.Groups["scheme"];
var host = match.Groups["host"];

View file

@ -1,341 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using AngleSharp.Html.Parser;
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;
namespace NzbDrone.Core.Indexers.Definitions
{
public class AnimeTorrents : TorrentIndexerBase<AnimeTorrentsSettings>
{
public override string Name => "AnimeTorrents";
public override string[] IndexerUrls => new[] { "https://animetorrents.me/" };
public override string Description => "Definitive source for anime and manga";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override bool SupportsPagination => true;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
public override IndexerCapabilities Capabilities => SetCapabilities();
public AnimeTorrents(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimeTorrentsRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new AnimeTorrentsParser(Settings, Capabilities.Categories);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php"))
{
throw new IndexerAuthException("AnimeTorrents authentication with cookies failed.");
}
return false;
}
protected override IDictionary<string, string> GetCookies()
{
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.MoviesHD, "Anime Movie HD");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Anime Series");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TVAnime, "Anime Series HD");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXXDVD, "Hentai (censored)");
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.XXXDVD, "Hentai (censored) HD");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.XXXDVD, "Hentai (un-censored)");
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.XXXDVD, "Hentai (un-censored) HD");
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.BooksForeign, "Light Novel");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksComics, "Manga");
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.BooksComics, "Manga 18+");
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.TVAnime, "OVA");
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.TVAnime, "OVA HD");
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.BooksComics, "Doujin Anime");
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.XXXDVD, "Doujin Anime 18+");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.AudioForeign, "Doujin Music");
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.BooksComics, "Doujinshi");
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.BooksComics, "Doujinshi 18+");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Audio, "OST");
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
return caps;
}
}
public class AnimeTorrentsRequestGenerator : IIndexerRequestGenerator
{
private readonly AnimeTorrentsSettings _settings;
private readonly IndexerCapabilities _capabilities;
public AnimeTorrentsRequestGenerator(AnimeTorrentsSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
{
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
}
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
{
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
}
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
{
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
}
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
{
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
}
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
{
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
}
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, string category, SearchCriteriaBase searchCriteria)
{
var searchUrl = _settings.BaseUrl + "ajax/torrents_data.php";
// replace non-word characters with % (wildcard)
var searchString = Regex.Replace(term.Trim(), @"[\W]+", "%");
var page = searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0 ? (int)(searchCriteria.Offset / searchCriteria.Limit) + 1 : 1;
var refererUri = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("cat", $"{category}");
if (_settings.DownloadableOnly)
{
refererUri = refererUri.AddQueryParam("dlable", "1");
}
var requestBuilder = new HttpRequestBuilder(searchUrl)
.AddQueryParam("total", "100") // Assuming the total number of pages
.AddQueryParam("cat", $"{category}")
.AddQueryParam("searchin", "filename")
.AddQueryParam("search", searchString)
.AddQueryParam("page", page)
.SetHeader("X-Requested-With", "XMLHttpRequest")
.SetHeader("Referer", refererUri.FullUri)
.Accept(HttpAccept.Html);
if (_settings.DownloadableOnly)
{
requestBuilder.AddQueryParam("dlable", "1");
}
yield return new IndexerRequest(requestBuilder.Build());
}
private List<string> GetTrackerCategories(string term, SearchCriteriaBase searchCriteria)
{
var searchTerm = term.Trim();
var categoryMapping = _capabilities.Categories
.MapTorznabCapsToTrackers(searchCriteria.Categories)
.Distinct()
.ToList();
return searchTerm.IsNullOrWhiteSpace() && categoryMapping.Count == 2
? categoryMapping
: new List<string> { categoryMapping.FirstIfSingleOrDefault("0") };
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnimeTorrentsParser : IParseIndexerResponse
{
private readonly AnimeTorrentsSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
using var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("table tr");
foreach (var (row, index) in rows.Skip(1).Select((v, i) => (v, i)))
{
var downloadVolumeFactor = row.QuerySelector("img[alt=\"Gold Torrent\"]") != null ? 0 : row.QuerySelector("img[alt=\"Silver Torrent\"]") != null ? 0.5 : 1;
// skip non-freeleech results when freeleech only is set
if (_settings.FreeleechOnly && downloadVolumeFactor != 0)
{
continue;
}
var qTitleLink = row.QuerySelector("td:nth-of-type(2) a:nth-of-type(1)");
var title = qTitleLink?.TextContent.Trim();
// If we search and get no results, we still get a table just with no info.
if (title.IsNullOrWhiteSpace())
{
break;
}
var infoUrl = qTitleLink?.GetAttribute("href");
// newbie users don't see DL links
// use details link as placeholder
// skipping the release prevents newbie users from adding the tracker (empty result)
var downloadUrl = row.QuerySelector("td:nth-of-type(3) a")?.GetAttribute("href") ?? infoUrl;
var connections = row.QuerySelector("td:nth-of-type(8)").TextContent.Trim().Split('/', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var seeders = ParseUtil.CoerceInt(connections[0]);
var leechers = ParseUtil.CoerceInt(connections[1]);
var grabs = ParseUtil.CoerceInt(connections[2]);
var categoryLink = row.QuerySelector("td:nth-of-type(1) a")?.GetAttribute("href") ?? string.Empty;
var categoryId = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat");
var publishedDate = DateTime.ParseExact(row.QuerySelector("td:nth-of-type(5)").TextContent, "dd MMM yy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
if (publishedDate.Date == DateTime.Today)
{
publishedDate = publishedDate.Date + DateTime.Now.TimeOfDay - TimeSpan.FromMinutes(index);
}
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadUrl,
Title = title,
Categories = _categories.MapTrackerCatToNewznab(categoryId),
PublishDate = publishedDate,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(6)").TextContent.Trim()),
Seeders = seeders,
Peers = leechers + seeders,
Grabs = grabs,
DownloadVolumeFactor = downloadVolumeFactor,
UploadVolumeFactor = 1,
Genres = row.QuerySelectorAll("td:nth-of-type(2) a.tortags").Select(t => t.TextContent.Trim()).ToList()
};
var uploadFactor = row.QuerySelector("img[alt*=\"x Multiplier Torrent\"]")?.GetAttribute("alt");
if (uploadFactor != null)
{
release.UploadVolumeFactor = ParseUtil.CoerceDouble(uploadFactor.Split('x')[0]);
}
releaseInfos.Add(release);
}
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnimeTorrentsSettings : CookieTorrentBaseSettings
{
public AnimeTorrentsSettings()
{
FreeleechOnly = false;
DownloadableOnly = false;
}
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech torrents only")]
public bool FreeleechOnly { get; set; }
[FieldDefinition(5, Label = "Downloadable Only", Type = FieldType.Checkbox, HelpText = "Search downloadable torrents only (enable this only if your account class is Newbie)", Advanced = true)]
public bool DownloadableOnly { get; set; }
}
}

View file

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers.Definitions.Avistaz;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions;
public class AnimeZ : AvistazBase
{
public override string Name => "AnimeZ";
public override string[] IndexerUrls => new[] { "https://animez.to/" };
public override string Description => "AnimeZ (ex-AnimeTorrents) is a Private Torrent Tracker for ANIME / MANGA";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public AnimeZ(IIndexerRepository indexerRepository,
IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(indexerRepository, httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnimeZRequestGenerator
{
Settings = Settings,
Capabilities = Capabilities,
PageSize = PageSize,
HttpClient = _httpClient,
Logger = _logger
};
}
public override IParseIndexerResponse GetParser()
{
return new AnimeZParser(Capabilities.Categories);
}
public override async Task<IndexerDownloadResponse> Download(Uri link)
{
try
{
return await base.Download(link).ConfigureAwait(false);
}
catch (ReleaseDownloadException ex) when (ex.InnerException is HttpException httpException &&
httpException.Response.StatusCode is HttpStatusCode.Unauthorized)
{
await DoLogin().ConfigureAwait(false);
}
return await base.Download(link).ConfigureAwait(false);
}
protected override Task<HttpRequest> GetDownloadRequest(Uri link)
{
var request = new HttpRequestBuilder(link.AbsoluteUri)
.Accept(HttpAccept.Json)
.SetHeader("Authorization", $"Bearer {Settings.Token}")
.Build();
return Task.FromResult(request);
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
LimitsDefault = PageSize,
LimitsMax = PageSize,
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q,
}
};
caps.Categories.AddCategoryMapping("TV", NewznabStandardCategory.TVAnime, "Anime > TV");
caps.Categories.AddCategoryMapping("TV_SHORT", NewznabStandardCategory.TVAnime, "Anime > TV Short");
caps.Categories.AddCategoryMapping("MOVIE", NewznabStandardCategory.Movies, "Anime > Movie");
caps.Categories.AddCategoryMapping("SPECIAL", NewznabStandardCategory.TVAnime, "Anime > Special");
caps.Categories.AddCategoryMapping("OVA", NewznabStandardCategory.TVAnime, "Anime > OVA");
caps.Categories.AddCategoryMapping("ONA", NewznabStandardCategory.TVAnime, "Anime > ONA");
caps.Categories.AddCategoryMapping("MUSIC", NewznabStandardCategory.TVAnime, "Anime > Music");
caps.Categories.AddCategoryMapping("MANGA", NewznabStandardCategory.BooksComics, "Manga > Manga");
caps.Categories.AddCategoryMapping("NOVEL", NewznabStandardCategory.BooksForeign, "Manga > Novel");
caps.Categories.AddCategoryMapping("ONE_SHOT", NewznabStandardCategory.BooksForeign, "Manga > One-Shot");
return caps;
}
}
public class AnimeZRequestGenerator : AvistazRequestGenerator
{
protected override List<KeyValuePair<string, string>> GetBasicSearchParameters(SearchCriteriaBase searchCriteria, string genre = null)
{
var parameters = new List<KeyValuePair<string, string>>
{
{ "limit", Math.Min(PageSize, searchCriteria.Limit.GetValueOrDefault(PageSize)).ToString() }
};
var categoryMappings = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
if (categoryMappings.Any())
{
foreach (var category in categoryMappings)
{
parameters.Add("format[]", category);
}
}
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
{
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
parameters.Add("page", page.ToString());
}
if (Settings.FreeleechOnly)
{
parameters.Add("freeleech", "1");
}
return parameters;
}
}
public class AnimeZParser(IndexerCapabilitiesCategories categories) : AvistazParserBase
{
protected override List<IndexerCategory> ParseCategories(AvistazRelease row)
{
return categories.MapTrackerCatToNewznab(row.Format).ToList();
}
protected override string ParseTitle(AvistazRelease row)
{
return row.ReleaseTitle.IsNotNullOrWhiteSpace() ? row.ReleaseTitle : row.FileName;
}
}

View file

@ -5,72 +5,77 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public class AvistazRelease
{
public string Url { get; set; }
public string Download { get; set; }
public Dictionary<string, string> Category { get; set; }
public string Url { get; init; }
public string Download { get; init; }
public Dictionary<string, string> Category { get; init; }
[JsonPropertyName("movie_tv")]
public AvistazIdInfo MovieTvinfo { get; set; }
[JsonPropertyName("created_at")]
public string CreatedAt { get; set; }
public AvistazIdInfo MovieTvinfo { get; init; }
[JsonPropertyName("created_at_iso")]
public string CreatedAtIso { get; set; }
public string CreatedAtIso { get; init; }
[JsonPropertyName("file_name")]
public string FileName { get; set; }
public string FileName { get; init; }
[JsonPropertyName("release_title")]
public string ReleaseTitle { get; init; }
[JsonPropertyName("info_hash")]
public string InfoHash { get; set; }
public int? Leech { get; set; }
public int? Completed { get; set; }
public int? Seed { get; set; }
public string InfoHash { get; init; }
public int? Leech { get; init; }
public int? Completed { get; init; }
public int? Seed { get; init; }
[JsonPropertyName("file_size")]
public long? FileSize { get; set; }
public long? FileSize { get; init; }
[JsonPropertyName("file_count")]
public int? FileCount { get; set; }
public int? FileCount { get; init; }
[JsonPropertyName("download_multiply")]
public double? DownloadMultiply { get; set; }
public double? DownloadMultiply { get; init; }
[JsonPropertyName("upload_multiply")]
public double? UploadMultiply { get; set; }
public double? UploadMultiply { get; init; }
[JsonPropertyName("video_quality")]
public string VideoQuality { get; set; }
public string Type { get; set; }
public List<AvistazLanguage> Audio { get; set; }
public List<AvistazLanguage> Subtitle { get; set; }
public string VideoQuality { get; init; }
public string Type { get; init; }
public string Format { get; init; }
public IReadOnlyCollection<AvistazLanguage> Audio { get; init; }
public IReadOnlyCollection<AvistazLanguage> Subtitle { get; init; }
}
public class AvistazLanguage
{
public int Id { get; set; }
public string Language { get; set; }
public int Id { get; init; }
public string Language { get; init; }
}
public class AvistazResponse
{
public List<AvistazRelease> Data { get; set; }
public IReadOnlyCollection<AvistazRelease> Data { get; init; }
}
public class AvistazErrorResponse
{
public string Message { get; set; }
public string Message { get; init; }
}
public class AvistazIdInfo
{
public string Tmdb { get; set; }
public string Tvdb { get; set; }
public string Imdb { get; set; }
public string Tmdb { get; init; }
public string Tvdb { get; init; }
public string Imdb { get; init; }
}
public class AvistazAuthResponse
{
public string Token { get; set; }
public string Token { get; init; }
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
@ -21,7 +20,7 @@ public abstract class AvistazBase : TorrentIndexerBase<AvistazSettings>
public override TimeSpan RateLimit => TimeSpan.FromSeconds(6);
public override IndexerCapabilities Capabilities => SetCapabilities();
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
private IIndexerRepository _indexerRepository;
private readonly IIndexerRepository _indexerRepository;
public AvistazBase(IIndexerRepository indexerRepository,
IIndexerHttpClient httpClient,
@ -57,7 +56,7 @@ protected override async Task DoLogin()
{
try
{
Settings.Token = await GetToken();
Settings.Token = await GetToken().ConfigureAwait(false);
if (Definition.Id > 0)
{
@ -66,7 +65,7 @@ protected override async Task DoLogin()
_logger.Debug("Avistaz authentication succeeded.");
}
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
catch (HttpException ex) when (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.UnprocessableEntity)
{
_logger.Warn(ex, "Failed to authenticate with Avistaz");
@ -90,11 +89,11 @@ protected override async Task<ValidationFailure> TestConnection()
{
try
{
await GetToken();
await GetToken().ConfigureAwait(false);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.UnprocessableEntity)
{
_logger.Warn(ex, "Unauthorized request to indexer");
@ -110,10 +109,10 @@ protected override async Task<ValidationFailure> TestConnection()
{
_logger.Warn(ex, "Unable to connect to indexer");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
}
return await base.TestConnection();
return await base.TestConnection().ConfigureAwait(false);
}
private async Task<string> GetToken()
@ -121,18 +120,17 @@ private async Task<string> GetToken()
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
Method = HttpMethod.Post
};
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
var authLoginRequest = requestBuilder
.Post()
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("pid", Settings.Pid.Trim())
.Accept(HttpAccept.Html)
.Accept(HttpAccept.Json)
.Build();
var response = await ExecuteAuth(authLoginRequest);
var response = await ExecuteAuth(authLoginRequest).ConfigureAwait(false);
if (!STJson.TryDeserialize<AvistazAuthResponse>(response.Content, out var authResponse))
{

View file

@ -24,7 +24,7 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
{
return releaseInfos.ToArray();
return releaseInfos;
}
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
@ -52,31 +52,28 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
foreach (var row in jsonResponse.Data)
{
var details = row.Url;
var link = row.Download;
var cats = ParseCategories(row);
var detailsUrl = row.Url;
var release = new TorrentInfo
{
Title = row.FileName,
DownloadUrl = link,
Guid = detailsUrl,
InfoUrl = detailsUrl,
Title = ParseTitle(row),
DownloadUrl = row.Download,
Categories = ParseCategories(row).ToList(),
InfoHash = row.InfoHash,
InfoUrl = details,
Guid = details,
Categories = cats,
PublishDate = DateTime.Parse(row.CreatedAtIso, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
Size = row.FileSize,
Files = row.FileCount,
Grabs = row.Completed,
Seeders = row.Seed,
Peers = row.Leech + row.Seed,
PublishDate = DateTime.Parse(row.CreatedAtIso, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
DownloadVolumeFactor = row.DownloadMultiply,
UploadVolumeFactor = row.UploadMultiply,
MinimumRatio = 1,
MinimumSeedTime = 259200, // 72 hours
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
Languages = row.Audio?.Select(x => x.Language).ToList() ?? [],
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? []
};
if (row.FileSize is > 0)
@ -90,54 +87,57 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
};
}
if (row.MovieTvinfo != null)
if (row.MovieTvinfo is not null)
{
release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault();
release.TmdbId = row.MovieTvinfo.Tmdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tmdb, out var tmdbResult) ? tmdbResult : 0;
release.TvdbId = row.MovieTvinfo.Tvdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
release.TmdbId = row.MovieTvinfo.Tmdb.IsNotNullOrWhiteSpace() && ParseUtil.TryCoerceInt(row.MovieTvinfo.Tmdb, out var tmdbResult) ? tmdbResult : 0;
release.TvdbId = row.MovieTvinfo.Tvdb.IsNotNullOrWhiteSpace() && ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
}
releaseInfos.Add(release);
}
// order by date
return releaseInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
// hook to adjust category parsing
protected virtual List<IndexerCategory> ParseCategories(AvistazRelease row)
protected virtual IReadOnlyList<IndexerCategory> ParseCategories(AvistazRelease row)
{
var cats = new List<IndexerCategory>();
var resolution = row.VideoQuality;
var categories = new List<IndexerCategory>();
var videoQuality = row.VideoQuality;
switch (row.Type)
switch (row.Type.ToUpperInvariant())
{
case "Movie":
cats.Add(resolution switch
case "MOVIE":
categories.Add(videoQuality switch
{
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD,
"2160p" => NewznabStandardCategory.MoviesUHD,
_ => NewznabStandardCategory.MoviesSD
});
break;
case "TV-Show":
cats.Add(resolution switch
case "TV-SHOW":
categories.Add(videoQuality switch
{
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.TVHD,
"2160p" => NewznabStandardCategory.TVUHD,
_ => NewznabStandardCategory.TVSD
});
break;
case "Music":
cats.Add(NewznabStandardCategory.Audio);
case "MUSIC":
categories.Add(NewznabStandardCategory.Audio);
break;
default:
throw new Exception($"Error parsing Avistaz category type {row.Type}");
throw new Exception($"Error parsing Avistaz category type \"{row.Type}\"");
}
return cats;
return categories;
}
protected virtual string ParseTitle(AvistazRelease row)
{
return row.FileName;
}
}
}

View file

@ -82,11 +82,10 @@ private IEnumerable<IndexerRequest> GetRequest(List<KeyValuePair<string, string>
{
var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString();
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}");
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.Token}");
request.HttpRequest.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
request.HttpRequest.SuppressHttpErrorStatusCodes = [HttpStatusCode.NotFound];
yield return request;
}

View file

@ -19,13 +19,7 @@ public class AvistazSettings : NoAuthTorrentBaseSettings
{
private static readonly AvistazSettingsValidator Validator = new();
public AvistazSettings()
{
Token = "";
FreeleechOnly = false;
}
public string Token { get; set; }
public string Token { get; set; } = string.Empty;
[FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }

View file

@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@ -112,25 +111,26 @@ public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCrit
{
var pageableRequests = new IndexerPageableRequestChain();
var queryParams = new NebulanceQuery
var queryParams = new NameValueCollection
{
Age = ">0"
{ "action", "search" },
{ "age", ">0" },
};
if (searchCriteria.TvMazeId is > 0)
{
queryParams.TvMaze = searchCriteria.TvMazeId.Value;
queryParams.Set("tvmaze", searchCriteria.TvMazeId.ToString());
}
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
{
queryParams.Imdb = searchCriteria.FullImdbId;
queryParams.Set("imdb", searchCriteria.FullImdbId);
}
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
if (searchQuery.IsNotNullOrWhiteSpace())
{
queryParams.Release = searchQuery;
queryParams.Set("release", searchQuery);
}
if (searchCriteria.Season.HasValue &&
@ -139,43 +139,43 @@ public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCrit
{
if (searchQuery.IsNotNullOrWhiteSpace())
{
queryParams.Name = searchQuery;
queryParams.Set("name", searchQuery);
}
queryParams.Release = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
queryParams.Set("release", showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture));
}
else
{
if (searchCriteria.Season.HasValue)
{
queryParams.Season = searchCriteria.Season.Value;
queryParams.Set("season", searchCriteria.Season.Value.ToString());
}
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber))
{
queryParams.Episode = episodeNumber;
queryParams.Set("episode", episodeNumber.ToString());
}
}
if ((queryParams.Season.HasValue || queryParams.Episode.HasValue) &&
queryParams.Name.IsNullOrWhiteSpace() &&
queryParams.Release.IsNullOrWhiteSpace() &&
!queryParams.TvMaze.HasValue &&
queryParams.Imdb.IsNullOrWhiteSpace())
if ((queryParams.Get("season").IsNotNullOrWhiteSpace() || queryParams.Get("episode").IsNotNullOrWhiteSpace()) &&
queryParams.Get("name").IsNullOrWhiteSpace() &&
queryParams.Get("release").IsNullOrWhiteSpace() &&
queryParams.Get("tvmaze").IsNullOrWhiteSpace() &&
queryParams.Get("imdb").IsNullOrWhiteSpace())
{
_logger.Debug("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
_logger.Warn("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
return new IndexerPageableRequestChain();
}
if (queryParams.Name is { Length: > 0 and < 3 } || queryParams.Release is { Length: > 0 and < 3 })
if (queryParams.Get("name") is { Length: > 0 and < 3 } || queryParams.Get("release") is { Length: > 0 and < 3 })
{
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
_logger.Warn("NBL API does not support release calls that are 2 characters or fewer.");
return new IndexerPageableRequestChain();
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria));
return pageableRequests;
}
@ -189,40 +189,45 @@ public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchC
{
var pageableRequests = new IndexerPageableRequestChain();
var queryParams = new NebulanceQuery
var queryParams = new NameValueCollection
{
Age = ">0"
{ "action", "search" },
{ "age", ">0" },
};
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
if (searchQuery.IsNotNullOrWhiteSpace())
{
queryParams.Release = searchQuery;
queryParams.Set("release", searchQuery);
}
if (queryParams.Release is { Length: > 0 and < 3 })
if (queryParams.Get("release") is { Length: > 0 and < 3 })
{
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
return new IndexerPageableRequestChain();
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
private IEnumerable<IndexerRequest> GetPagedRequests(NameValueCollection parameters, SearchCriteriaBase searchCriteria)
{
var apiUrl = _settings.BaseUrl + "api.php";
parameters.Set("api_key", _settings.ApiKey);
parameters.Set("per_page", searchCriteria.Limit.GetValueOrDefault(100).ToString());
var builder = new JsonRpcRequestBuilder(apiUrl)
.Call("getTorrents", _settings.ApiKey, parameters, results ?? 100, offset ?? 0);
if (searchCriteria.Limit > 0 && searchCriteria.Offset > 0)
{
var page = searchCriteria.Offset / searchCriteria.Limit;
parameters.Set("page", page.ToString());
}
builder.SuppressHttpError = true;
var apiUrl = $"{_settings.BaseUrl}api.php?{parameters.GetQueryString()}";
yield return new IndexerRequest(builder.Build());
yield return new IndexerRequest(apiUrl, HttpAccept.Json);
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
@ -244,16 +249,14 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
STJson.TryDeserialize<JsonRpcResponse<NebulanceErrorResponse>>(indexerResponse.HttpResponse.Content, out var errorResponse);
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request: {1}", indexerResponse.HttpResponse.StatusCode, errorResponse?.Result?.Error?.Message ?? "Check the logs for more information.");
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request. Check the logs for more information.", indexerResponse.HttpResponse.StatusCode);
}
JsonRpcResponse<NebulanceResponse> jsonResponse;
NebulanceResponse jsonResponse;
try
{
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceResponse>>(indexerResponse.HttpResponse.Content);
jsonResponse = STJson.Deserialize<NebulanceResponse>(indexerResponse.HttpResponse.Content);
}
catch (Exception ex)
{
@ -262,19 +265,17 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", ex, response?.Result ?? ex.Message);
}
if (jsonResponse.Error != null || jsonResponse.Result == null)
if (jsonResponse.Error != null)
{
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error?.Message);
}
if (jsonResponse.Result?.Items == null || jsonResponse.Result.Items.Count == 0)
if (jsonResponse.TotalResults == 0 || jsonResponse.Items == null || jsonResponse.Items.Count == 0)
{
return torrentInfos;
}
var rows = jsonResponse.Result.Items;
foreach (var row in rows)
foreach (var row in jsonResponse.Items)
{
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
@ -284,26 +285,30 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
Guid = details,
InfoUrl = details,
DownloadUrl = row.Download,
DownloadUrl = row.DownloadLink,
Title = title.Trim(),
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
Size = ParseUtil.CoerceLong(row.Size),
Files = row.FileList.Count(),
Size = row.Size,
Files = row.FileList.Count,
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
Grabs = ParseUtil.CoerceInt(row.Snatch),
Seeders = ParseUtil.CoerceInt(row.Seed),
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
Grabs = row.Snatch,
Seeders = row.Seed,
Peers = row.Seed + row.Leech,
Scene = row.Tags?.ContainsIgnoreCase("scene"),
MinimumRatio = 0, // ratioless
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
MinimumSeedTime = row.Category.ToUpperInvariant() == "SEASON" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
DownloadVolumeFactor = 0, // ratioless tracker
UploadVolumeFactor = 1,
PosterUrl = row.Banner
};
if (row.TvMazeId.IsNotNullOrWhiteSpace())
if (row.ImdbId.IsNotNullOrWhiteSpace())
{
release.TvMazeId = ParseUtil.CoerceInt(row.TvMazeId);
release.ImdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault();
}
if (row.TvMazeId is > 0)
{
release.TvMazeId = row.TvMazeId.Value;
}
torrentInfos.Add(release);
@ -326,100 +331,55 @@ public NebulanceSettings()
public string ApiKey { get; set; }
}
public class NebulanceQuery
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Id { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Time { get; set; }
[JsonProperty(PropertyName="age", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Age { get; set; }
[JsonProperty(PropertyName="tvmaze", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? TvMaze { get; set; }
[JsonProperty(PropertyName="imdb", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Imdb { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Hash { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string[] Tags { get; set; }
[JsonProperty(PropertyName="name", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty(PropertyName="release", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Release { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Category { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Series { get; set; }
[JsonProperty(PropertyName="season", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? Season { get; set; }
[JsonProperty(PropertyName="episode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? Episode { get; set; }
public NebulanceQuery Clone()
{
return MemberwiseClone() as NebulanceQuery;
}
}
public class NebulanceResponse
{
public List<NebulanceTorrent> Items { get; set; }
[JsonPropertyName("total_results")]
public int TotalResults { get; init; }
public IReadOnlyCollection<NebulanceTorrent> Items { get; init; }
public NebulanceErrorMessage Error { get; init; }
}
public class NebulanceTorrent
{
[JsonPropertyName("rls_name")]
public string ReleaseTitle { get; set; }
public string ReleaseTitle { get; init; }
[JsonPropertyName("cat")]
public string Category { get; set; }
public string Category { get; init; }
public string Size { get; set; }
public string Seed { get; set; }
public string Leech { get; set; }
public string Snatch { get; set; }
public string Download { get; set; }
public long Size { get; init; }
public int Seed { get; init; }
public int Leech { get; init; }
public int Snatch { get; init; }
[JsonPropertyName("download")]
public string DownloadLink { get; init; }
[JsonPropertyName("file_list")]
public IEnumerable<string> FileList { get; set; } = Array.Empty<string>();
public IReadOnlyCollection<string> FileList { get; init; } = [];
[JsonPropertyName("group_name")]
public string GroupName { get; set; }
[JsonPropertyName("series_banner")]
public string Banner { get; set; }
public string GroupName { get; init; }
[JsonPropertyName("group_id")]
public string TorrentId { get; set; }
public int TorrentId { get; init; }
[JsonPropertyName("series_id")]
public string TvMazeId { get; set; }
[JsonPropertyName("imdb_id")]
public string ImdbId { get; init; }
[JsonPropertyName("tvmaze_id")]
public int? TvMazeId { get; init; }
[JsonPropertyName("rls_utc")]
public string PublishDateUtc { get; set; }
public string PublishDateUtc { get; init; }
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
}
public class NebulanceErrorResponse
{
public NebulanceErrorMessage Error { get; set; }
public IReadOnlyCollection<string> Tags { get; init; } = [];
}
public class NebulanceErrorMessage
{
public string Message { get; set; }
public string Message { get; init; }
}
}

View file

@ -10,7 +10,7 @@ public class PrivateHD : AvistazBase
{
public override string Name => "PrivateHD";
public override string[] IndexerUrls => new[] { "https://privatehd.to/" };
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeZ";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public PrivateHD(IIndexerRepository indexerRepository,

View file

@ -233,23 +233,22 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
return releaseInfos; // no results
}
var headerColumns = table.QuerySelectorAll("tbody > tr > td.cat_Head").Select(x => x.TextContent).ToList();
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type"));
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name"));
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size"));
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeders"));
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leechers"));
var headerColumns = table.QuerySelectorAll("thead > tr > th.cat_Head")
.Select(x => x.GetAttribute("title").IsNotNullOrWhiteSpace() ? x.GetAttribute("title") : x.TextContent)
.ToList();
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeder(s)", StringComparison.OrdinalIgnoreCase));
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leecher(s)", StringComparison.OrdinalIgnoreCase));
var rows = dom.QuerySelectorAll("tr.browse");
var rows = table.QuerySelectorAll("tbody > tr");
foreach (var row in rows)
{
var qDescCol = row.Children[nameIndex];
var qLink = qDescCol.QuerySelector("a");
// Clean up title
qLink.QuerySelectorAll("font[color=\"green\"]").ToList().ForEach(e => e.Remove());
var title = qLink.TextContent.Trim();
var title = qLink.QuerySelector("span.torrent-text").TextContent.Trim();
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
@ -274,7 +273,7 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
Size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent),
Seeders = seeders,
Peers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent.Trim()) + seeders,
DownloadVolumeFactor = row.QuerySelector("font > b:contains(Freeleech)") != null ? 0 : 1,
DownloadVolumeFactor = row.QuerySelector("span.tag.free") is not null ? 0 : 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 259200 // 72 hours

View file

@ -94,7 +94,9 @@ protected override async Task DoLogin()
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return (httpResponse.HasHttpRedirect && httpResponse.RedirectUrl.ContainsIgnoreCase("login")) || httpResponse.Content.ContainsIgnoreCase("sign in now");
return (httpResponse.HasHttpRedirect && httpResponse.RedirectUrl.ContainsIgnoreCase("login")) ||
httpResponse.Content.ContainsIgnoreCase("sign in now") ||
(httpResponse.Content.ContainsIgnoreCase("fullRedirect") && httpResponse.Content.ContainsIgnoreCase("login"));
}
private IndexerCapabilities SetCapabilities()
@ -168,9 +170,9 @@ private IEnumerable<IndexerRequest> GetPagedRequests(string term)
if (term.IsNotNullOrWhiteSpace())
{
var request = new HttpRequestBuilder(_settings.BaseUrl + "search").Post()
.AddFormParameter("search", term)
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
var request = new HttpRequestBuilder(_settings.BaseUrl + "search")
.AddQueryParam("search", term)
.AddQueryParam("portlet", "true")
.SetHeader("X-Requested-With", "XMLHttpRequest")
.SetHeader("Referer", _settings.BaseUrl)
.Accept(HttpAccept.Html)
@ -195,7 +197,7 @@ private static string FixSearchTerm(string term)
term = Regex.Replace(term, @"(.+)\b\d{4}(\.\d{2}\.\d{2})?\b", "$1");
term = Regex.Replace(term, @"[\.\s\(\)\[\]]+", " ");
return term.ToLower().Trim();
return term.ToLowerInvariant().Trim();
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
@ -260,15 +262,15 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
var showPageUrl = new HttpRequestBuilder(_settings.BaseUrl + "show")
.AddQueryParam("id", show.GetAttribute("data-id"))
.Build()
.Url.FullUri;
.Url
.FullUri;
var showRequest = new HttpRequestBuilder(_settings.BaseUrl + "show").Post()
var showRequest = new HttpRequestBuilder(_settings.BaseUrl + "show")
.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")
.AddQueryParam("portlet", "true")
.AddQueryParam("tab", "true")
.SetHeader("X-Requested-With", "XMLHttpRequest")
.SetHeader("Referer", showPageUrl)
.Accept(HttpAccept.Html)

View file

@ -804,7 +804,7 @@
"DownloadClientRTorrentSettingsAddStopped": "Afegeix aturat",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "En activar s'afegiran torrents i imants a rTorrent en un estat aturat. Això pot trencar els fitxers magnet.",
"DownloadClientFreeboxSettingsAppTokenHelpText": "S'ha recuperat el testimoni de l'aplicació en crear l'accés a l'API de Freebox (ex: 'app_token')",
"DownloadClientUTorrentProviderMessage": "uTorrent té un historial d'inclusió de criptominers, programari maliciós i anuncis, us animem a triar un client diferent.",
"DownloadClientUTorrentProviderMessage": "uTorrent té historial d'incloure criptominers, malware i anuncis, suggerim fortament que escolleixis un client diferent.",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estat inicial dels torrents afegits a qBittorrent. Tingueu en compte que els torrents forçats no compleixen amb les restriccions de llavors",
"LogSizeLimit": "Límit de la mida del registre",
"SelectDownloadClientModalTitle": "{modalTitle} - Seleccioneu el client de baixada"

View file

@ -643,7 +643,7 @@
"DownloadClientRTorrentSettingsUrlPathHelpText": "Az XMLRPC végpont elérési útja, lásd: {url}. ruTorrent használata esetén ez általában az RPC2 vagy a [ruTorrent elérési útvonala]{url2}.",
"DownloadClientSettingsDestinationHelpText": "A letöltési útvonal kézi beállítása. Ha üresen marad, az alapértelmezett lesz használva",
"DownloadClientSettingsUrlBaseHelpText": "Prefixet ad a(z) {clientName} hivatkozáshoz, például: {url}",
"DownloadClientUTorrentProviderMessage": "uTorrent korábban többször is tartalmazott kriptobányászokat, kártevőket és hirdetéseket, ezért erősen javasoljuk, hogy válasszon másik klienst.",
"DownloadClientUTorrentProviderMessage": "A uTorrent múltjában előfordult, hogy kriptobányászót, kártevőket és reklámokat tartalmazott, ezért erősen javasoljuk, hogy válasszon egy másik klienst.",
"TheLogLevelDefault": "A naplózási szint alapértéke a 'Debug', ez megváltoztatható az Általános beállítások/beállítások/általános menüpontban",
"IndexerHDBitsSettingsCodecsHelpText": "Ha nincs megadva, minden lehetőség felhasználásra kerül.",
"IndexerHDBitsSettingsMediumsHelpText": "Ha nincs megadva, minden lehetőség felhasználásra kerül.",

View file

@ -5,7 +5,7 @@
"AddingTag": "Legger til tag",
"All": "Alle",
"Analytics": "Analyse",
"AppDataDirectory": "AppData -katalog",
"AppDataDirectory": "AppData Katalog",
"ApplyTags": "Bruk Tags",
"Authentication": "Godkjenning",
"Automatic": "Automatisk",
@ -191,5 +191,24 @@
"SyncProfiles": "Legg til forsinkelsesprofil",
"DeleteClientCategory": "Slett Nedlastingsklient",
"DeleteSelectedDownloadClients": "Nedlastingsklient",
"EditSelectedDownloadClients": "Nedlastingsklient"
"EditSelectedDownloadClients": "Nedlastingsklient",
"AppUpdated": "{appName} Oppdatert",
"AppUpdatedVersion": "{appName} er oppdatert til `{version}`, for å aktivere de siste endringene må du laste inn {appName} på ny.",
"AuthenticationMethodHelpTextWarning": "Vennligst velg en valid autentiserings metode.",
"AuthenticationRequired": "Verefisering påkrevd",
"AddCategory": "Legg til kategori",
"AddIndexerProxy": "Legg til indexer Proxy",
"AddRemoveOnly": "Legg til og fjern kun",
"AddToDownloadClient": "Legg til ny utgave i nedlastingsagenten",
"AddedToDownloadClient": "Ny utgave lagt til klient",
"AdvancedSettingsHiddenClickToShow": "Avanserte innstillinger er skjult. Klikk for å vise",
"AdvancedSettingsShownClickToHide": "Avanserte innstilinger vises.Klikk for å skjule",
"AppProfileInUse": "App profil i bruk",
"ApplicationLongTermStatusCheckAllClientMessage": "Alle indexere er ikke tilgjengelig på grunn av feil i mer enn 6 timer",
"Any": "Hvilken som helst",
"AuthenticationRequiredHelpText": "Endre hvilke forespørsler som krever autentisering. Ikke endre dette med mindre du forstår risikoen.",
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Gjenta nytt passord",
"AuthenticationRequiredPasswordHelpTextWarning": "Oppgi nytt passord",
"AuthenticationRequiredUsernameHelpTextWarning": "Oppgi nytt bruernavn",
"AuthenticationRequiredWarning": "For å forhindre ekstern tilgang uten pålogging, krever {appName} nå at autentisering er aktivert. Du kan velge å deaktivere autentisering for lokale adresser."
}

View file

@ -67,7 +67,7 @@
"Clear": "Wis",
"ClearHistory": "Geschiedenis verwijderen",
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van {appName} wilt verwijderen?",
"ClientPriority": "Client Prioriteit",
"ClientPriority": "Client prioriteit",
"CloneProfile": "Dupliceer Profiel",
"Close": "Sluit",
"CloseCurrentModal": "Sluit Huidig Bericht",

View file

@ -503,7 +503,7 @@
"ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas",
"Implementation": "Implementação",
"SelectIndexers": "Pesquisar indexadores",
"ApplyTagsHelpTextHowToApplyApplications": "Como aplicar tags ao autor selecionado",
"ApplyTagsHelpTextHowToApplyApplications": "Como aplicar etiquetas aos aplicativos selecionados",
"ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados",
"ApplyTagsHelpTextRemove": "Remover: remove as etiquetas inseridas",
"ApplyTagsHelpTextReplace": "Substituir: substitui as etiquetas atuais pelas inseridas (deixe em branco para limpar todas as etiquetas)",
@ -559,7 +559,7 @@
"AppUpdated": "{appName} atualizado",
"AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName}",
"ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.",
"RecentChanges": "Mudanças Recentes",
"RecentChanges": "Mudanças recentes",
"WhatsNew": "O que há de novo?",
"ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.",
"AddApplicationImplementation": "Adicionar Aplicativo - {implementationName}",
@ -587,7 +587,7 @@
"DisabledForLocalAddresses": "Desabilitado para endereços locais",
"External": "Externo",
"None": "Nenhum",
"ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave de API?",
"ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave da API?",
"AuthBasic": "Básico (pop-up do navegador)",
"ActiveIndexers": "Indexadores Ativos",
"ActiveApps": "Apps Ativos",
@ -631,7 +631,7 @@
"IndexerGazelleGamesSettingsSearchGroupNames": "Pesquisar Nomes de Grupos",
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Pesquisar lançamentos por nomes de grupos",
"IndexerHDBitsSettingsCodecs": "Codecs",
"IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.",
"IndexerHDBitsSettingsMediumsHelpText": "Se não for especificado, todas as opções serão usadas.",
"IndexerHDBitsSettingsOriginsHelpText": "Se não for especificado, todas as opções serão usadas.",
"IndexerHDBitsSettingsUseFilenames": "Usar nomes de arquivos",
"IndexerHDBitsSettingsUsernameHelpText": "Nome de Usuário do Site",
@ -643,10 +643,10 @@
"IndexerNzbIndexSettingsApiKeyHelpText": "Chave de API do site",
"IndexerOrpheusSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
"IndexerPassThePopcornSettingsApiKeyHelpText": "Chave de API do site",
"IndexerPassThePopcornSettingsApiUserHelpText": "Essas configurações são encontradas nas configurações de segurança do PassThePopcorn (Editar Perfil > Segurança).",
"IndexerPassThePopcornSettingsApiUserHelpText": "Essas configurações estão nas configurações de segurança do PassThePopcorn (Edit Profile [Editar perfil] > Security [Segurança]).",
"IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
"IndexerRedactedSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
"IndexerSettingsAdditionalParameters": "Parâmetros Adicionais",
"IndexerSettingsAdditionalParameters": "Parâmetros adicionais",
"IndexerSettingsApiPath": "Caminho da API",
"IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}",
"IndexerSettingsApiUser": "Usuário da API",
@ -669,7 +669,7 @@
"IndexerAlphaRatioSettingsExcludeScene": "Excluir SCENE",
"IndexerBeyondHDSettingsApiKeyHelpText": "Chave de API do site (encontrada em Minha segurança = chave de API)",
"IndexerBeyondHDSettingsSearchTypesHelpText": "Selecione os tipos de lançamentos nos quais você está interessado. Se nenhum for selecionado, todas as opções serão usadas.",
"IndexerHDBitsSettingsCodecsHelpText": "se não for especificado, todas as opções serão usadas.",
"IndexerHDBitsSettingsCodecsHelpText": "Se não for especificado, todas as opções serão usadas.",
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostrar apenas lançamentos freeleech",
"IndexerHDBitsSettingsUseFilenamesHelpText": "Marque esta opção se quiser usar nomes de arquivos torrent como títulos de lançamento",
"IndexerIPTorrentsSettingsCookieUserAgent": "Agente de Usuário para Cookies",
@ -763,17 +763,17 @@
"SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download",
"Any": "Quaisquer",
"Script": "Script",
"BuiltIn": "Embutido",
"BuiltIn": "Incorporado",
"InfoUrl": "URL de informações",
"PublishedDate": "Data de Publicação",
"Redirected": "Redirecionar",
"PublishedDate": "Data de publicação",
"Redirected": "Redirecionado",
"AverageQueries": "Média de Consultas",
"AverageGrabs": "Média de Capturas",
"AllSearchResultsHiddenByFilter": "Todos os resultados da pesquisa são ocultados pelo filtro aplicado.",
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
"LogSizeLimit": "Limite de Tamanho do Registro",
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.",
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha, ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
"LogSizeLimit": "Limite de tamanho do log",
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de log, em MB, antes do arquivamento. O padrão é 1 MB.",
"PreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
"IndexerSettingsPreferMagnetUrl": "Preferir URL Magnético",
"IndexerSettingsPreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
@ -781,7 +781,7 @@
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Apenas Golden Popcorn",
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Pesquisar somente lançamentos em Golden Popcorn",
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
"IndexerAvistazSettingsUsernameHelpText": "Nome de Usuário do Site",
"IndexerAvistazSettingsUsernameHelpText": "Nome de usuário do site",
"IndexerAvistazSettingsPasswordHelpText": "Senha do Site",
"IndexerAvistazSettingsPidHelpText": "PID da página Minha Conta ou Meu Perfil",
"IndexerAvistazSettingsUsernameHelpTextWarning": "Somente membros com rank e acima podem usar a API neste indexador.",
@ -795,17 +795,17 @@
"Logout": "Sair",
"NoEventsFound": "Nenhum evento encontrado",
"TheLogLevelDefault": "O nível de log padrão é ' Debug ' e pode ser alterado em [ Configurações gerais](/ configurações/geral)",
"UpdateAppDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,",
"UpdateAppDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,",
"UpdaterLogFiles": "Arquivos de log do atualizador",
"WouldYouLikeToRestoreBackup": "Gostaria de restaurar o backup '{name}'?",
"AptUpdater": "Usar apt para instalar atualizações",
"Install": "Instalar",
"InstallLatest": "Instalar o mais recente",
"InstallMajorVersionUpdate": "Instalar Atualização",
"InstallMajorVersionUpdate": "Instalar atualização",
"InstallMajorVersionUpdateMessage": "Esta atualização instalará uma nova versão principal e pode não ser compatível com o seu sistema. Tem certeza de que deseja instalar esta atualização?",
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.",
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para saber mais.",
"FailedToFetchSettings": "Falha ao obter configurações",
"CurrentlyInstalled": "Atualmente instalado",
"PreviouslyInstalled": "Instalado anteriormente",
"DownloadClientUTorrentProviderMessage": "O uTorrent tem um histórico de incluir criptomineradores, malware e anúncios, recomendamos que você escolha outro cliente de download."
"DownloadClientUTorrentProviderMessage": "O uTorrent tem um histórico de inclusão de criptomineradores, malware e anúncios. Recomendamos fortemente que você escolha um cliente diferente."
}

View file

@ -491,7 +491,7 @@
"Stats": "Status",
"CurrentlyInstalled": "În prezent instalat",
"Mixed": "Fix",
"Season": "Motiv",
"Season": "Sezon",
"ActiveIndexers": "Indexatorii activi",
"Any": "Oricare",
"AdvancedSettingsShownClickToHide": "Setări avansate afișate, click pentru a le ascunde",

View file

@ -72,7 +72,7 @@ public async Task<IActionResult> Login([FromForm] LoginResource resource, [FromQ
return Unauthorized();
}
if (returnUrl.IsNullOrWhiteSpace())
if (returnUrl.IsNullOrWhiteSpace() || !Url.IsLocalUrl(returnUrl))
{
return Redirect(_configFileProvider.UrlBase + "/");
}