diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..765e24fbc --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c1fd20bd6..7e81431cb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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)' diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs index 2277ed60d..6ddb4eb69 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -6,13 +6,14 @@ namespace NzbDrone.Common.Http { - public class HttpUri : IEquatable + public partial class HttpUri : IEquatable { - private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly string _uri; public string FullUri => _uri; + [GeneratedRegex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", 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"]; diff --git a/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs b/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs deleted file mode 100644 index 14b64979b..000000000 --- a/src/NzbDrone.Core/Indexers/Definitions/AnimeTorrents.cs +++ /dev/null @@ -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 - { - 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 GetCookies() - { - return CookieUtil.CookieHeaderToDictionary(Settings.Cookie); - } - - private IndexerCapabilities SetCapabilities() - { - var caps = new IndexerCapabilities - { - TvSearchParams = new List - { - TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep - }, - MovieSearchParams = new List - { - 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 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 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 { categoryMapping.FirstIfSingleOrDefault("0") }; - } - - public Func> GetCookies { get; set; } - public Action, 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 ParseResponse(IndexerResponse indexerResponse) - { - var releaseInfos = new List(); - - 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, 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; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Definitions/AnimeZ.cs b/src/NzbDrone.Core/Indexers/Definitions/AnimeZ.cs new file mode 100644 index 000000000..c8b9ce55f --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Definitions/AnimeZ.cs @@ -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 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 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.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + }, + BookSearchParams = new List + { + 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> GetBasicSearchParameters(SearchCriteriaBase searchCriteria, string genre = null) + { + var parameters = new List> + { + { "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 ParseCategories(AvistazRelease row) + { + return categories.MapTrackerCatToNewznab(row.Format).ToList(); + } + + protected override string ParseTitle(AvistazRelease row) + { + return row.ReleaseTitle.IsNotNullOrWhiteSpace() ? row.ReleaseTitle : row.FileName; + } +} diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs index 74f115368..754ae013a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazApi.cs @@ -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 Category { get; set; } + public string Url { get; init; } + public string Download { get; init; } + public Dictionary 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 Audio { get; set; } - public List Subtitle { get; set; } + public string VideoQuality { get; init; } + + public string Type { get; init; } + + public string Format { get; init; } + + public IReadOnlyCollection Audio { get; init; } + public IReadOnlyCollection 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 Data { get; set; } + public IReadOnlyCollection 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; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs index b14bac702..fb1b43276 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazBase.cs @@ -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 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 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 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 GetToken() @@ -121,18 +120,17 @@ private async Task 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(response.Content, out var authResponse)) { diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs index 6411c858f..31255aa8c 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazParserBase.cs @@ -24,7 +24,7 @@ public IList 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 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(), - Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List() + 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 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 ParseCategories(AvistazRelease row) + protected virtual IReadOnlyList ParseCategories(AvistazRelease row) { - var cats = new List(); - var resolution = row.VideoQuality; + var categories = new List(); + 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; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs index 6513cc913..5265fdd11 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazRequestGenerator.cs @@ -82,11 +82,10 @@ private IEnumerable GetRequest(List { 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; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs index bb431c47b..e67cf7c7f 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Avistaz/AvistazSettings.cs @@ -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; } diff --git a/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs b/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs index dcc26338e..68cd81f87 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Nebulance.cs @@ -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 GetPagedRequests(NebulanceQuery parameters, int? results, int? offset) + private IEnumerable 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> GetCookies { get; set; } @@ -244,16 +249,14 @@ public IList ParseResponse(IndexerResponse indexerResponse) if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) { - STJson.TryDeserialize>(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 jsonResponse; + NebulanceResponse jsonResponse; try { - jsonResponse = STJson.Deserialize>(indexerResponse.HttpResponse.Content); + jsonResponse = STJson.Deserialize(indexerResponse.HttpResponse.Content); } catch (Exception ex) { @@ -262,19 +265,17 @@ public IList 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 ParseResponse(IndexerResponse indexerResponse) { Guid = details, InfoUrl = details, - DownloadUrl = row.Download, + DownloadUrl = row.DownloadLink, Title = title.Trim(), Categories = new List { 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 Items { get; set; } + [JsonPropertyName("total_results")] + public int TotalResults { get; init; } + + public IReadOnlyCollection 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 FileList { get; set; } = Array.Empty(); + public IReadOnlyCollection 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 Tags { get; set; } = Array.Empty(); - } - - public class NebulanceErrorResponse - { - public NebulanceErrorMessage Error { get; set; } + public IReadOnlyCollection Tags { get; init; } = []; } public class NebulanceErrorMessage { - public string Message { get; set; } + public string Message { get; init; } } } diff --git a/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs b/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs index 21fc76ede..c6ec1c471 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/PrivateHD.cs @@ -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, diff --git a/src/NzbDrone.Core/Indexers/Definitions/SceneTime.cs b/src/NzbDrone.Core/Indexers/Definitions/SceneTime.cs index 9a0f430a0..fba3a624a 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/SceneTime.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/SceneTime.cs @@ -233,23 +233,22 @@ public IList 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 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 diff --git a/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs b/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs index d2118bcb0..f799ecdfb 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/Shazbat.cs @@ -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 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> GetCookies { get; set; } @@ -260,15 +262,15 @@ public IList 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()) .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) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index bc3ec68b0..7ea9b6a0e 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -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" diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index cd3bb8a02..fa2318eb1 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -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.", diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index cee150843..e729d0c6f 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -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." } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 0a8ae9244..f9b6c4839 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -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", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 410f22c29..1d55dcbf9 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -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." } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index ab29f0891..0ef3693df 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -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", diff --git a/src/Prowlarr.Http/Authentication/AuthenticationController.cs b/src/Prowlarr.Http/Authentication/AuthenticationController.cs index 05b058895..78e6784d1 100644 --- a/src/Prowlarr.Http/Authentication/AuthenticationController.cs +++ b/src/Prowlarr.Http/Authentication/AuthenticationController.cs @@ -72,7 +72,7 @@ public async Task Login([FromForm] LoginResource resource, [FromQ return Unauthorized(); } - if (returnUrl.IsNullOrWhiteSpace()) + if (returnUrl.IsNullOrWhiteSpace() || !Url.IsLocalUrl(returnUrl)) { return Redirect(_configFileProvider.UrlBase + "/"); }