Compare commits

...

20 Commits

Author SHA1 Message Date
Bogdan
8cd9ad01c2 Fixed: (Indexers) Use the defined names for C# indexers 2023-01-22 20:08:30 +02:00
Bogdan
ce2f322478 New: Add Anidex 2023-01-22 19:50:41 +02:00
Bogdan
0487309ee8 New: Add Toloka.to 2023-01-22 19:49:51 +02:00
Qstick
9862584611 Fixed: Catch InvalidDataException during initial config to prevent boot loop 2023-01-21 17:19:06 -06:00
Qstick
6a00e0db90 Filter useless PG Errors from coming to Sentry 2023-01-21 17:16:54 -06:00
Qstick
c93831dd8b Fixed: (TorrentBytes) Avoid null exception in DoLogin error handling 2023-01-21 16:51:19 -06:00
Bogdan
6546ba773c New: (Notification) Apprise 2023-01-21 20:24:28 +02:00
Qstick
4c3484a898 New: (Notifications) Add Ntfy 2023-01-21 12:14:21 -06:00
Qstick
8561b862f9 New: (Notifications) Add Simplepush 2023-01-21 12:13:50 -06:00
Bogdan
e1032fb0f5 New: Add optional app minimum seeders per indexer 2023-01-21 11:26:09 -06:00
Bogdan
4063219430 Fixed: (Orpheus) Title improvements to include ReleaseType and fix categories 2023-01-21 11:25:35 -06:00
Bogdan
e008be8581 Fixed: (Redacted) Search requests, title improvements 2023-01-21 11:25:35 -06:00
Bogdan
d6b379df64 Fixed: Validation inheritance 2023-01-19 21:04:08 -06:00
Bogdan
27094ccf62 Fixed: (ImmortalSeed) Improve tv search with season+ep and parsing, add MR/MST 2023-01-18 18:46:27 -06:00
Bogdan
edf9473e9a Fixed: (TorrentDay) Add freeleech only setting 2023-01-18 18:44:22 -06:00
Qstick
a0d11e7e33 Bump version to 1.1.1 2023-01-16 21:06:48 -06:00
Bogdan
7729eb398a Fixed: (Nebulance) CS cleanup 2023-01-16 19:48:04 -06:00
Bogdan
989564dbce Fixed: (IPTorrents) Improve clean title 2023-01-16 19:48:04 -06:00
Bogdan
c1f917f1ac Fixed: (SpeedCD) Improve clean title 2023-01-16 19:48:04 -06:00
Bogdan
4b7e47c397 Fixed: (RetroFlix) Update description and improve clean title 2023-01-16 19:48:04 -06:00
63 changed files with 1925 additions and 370 deletions

View File

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

View File

@@ -8,6 +8,7 @@ using System.Threading;
using NLog;
using NLog.Common;
using NLog.Targets;
using Npgsql;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry;
@@ -34,6 +35,14 @@ namespace NzbDrone.Common.Instrumentation.Sentry
SQLiteErrorCode.Auth
};
private static readonly HashSet<string> FilteredPostgresErrorCodes = new HashSet<string>
{
PostgresErrorCodes.OutOfMemory,
PostgresErrorCodes.TooManyConnections,
PostgresErrorCodes.DiskFull,
PostgresErrorCodes.ProgramLimitExceeded
};
// use string and not Type so we don't need a reference to the project
// where these are defined
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string>
@@ -239,6 +248,19 @@ namespace NzbDrone.Common.Instrumentation.Sentry
return false;
}
var pgEx = logEvent.Exception as PostgresException;
if (pgEx != null && FilteredPostgresErrorCodes.Contains(pgEx.SqlState))
{
return false;
}
// We don't care about transient network and timeout errors
var npgEx = logEvent.Exception as NpgsqlException;
if (npgEx != null && npgEx.IsTransient)
{
return false;
}
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
{
return false;

View File

@@ -10,6 +10,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
torrentInfo.Title.Should().Be("The Beatles - Abbey Road [1969] [Album] [2.0 Mix 2019] [MP3 V2 (VBR)] [BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (indexer.Protocol == DownloadProtocol.Torrent)
{
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Applications.Radarr
if (indexer.Protocol == DownloadProtocol.Torrent)
{
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}

View File

@@ -192,7 +192,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (indexer.Protocol == DownloadProtocol.Torrent)
{
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;

View File

@@ -198,7 +198,7 @@ namespace NzbDrone.Core.Applications.Sonarr
if (indexer.Protocol == DownloadProtocol.Torrent)
{
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;

View File

@@ -192,7 +192,7 @@ namespace NzbDrone.Core.Applications.Whisparr
if (indexer.Protocol == DownloadProtocol.Torrent)
{
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = indexer.AppProfile.Value.MinimumSeeders;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.AppMinimumSeeders ?? indexer.AppProfile.Value.MinimumSeeders;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedRatio;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(026)]
public class torrentday_cookiesettings_to_torrentdaysettings : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Update.Table("Indexers").Set(new { ConfigContract = "TorrentDaySettings" }).Where(new { Implementation = "TorrentDay" });
}
}
}

View File

@@ -0,0 +1,361 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
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 Anidex : TorrentIndexerBase<AnidexSettings>
{
public override string Name => "Anidex";
public override string[] IndexerUrls => new[] { "https://anidex.info/" };
public override string Description => "Anidex is a Public torrent tracker and indexer, primarily for English fansub groups of anime";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Anidex(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new AnidexRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new AnidexParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q
}
};
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.TVAnime, "Anime - Sub");
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.TVAnime, "Anime - Raw");
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TVAnime, "Anime - Dub");
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.TVAnime, "LA - Sub");
caps.Categories.AddCategoryMapping("5", NewznabStandardCategory.TVAnime, "LA - Raw");
caps.Categories.AddCategoryMapping("6", NewznabStandardCategory.BooksEBook, "Light Novel");
caps.Categories.AddCategoryMapping("7", NewznabStandardCategory.BooksComics, "Manga - TLed");
caps.Categories.AddCategoryMapping("8", NewznabStandardCategory.BooksComics, "Manga - Raw");
caps.Categories.AddCategoryMapping("9", NewznabStandardCategory.AudioMP3, "♫ - Lossy");
caps.Categories.AddCategoryMapping("10", NewznabStandardCategory.AudioLossless, "♫ - Lossless");
caps.Categories.AddCategoryMapping("11", NewznabStandardCategory.AudioVideo, "♫ - Video");
caps.Categories.AddCategoryMapping("12", NewznabStandardCategory.PCGames, "Games");
caps.Categories.AddCategoryMapping("13", NewznabStandardCategory.PC0day, "Applications");
caps.Categories.AddCategoryMapping("14", NewznabStandardCategory.XXXImageSet, "Pictures");
caps.Categories.AddCategoryMapping("15", NewznabStandardCategory.XXX, "Adult Video");
caps.Categories.AddCategoryMapping("16", NewznabStandardCategory.Other, "Other");
return caps;
}
}
public class AnidexRequestGenerator : IIndexerRequestGenerator
{
private readonly AnidexSettings _settings;
private readonly IndexerCapabilities _capabilities;
public AnidexRequestGenerator(AnidexSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var parameters = new NameValueCollection
{
{ "page", "search" },
{ "s", "upload_timestamp" },
{ "o", "desc" },
{ "group_id", "0" }, // All groups
{ "q", term ?? string.Empty }
};
if (_settings.AuthorisedOnly)
{
parameters.Add("a", "1");
}
var searchUrl = $"{_settings.BaseUrl}?{parameters.GetQueryString()}";
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Any())
{
searchUrl += "&id=" + string.Join(",", queryCats);
}
if (_settings.LanguagesOnly.Any())
{
searchUrl += "&lang_id=" + string.Join(",", _settings.LanguagesOnly);
}
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnidexParser : IParseIndexerResponse
{
private readonly AnidexSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public AnidexParser(AnidexSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Anidex search returned unexpected result. Expected 200 OK but got {indexerResponse.HttpResponse.StatusCode}.");
}
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("div#content table > tbody > tr");
foreach (var row in rows)
{
var downloadUrl = _settings.BaseUrl + row.QuerySelector("a[href^=\"/dl/\"]")?.GetAttribute("href");
var infoUrl = _settings.BaseUrl + row.QuerySelector("td:nth-child(3) a")?.GetAttribute("href");
var title = row.QuerySelector("td:nth-child(3) span")?.GetAttribute("title")?.Trim();
var language = row.QuerySelector("td:nth-child(1) img")?.GetAttribute("title")?.Trim();
if (language.IsNotNullOrWhiteSpace())
{
title += $" [{language}]";
}
var categoryLink = row.QuerySelector("td:nth-child(1) a").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "id");
var categories = _categories.MapTrackerCatToNewznab(cat);
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)")?.TextContent);
var peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(10)")?.TextContent.Trim());
var added = row.QuerySelector("td:nth-child(8)").GetAttribute("title").Trim();
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadUrl,
MagnetUrl = row.QuerySelector("a[href^=\"magnet:?\"]")?.GetAttribute("href"),
Title = title,
Categories = categories,
Seeders = seeders,
Peers = peers,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(7)")?.TextContent.Trim()),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(11)")?.TextContent),
PublishDate = DateTime.ParseExact(added, "yyyy-MM-dd HH:mm:ss UTC", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1
};
releaseInfos.Add(release);
}
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnidexSettings : NoAuthTorrentBaseSettings
{
public AnidexSettings()
{
AuthorisedOnly = false;
LanguagesOnly = Array.Empty<int>();
}
[FieldDefinition(2, Label = "Authorised Only", Type = FieldType.Checkbox, HelpText = "Search authorised torrents only")]
public bool AuthorisedOnly { get; set; }
[FieldDefinition(3, Label = "Languages Only", Type = FieldType.Select, SelectOptions = typeof(AnidexLanguage), HelpText = "Search selected languages only. None ticked = ALL.")]
public IEnumerable<int> LanguagesOnly { get; set; }
}
public enum AnidexLanguage
{
[FieldOption(Hint = "English")]
GB = 1,
[FieldOption(Hint = "Japanese")]
JP = 2,
[FieldOption(Hint = "Polish")]
PL = 3,
[FieldOption(Hint = "Serbo-Croatian")]
RS = 4,
[FieldOption(Hint = "Dutch")]
NL = 5,
[FieldOption(Hint = "Italian")]
IT = 6,
[FieldOption(Hint = "Russian")]
RU = 7,
[FieldOption(Hint = "German")]
DE = 8,
[FieldOption(Hint = "Hungarian")]
HU = 9,
[FieldOption(Hint = "French")]
FR = 10,
[FieldOption(Hint = "Finnish")]
FI = 11,
[FieldOption(Hint = "Vietnamese")]
VN = 12,
[FieldOption(Hint = "Greek")]
GR = 13,
[FieldOption(Hint = "Bulgarian")]
BG = 14,
[FieldOption(Hint = "Spanish (Spain)")]
ES = 15,
[FieldOption(Hint = "Portuguese (Brazil)")]
BR = 16,
[FieldOption(Hint = "Portuguese (Portugal)")]
PT = 17,
[FieldOption(Hint = "Swedish")]
SE = 18,
[FieldOption(Hint = "Arabic")]
SA = 19,
[FieldOption(Hint = "Danish")]
DK = 20,
[FieldOption(Hint = "Chinese (Simplified)")]
CN = 21,
[FieldOption(Hint = "Bengali")]
BD = 22,
[FieldOption(Hint = "Romanian")]
RO = 23,
[FieldOption(Hint = "Czech")]
CZ = 24,
[FieldOption(Hint = "Mongolian")]
MN = 25,
[FieldOption(Hint = "Turkish")]
TR = 26,
[FieldOption(Hint = "Indonesian")]
ID = 27,
[FieldOption(Hint = "Korean")]
KR = 28,
[FieldOption(Hint = "Spanish (LATAM)")]
MX = 29,
[FieldOption(Hint = "Persian")]
IR = 30,
[FieldOption(Hint = "Malaysian")]
MY = 31,
}
}

View File

@@ -521,7 +521,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class AnimeBytesSettingsValidator : AbstractValidator<AnimeBytesSettings>
public class AnimeBytesSettingsValidator : NoAuthSettingsValidator<AnimeBytesSettings>
{
public AnimeBytesSettingsValidator()
{
@@ -535,7 +535,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AnimeBytesSettings : NoAuthTorrentBaseSettings
{
private static readonly AnimeBytesSettingsValidator Validator = new AnimeBytesSettingsValidator();
private static readonly AnimeBytesSettingsValidator Validator = new ();
public AnimeBytesSettings()
{

View File

@@ -5,7 +5,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public class AvistazSettingsValidator : AbstractValidator<AvistazSettings>
public class AvistazSettingsValidator : NoAuthSettingsValidator<AvistazSettings>
{
public AvistazSettingsValidator()
{
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
public class AvistazSettings : NoAuthTorrentBaseSettings
{
private static readonly AvistazSettingsValidator Validator = new AvistazSettingsValidator();
private static readonly AvistazSettingsValidator Validator = new ();
public AvistazSettings()
{

View File

@@ -246,7 +246,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class BeyondHDSettingsValidator : AbstractValidator<BeyondHDSettings>
public class BeyondHDSettingsValidator : NoAuthSettingsValidator<BeyondHDSettings>
{
public BeyondHDSettingsValidator()
{
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class BeyondHDSettings : NoAuthTorrentBaseSettings
{
private static readonly BeyondHDSettingsValidator Validator = new BeyondHDSettingsValidator();
private static readonly BeyondHDSettingsValidator Validator = new ();
public BeyondHDSettings()
{

View File

@@ -219,7 +219,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public string BaseUrl { get; set; }
[FieldDefinition(2)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
public NzbDroneValidationResult Validate()
{

View File

@@ -5,7 +5,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.BroadcastheNet
{
public class BroadcastheNetSettingsValidator : AbstractValidator<BroadcastheNetSettings>
public class BroadcastheNetSettingsValidator : NoAuthSettingsValidator<BroadcastheNetSettings>
{
public BroadcastheNetSettingsValidator()
{
@@ -15,11 +15,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public class BroadcastheNetSettings : NoAuthTorrentBaseSettings
{
private static readonly BroadcastheNetSettingsValidator Validator = new BroadcastheNetSettingsValidator();
public BroadcastheNetSettings()
{
}
private static readonly BroadcastheNetSettingsValidator Validator = new ();
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }

View File

@@ -5,7 +5,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.FileList
{
public class FileListSettingsValidator : AbstractValidator<FileListSettings>
public class FileListSettingsValidator : NoAuthSettingsValidator<FileListSettings>
{
public FileListSettingsValidator()
{

View File

@@ -1,20 +1,26 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Gazelle
{
public class GazelleSettingsValidator : UserPassBaseSettingsValidator<GazelleSettings>
{
}
public class GazelleSettings : UserPassTorrentBaseSettings
{
public GazelleSettings()
{
}
private static readonly GazelleSettingsValidator Validator = new ();
public string AuthKey;
public string PassKey;
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use freeleech tokens when available")]
public bool UseFreeleechToken { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -431,7 +431,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class GazelleGamesSettingsValidator : AbstractValidator<GazelleGamesSettings>
public class GazelleGamesSettingsValidator : NoAuthSettingsValidator<GazelleGamesSettings>
{
public GazelleGamesSettingsValidator()
{
@@ -441,7 +441,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class GazelleGamesSettings : NoAuthTorrentBaseSettings
{
private static readonly GazelleGamesSettingsValidator Validator = new GazelleGamesSettingsValidator();
private static readonly GazelleGamesSettingsValidator Validator = new ();
public GazelleGamesSettings()
{

View File

@@ -6,7 +6,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.HDBits
{
public class HDBitsSettingsValidator : AbstractValidator<HDBitsSettings>
public class HDBitsSettingsValidator : NoAuthSettingsValidator<HDBitsSettings>
{
public HDBitsSettingsValidator()
{
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.Indexers.HDBits
public class HDBitsSettings : NoAuthTorrentBaseSettings
{
private static readonly HDBitsSettingsValidator Validator = new HDBitsSettingsValidator();
private static readonly HDBitsSettingsValidator Validator = new ();
public HDBitsSettings()
{

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.Indexers.Headphones
public class HeadphonesSettings : IIndexerSettings
{
private static readonly HeadphonesSettingsValidator Validator = new HeadphonesSettingsValidator();
private static readonly HeadphonesSettingsValidator Validator = new ();
public HeadphonesSettings()
{
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Headphones
public string Password { get; set; }
[FieldDefinition(4)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
public virtual NzbDroneValidationResult Validate()
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
@@ -19,7 +20,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public class IPTorrents : TorrentIndexerBase<IPTorrentsSettings>
{
public override string Name => "IPTorrents";
public override string[] IndexerUrls => new[]
{
"https://iptorrents.com/",
@@ -152,6 +152,7 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(81, NewznabStandardCategory.XXX, "XXX/Movie/0Day");
caps.Categories.AddCategoryMapping(91, NewznabStandardCategory.XXXPack, "XXX/Packs");
caps.Categories.AddCategoryMapping(84, NewznabStandardCategory.XXXImageSet, "XXX/Pics/Wallpapers");
return caps;
}
}
@@ -167,6 +168,16 @@ namespace NzbDrone.Core.Indexers.Definitions
var qc = new NameValueCollection();
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
{
qc.Add(cat, string.Empty);
}
if (Settings.FreeLeechOnly)
{
qc.Add("free", "on");
}
if (imdbId.IsNotNullOrWhiteSpace())
{
// ipt uses sphinx, which supports boolean operators and grouping
@@ -180,16 +191,6 @@ namespace NzbDrone.Core.Indexers.Definitions
qc.Add("q", "+(" + term + ")");
}
if (Settings.FreeLeechOnly)
{
qc.Add("free", "on");
}
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
{
qc.Add(cat, string.Empty);
}
if (offset > 0 && limit > 0)
{
var page = (int)(offset / limit) + 1;
@@ -278,7 +279,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var parser = new HtmlParser();
var doc = parser.ParseDocument(indexerResponse.Content);
var rows = doc.QuerySelectorAll("table[id='torrents'] > tbody > tr");
var rows = doc.QuerySelectorAll("table[id=\"torrents\"] > tbody > tr");
foreach (var row in rows)
{
var qTitleLink = row.QuerySelector("a.hv");
@@ -289,8 +290,7 @@ namespace NzbDrone.Core.Indexers.Definitions
continue;
}
// drop invalid char that seems to have cropped up in some titles. #6582
var title = qTitleLink.TextContent.Trim().Replace("\u000f", "");
var title = CleanTitle(qTitleLink.TextContent);
var details = new Uri(_settings.BaseUrl + qTitleLink.GetAttribute("href").TrimStart('/'));
var qLink = row.QuerySelector("a[href^=\"/download.php/\"]");
@@ -355,14 +355,23 @@ namespace NzbDrone.Core.Indexers.Definitions
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private static string CleanTitle(string title)
{
// drop invalid chars that seems to have cropped up in some titles. #6582
title = Regex.Replace(title, @"[\u0000-\u0008\u000A-\u001F\u0100-\uFFFF]", string.Empty, RegexOptions.Compiled);
title = Regex.Replace(title, @"[\(\[\{]REQ(UEST(ED)?)?[\)\]\}]", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return title.Trim(' ', '-', ':');
}
}
public class IPTorrentsSettings : CookieTorrentBaseSettings
{
[FieldDefinition(2, Label = "Cookie User-Agent", Type = FieldType.Textbox, HelpText = "User-Agent associated with cookie used from Browser")]
[FieldDefinition(3, Label = "Cookie User-Agent", Type = FieldType.Textbox, HelpText = "User-Agent associated with cookie used from Browser")]
public string UserAgent { get; set; }
[FieldDefinition(3, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search Freeleech torrents only")]
[FieldDefinition(4, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search Freeleech torrents only")]
public bool FreeLeechOnly { get; set; }
}
}

View File

@@ -29,19 +29,23 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerCapabilities Capabilities => SetCapabilities();
private string LoginUrl => Settings.BaseUrl + "takelogin.php";
public ImmortalSeed(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public ImmortalSeed(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new ImmortalSeedRequestGenerator { Settings = Settings, Capabilities = Capabilities };
return new ImmortalSeedRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new ImmortalSeedParser(Settings, Capabilities.Categories);
return new ImmortalSeedParser(Capabilities.Categories);
}
protected override async Task DoLogin()
@@ -49,15 +53,11 @@ namespace NzbDrone.Core.Indexers.Definitions
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
requestBuilder.Method = HttpMethod.Post;
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var cookies = Cookies;
Cookies = null;
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Indexers.Definitions
throw new IndexerAuthException("ImmortalSeed Auth Failed");
}
cookies = response.GetCookies();
var cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("ImmortalSeed authentication succeeded.");
@@ -156,15 +156,64 @@ namespace NzbDrone.Core.Indexers.Definitions
public class ImmortalSeedRequestGenerator : IIndexerRequestGenerator
{
public UserPassTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilities _capabilities;
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria)
public ImmortalSeedRequestGenerator(UserPassTorrentBaseSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedTvSearchString}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var parameters = new NameValueCollection();
var term = searchCriteria.SanitizedSearchTerm;
if (term.IsNotNullOrWhiteSpace())
{
parameters.Add("do", "search");
@@ -174,14 +223,14 @@ namespace NzbDrone.Core.Indexers.Definitions
parameters.Add("include_dead_torrents", "no");
}
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Count > 0)
if (queryCats.Any())
{
parameters.Add("selectedcats2", string.Join(",", queryCats));
}
var searchUrl = Settings.BaseUrl + "browse.php";
var searchUrl = _settings.BaseUrl + "browse.php";
if (parameters.Count > 0)
{
@@ -193,111 +242,66 @@ namespace NzbDrone.Core.Indexers.Definitions
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(searchCriteria));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(searchCriteria));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(searchCriteria));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(searchCriteria));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(searchCriteria));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class ImmortalSeedParser : IParseIndexerResponse
{
private readonly UserPassTorrentBaseSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public ImmortalSeedParser(UserPassTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
public ImmortalSeedParser(IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("#sortabletable tr:has(a[href*=\"details.php?id=\"])");
var rows = dom.QuerySelectorAll("table#sortabletable > tbody > tr:has(a[href*=\"details.php?id=\"])");
foreach (var row in rows)
{
var release = new TorrentInfo();
var qDetails = row.QuerySelector("div > a[href*=\"details.php?id=\"]"); // details link, release name get's shortened if it's to long
// details link, release name gets shortened if it's to long
var qDetails = row.QuerySelector("div > a[href*=\"details.php?id=\"]");
// use Title from tooltip or fallback to Details link if there's no tooltip
var qTitle = row.QuerySelector(".tooltip-content > div:nth-of-type(1)") ?? qDetails;
release.Title = qTitle.TextContent;
var title = qTitle?.TextContent.Trim();
var description = row.QuerySelector(".tooltip-content > div:nth-of-type(2)")?.TextContent.Replace("|", ",").Replace(" ", "").Trim();
var qDesciption = row.QuerySelectorAll(".tooltip-content > div");
if (qDesciption.Any())
{
release.Description = qDesciption[1].TextContent.Trim();
}
var infoUrl = qDetails?.GetAttribute("href");
var downloadUrl = row.QuerySelector("a[href*=\"download.php\"]")?.GetAttribute("href");
var qLink = row.QuerySelector("a[href*=\"download.php\"]");
release.DownloadUrl = qLink.GetAttribute("href");
release.Guid = release.DownloadUrl;
release.InfoUrl = qDetails.GetAttribute("href");
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(7)")?.TextContent);
var peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(8)")?.TextContent.Trim());
var categoryLink = row.QuerySelector("td:nth-of-type(1) a").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "category");
// 2021-03-17 03:39 AM
var dateString = row.QuerySelectorAll("td:nth-of-type(2) div").Last().LastChild.TextContent.Trim();
release.PublishDate = DateTime.ParseExact(dateString, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture);
var added = row.QuerySelector("td:nth-of-type(2) > div:last-child").LastChild.TextContent.Trim();
var sizeStr = row.QuerySelector("td:nth-of-type(5)").TextContent.Trim();
release.Size = ParseUtil.GetBytes(sizeStr);
release.Seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(7)").TextContent.Trim());
release.Peers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-of-type(8)").TextContent.Trim()) + release.Seeders;
var catLink = row.QuerySelector("td:nth-of-type(1) a").GetAttribute("href");
var catSplit = catLink.IndexOf("category=");
if (catSplit > -1)
var release = new TorrentInfo
{
catLink = catLink.Substring(catSplit + 9);
}
release.Categories = _categories.MapTrackerCatToNewznab(catLink);
var grabs = row.QuerySelector("td:nth-child(6)").TextContent;
release.Grabs = ParseUtil.CoerceInt(grabs);
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = downloadUrl,
Title = title,
Description = description,
Categories = _categories.MapTrackerCatToNewznab(cat),
Seeders = seeders,
Peers = peers,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(5)")?.TextContent.Trim()),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)")?.TextContent),
PublishDate = DateTime.ParseExact(added, "yyyy-MM-dd hh:mm tt", CultureInfo.InvariantCulture),
UploadVolumeFactor = row.QuerySelector("img[title^=\"x2 Torrent\"]") != null ? 2 : 1,
MinimumRatio = 1,
MinimumSeedTime = 86400 // 24 hours
};
if (row.QuerySelector("img[title^=\"Free Torrent\"]") != null)
{
@@ -312,19 +316,10 @@ namespace NzbDrone.Core.Indexers.Definitions
release.DownloadVolumeFactor = 1;
}
if (row.QuerySelector("img[title^=\"x2 Torrent\"]") != null)
{
release.UploadVolumeFactor = 2;
}
else
{
release.UploadVolumeFactor = 1;
}
torrentInfos.Add(release);
releaseInfos.Add(release);
}
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }

View File

@@ -480,7 +480,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class MyAnonamouseSettingsValidator : AbstractValidator<MyAnonamouseSettings>
public class MyAnonamouseSettingsValidator : NoAuthSettingsValidator<MyAnonamouseSettings>
{
public MyAnonamouseSettingsValidator()
{
@@ -490,7 +490,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class MyAnonamouseSettings : NoAuthTorrentBaseSettings
{
private static readonly MyAnonamouseSettingsValidator Validator = new MyAnonamouseSettingsValidator();
private static readonly MyAnonamouseSettingsValidator Validator = new ();
public MyAnonamouseSettings()
{

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class Nebulance : TorrentIndexerBase<NebulanceSettings>
{
public override string Name => "Nebulance";
public override string[] IndexerUrls => new string[] { "https://nebulance.io/" };
public override string[] IndexerUrls => new[] { "https://nebulance.io/" };
public override string Description => "Nebulance (NBL) is a ratioless Private Torrent Tracker for TV";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IParseIndexerResponse GetParser()
{
return new NebulanceParser(Settings, Capabilities.Categories);
return new NebulanceParser(Settings);
}
private IndexerCapabilities SetCapabilities()
@@ -49,9 +49,9 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvMazeId
}
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvMazeId
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
@@ -67,10 +67,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public NebulanceSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public NebulanceRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
{
var apiUrl = Settings.BaseUrl + "api.php";
@@ -85,16 +81,12 @@ namespace NzbDrone.Core.Indexers.Definitions
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
@@ -137,9 +129,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
@@ -168,19 +158,17 @@ namespace NzbDrone.Core.Indexers.Definitions
public class NebulanceParser : IParseIndexerResponse
{
private readonly NebulanceSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public NebulanceParser(NebulanceSettings settings, IndexerCapabilitiesCategories categories)
public NebulanceParser(NebulanceSettings settings)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
JsonRpcResponse<NebulanceTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
var jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
if (jsonResponse.Error != null || jsonResponse.Result == null)
{

View File

@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public class NewznabSettings : IIndexerSettings
{
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
private static readonly NewznabSettingsValidator Validator = new ();
public NewznabSettings()
{
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public string VipExpiration { get; set; }
[FieldDefinition(7)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
public List<IndexerCategory> Categories { get; set; }

View File

@@ -1168,7 +1168,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class NzbIndexSettings : IIndexerSettings
{
private static readonly NzbIndexSettingsValidator Validator = new NzbIndexSettingsValidator();
private static readonly NzbIndexSettingsValidator Validator = new ();
public NzbIndexSettings()
{
@@ -1182,7 +1182,9 @@ namespace NzbDrone.Core.Indexers.Definitions
public string ApiKey { get; set; }
[FieldDefinition(3)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings(); public NzbDroneValidationResult Validate()
public IndexerBaseSettings BaseSettings { get; set; } = new ();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -31,14 +31,18 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public Orpheus(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
public Orpheus(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new OrpheusRequestGenerator { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient };
return new OrpheusRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
@@ -52,7 +56,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Album, MusicSearchParam.Artist, MusicSearchParam.Label, MusicSearchParam.Year
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
},
BookSearchParams = new List<BookSearchParam>
{
@@ -62,11 +66,11 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio, "Music");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC, "Applications");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Books, "E-Books");
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook, "E-Books");
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Other, "E-Learning Videos");
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.Other, "Comedy");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.Books, "Comics");
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksComics, "Comics");
return caps;
}
@@ -114,11 +118,17 @@ namespace NzbDrone.Core.Indexers.Definitions
public class OrpheusRequestGenerator : IIndexerRequestGenerator
{
public OrpheusSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
private readonly OrpheusSettings _settings;
private readonly IndexerCapabilities _capabilities;
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public OrpheusRequestGenerator(OrpheusSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
@@ -135,11 +145,6 @@ namespace NzbDrone.Core.Indexers.Definitions
parameters.Add("groupname", searchCriteria.Album);
}
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
{
parameters.Add("recordlabel", searchCriteria.Label);
}
if (searchCriteria.Year.HasValue)
{
parameters.Add("year", searchCriteria.Year.ToString());
@@ -189,7 +194,7 @@ namespace NzbDrone.Core.Indexers.Definitions
parameters.Add("order_way", "desc");
parameters.Add("searchstr", term);
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Count > 0)
{
@@ -199,18 +204,18 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
var req = RequestBuilder()
.Resource($"ajax.php?{parameters.GetQueryString()}")
var request = RequestBuilder()
.Resource($"/ajax.php?{parameters.GetQueryString()}")
.Build();
yield return new IndexerRequest(req);
yield return new IndexerRequest(request);
}
private HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
return new HttpRequestBuilder($"{_settings.BaseUrl.TrimEnd('/')}")
.Accept(HttpAccept.Json)
.SetHeader("Authorization", $"token {Settings.Apikey}");
.SetHeader("Authorization", $"token {_settings.Apikey}");
}
}
@@ -339,11 +344,16 @@ namespace NzbDrone.Core.Indexers.Definitions
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
{
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear})";
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
{
title += " [" + result.ReleaseType + "]";
}
if (torrent.RemasterTitle.IsNotNullOrWhiteSpace())
{
title += $" [{string.Format("{0} {1}", torrent.RemasterTitle, torrent.RemasterYear).Trim()}]";
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
}
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
@@ -385,7 +395,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class OrpheusSettingsValidator : AbstractValidator<OrpheusSettings>
public class OrpheusSettingsValidator : NoAuthSettingsValidator<OrpheusSettings>
{
public OrpheusSettingsValidator()
{

View File

@@ -5,7 +5,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.PassThePopcorn
{
public class PassThePopcornSettingsValidator : AbstractValidator<PassThePopcornSettings>
public class PassThePopcornSettingsValidator : NoAuthSettingsValidator<PassThePopcornSettings>
{
public PassThePopcornSettingsValidator()
{
@@ -16,11 +16,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
public class PassThePopcornSettings : NoAuthTorrentBaseSettings
{
private static readonly PassThePopcornSettingsValidator Validator = new PassThePopcornSettingsValidator();
public PassThePopcornSettings()
{
}
private static readonly PassThePopcornSettingsValidator Validator = new ();
[FieldDefinition(2, Label = "API User", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
public string APIUser { get; set; }

View File

@@ -380,7 +380,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class PreToMeSettingsValidator : AbstractValidator<PreToMeSettings>
public class PreToMeSettingsValidator : NoAuthSettingsValidator<PreToMeSettings>
{
public PreToMeSettingsValidator()
{
@@ -392,7 +392,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class PreToMeSettings : NoAuthTorrentBaseSettings
{
private static readonly PreToMeSettingsValidator Validator = new PreToMeSettingsValidator();
private static readonly PreToMeSettingsValidator Validator = new ();
public PreToMeSettings()
{

View File

@@ -1,7 +1,5 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Rarbg
{

View File

@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
@@ -25,21 +24,25 @@ namespace NzbDrone.Core.Indexers.Definitions
public class Redacted : TorrentIndexerBase<RedactedSettings>
{
public override string Name => "Redacted";
public override string[] IndexerUrls => new string[] { "https://redacted.ch/" };
public override string[] IndexerUrls => new[] { "https://redacted.ch/" };
public override string Description => "REDActed (Aka.PassTheHeadPhones) is one of the most well-known music trackers.";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public Redacted(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
public Redacted(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new RedactedRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient };
return new RedactedRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
@@ -51,22 +54,14 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q, MusicSearchParam.Album, MusicSearchParam.Artist, MusicSearchParam.Label, MusicSearchParam.Year
},
{
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Year
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio, "Music");
@@ -105,21 +100,39 @@ namespace NzbDrone.Core.Indexers.Definitions
public class RedactedRequestGenerator : IIndexerRequestGenerator
{
public RedactedSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
private readonly RedactedSettings _settings;
private readonly IndexerCapabilities _capabilities;
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public RedactedRequestGenerator()
public RedactedRequestGenerator(RedactedSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
pageableRequests.Add(GetRequest(string.Format("&artistname={0}&groupname={1}", searchCriteria.Artist, searchCriteria.Album)));
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
{
parameters.Add("artistname", searchCriteria.Artist);
}
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
{
parameters.Add("groupname", searchCriteria.Album);
}
if (searchCriteria.Year.HasValue)
{
parameters.Add("year", searchCriteria.Year.ToString());
}
pageableRequests.Add(GetRequest(searchCriteria, parameters));
return pageableRequests;
}
@@ -127,8 +140,9 @@ namespace NzbDrone.Core.Indexers.Definitions
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
pageableRequests.Add(GetRequest(searchCriteria, parameters));
return pageableRequests;
}
@@ -146,26 +160,43 @@ namespace NzbDrone.Core.Indexers.Definitions
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
pageableRequests.Add(GetRequest(searchCriteria, parameters));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
private IEnumerable<IndexerRequest> GetRequest(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
{
var req = RequestBuilder()
.Resource($"ajax.php?action=browse&searchstr={searchParameters}")
var term = searchCriteria.SanitizedSearchTerm.Trim();
parameters.Add("action", "browse");
parameters.Add("order_by", "time");
parameters.Add("order_way", "desc");
parameters.Add("searchstr", term);
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (queryCats.Any())
{
foreach (var cat in queryCats)
{
parameters.Add($"filter_cat[{cat}]", "1");
}
}
var request = RequestBuilder()
.Resource($"/ajax.php?{parameters.GetQueryString()}")
.Build();
yield return new IndexerRequest(req);
yield return new IndexerRequest(request);
}
private HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
return new HttpRequestBuilder($"{_settings.BaseUrl.TrimEnd('/')}")
.Accept(HttpAccept.Json)
.SetHeader("Authorization", Settings.Apikey);
.SetHeader("Authorization", _settings.Apikey);
}
}
@@ -210,24 +241,14 @@ namespace NzbDrone.Core.Indexers.Definitions
foreach (var torrent in result.Torrents)
{
var id = torrent.TorrentId;
var artist = WebUtility.HtmlDecode(result.Artist);
var album = WebUtility.HtmlDecode(result.GroupName);
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
if (torrent.HasCue)
{
title += " [Cue]";
}
var title = GetTitle(result, torrent);
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
var release = new GazelleInfo
{
Guid = infoUrl,
// Splice Title from info to avoid calling API again for every torrent.
Title = WebUtility.HtmlDecode(title),
Container = torrent.Encoding,
Codec = torrent.Format,
Size = long.Parse(torrent.Size),
@@ -264,7 +285,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var id = result.TorrentId;
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
var release = new GazelleInfo
{
Guid = infoUrl,
Title = WebUtility.HtmlDecode(result.GroupName),
@@ -302,6 +323,30 @@ namespace NzbDrone.Core.Indexers.Definitions
.ToArray();
}
private string GetTitle(GazelleRelease result, GazelleTorrent torrent)
{
var title = $"{result.Artist} - {result.GroupName} [{result.GroupYear}]";
if (result.ReleaseType.IsNotNullOrWhiteSpace() && result.ReleaseType != "Unknown")
{
title += " [" + result.ReleaseType + "]";
}
if (torrent.RemasterTitle.IsNotNullOrWhiteSpace())
{
title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]";
}
title += $" [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
if (torrent.HasCue)
{
title += " [Cue]";
}
return title;
}
private string GetDownloadUrl(int torrentId, bool canUseToken)
{
// AuthKey is required but not checked, just pass in a dummy variable
@@ -326,7 +371,7 @@ namespace NzbDrone.Core.Indexers.Definitions
}
}
public class RedactedSettingsValidator : AbstractValidator<RedactedSettings>
public class RedactedSettingsValidator : NoAuthSettingsValidator<RedactedSettings>
{
public RedactedSettingsValidator()
{
@@ -336,7 +381,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class RedactedSettings : NoAuthTorrentBaseSettings
{
private static readonly RedactedSettingsValidator Validator = new RedactedSettingsValidator();
private static readonly RedactedSettingsValidator Validator = new ();
public RedactedSettings()
{

View File

@@ -9,9 +9,9 @@ namespace NzbDrone.Core.Indexers.Definitions
public class RetroFlix : SpeedAppBase
{
public override string Name => "RetroFlix";
public override string[] IndexerUrls => new string[] { "https://retroflix.club/" };
public override string[] LegacyUrls => new string[] { "https://retroflix.net/" };
public override string Description => "Private Torrent Tracker for Classic Movies / TV / General Releases";
public override string[] IndexerUrls => new[] { "https://retroflix.club/" };
public override string[] LegacyUrls => new[] { "https://retroflix.net/" };
public override string Description => "RetroFlix (RFX) is a Private Torrent Tracker for Classic Movies / TV / General Releases";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(2.1);
protected override int MinimumSeedTime => 432000; // 120 hours

View File

@@ -233,7 +233,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class SceneHDSettingsValidator : AbstractValidator<SceneHDSettings>
public class SceneHDSettingsValidator : NoAuthSettingsValidator<SceneHDSettings>
{
public SceneHDSettingsValidator()
{
@@ -243,7 +243,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class SceneHDSettings : NoAuthTorrentBaseSettings
{
private static readonly SceneHDSettingsValidator Validator = new SceneHDSettingsValidator();
private static readonly SceneHDSettingsValidator Validator = new ();
public SceneHDSettings()
{

View File

@@ -304,7 +304,7 @@ namespace NzbDrone.Core.Indexers.Definitions
return jsonResponse.Resource.Select(torrent => new TorrentInfo
{
Guid = torrent.Id.ToString(),
Title = Regex.Replace(torrent.Name, @"(?i:\[REQUESTED\])", "").Trim(' ', '.'),
Title = CleanTitle(torrent.Name),
Description = torrent.ShortDescription,
Size = torrent.Size,
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
@@ -323,9 +323,16 @@ namespace NzbDrone.Core.Indexers.Definitions
UploadVolumeFactor = torrent.UploadVolumeFactor,
}).ToArray();
}
private static string CleanTitle(string title)
{
title = Regex.Replace(title, @"\[REQUEST(ED)?\]", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return title.Trim(' ', '.');
}
}
public class SpeedAppSettingsValidator : AbstractValidator<SpeedAppSettings>
public class SpeedAppSettingsValidator : NoAuthSettingsValidator<SpeedAppSettings>
{
public SpeedAppSettingsValidator()
{

View File

@@ -30,7 +30,6 @@ namespace NzbDrone.Core.Indexers.Definitions
"https://speed.click/",
"https://speeders.me/"
};
public override string Description => "Your home now!";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
@@ -309,7 +308,7 @@ namespace NzbDrone.Core.Indexers.Definitions
foreach (var row in rows)
{
var title = Regex.Replace(row.QuerySelector("td:nth-child(2) > div > a[href^=\"/t/\"]").TextContent, @"(?i:\[REQ\])", "").Trim(' ', '.');
var title = CleanTitle(row.QuerySelector("td:nth-child(2) > div > a[href^=\"/t/\"]").TextContent);
var downloadUrl = new Uri(_settings.BaseUrl + row.QuerySelector("td:nth-child(4) a[href^=\"/download/\"]").GetAttribute("href").TrimStart('/'));
var infoUrl = new Uri(_settings.BaseUrl + row.QuerySelector("td:nth-child(2) > div > a[href^=\"/t/\"]").GetAttribute("href").TrimStart('/'));
var size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(6)").TextContent);
@@ -348,6 +347,13 @@ namespace NzbDrone.Core.Indexers.Definitions
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private static string CleanTitle(string title)
{
title = Regex.Replace(title, @"\[REQ(UEST)?\]", string.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return title.Trim(' ', '.');
}
}
public class SpeedCDSettings : UserPassTorrentBaseSettings

View File

@@ -0,0 +1,469 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
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 Toloka : TorrentIndexerBase<TolokaSettings>
{
public override string Name => "Toloka.to";
public override string[] IndexerUrls => new[] { "https://toloka.to/" };
public override string Description => "Toloka.to is a Semi-Private Ukrainian torrent site with a thriving file-sharing community";
public override string Language => "uk-UA";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPrivate;
public override IndexerCapabilities Capabilities => SetCapabilities();
public Toloka(IIndexerHttpClient httpClient,
IEventAggregator eventAggregator,
IIndexerStatusService indexerStatusService,
IConfigService configService,
Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new TolokaRequestGenerator(Settings, Capabilities);
}
public override IParseIndexerResponse GetParser()
{
return new TolokaParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var loginUrl = Settings.BaseUrl + "login.php";
var requestBuilder = new HttpRequestBuilder(loginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
var authLoginRequest = requestBuilder
.AddFormParameter("username", Settings.Username)
.AddFormParameter("password", Settings.Password)
.AddFormParameter("autologin", "on")
.AddFormParameter("ssl", "on")
.AddFormParameter("redirect", "")
.AddFormParameter("login", "Вхід")
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
.SetHeader("Referer", loginUrl)
.Build();
var response = await ExecuteAuth(authLoginRequest);
if (CheckIfLoginNeeded(response))
{
_logger.Debug(response.Content);
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("table.forumline table span.gen")?.FirstChild?.TextContent;
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
var cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Toloka.to authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return !httpResponse.Content.Contains("logout=true");
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping("117", NewznabStandardCategory.Movies, "Українське кіно");
caps.Categories.AddCategoryMapping("84", NewznabStandardCategory.Movies, "|-Мультфільми і казки");
caps.Categories.AddCategoryMapping("42", NewznabStandardCategory.Movies, "|-Художні фільми");
caps.Categories.AddCategoryMapping("124", NewznabStandardCategory.TV, "|-Телесеріали");
caps.Categories.AddCategoryMapping("125", NewznabStandardCategory.TV, "|-Мультсеріали");
caps.Categories.AddCategoryMapping("129", NewznabStandardCategory.Movies, "|-АртХаус");
caps.Categories.AddCategoryMapping("219", NewznabStandardCategory.Movies, "|-Аматорське відео");
caps.Categories.AddCategoryMapping("118", NewznabStandardCategory.Movies, "Українське озвучення");
caps.Categories.AddCategoryMapping("16", NewznabStandardCategory.Movies, "|-Фільми");
caps.Categories.AddCategoryMapping("32", NewznabStandardCategory.TV, "|-Телесеріали");
caps.Categories.AddCategoryMapping("19", NewznabStandardCategory.Movies, "|-Мультфільми");
caps.Categories.AddCategoryMapping("44", NewznabStandardCategory.TV, "|-Мультсеріали");
caps.Categories.AddCategoryMapping("127", NewznabStandardCategory.TVAnime, "|-Аніме");
caps.Categories.AddCategoryMapping("55", NewznabStandardCategory.Movies, "|-АртХаус");
caps.Categories.AddCategoryMapping("94", NewznabStandardCategory.MoviesOther, "|-Трейлери");
caps.Categories.AddCategoryMapping("144", NewznabStandardCategory.Movies, "|-Короткометражні");
caps.Categories.AddCategoryMapping("190", NewznabStandardCategory.Movies, "Українські субтитри");
caps.Categories.AddCategoryMapping("70", NewznabStandardCategory.Movies, "|-Фільми");
caps.Categories.AddCategoryMapping("192", NewznabStandardCategory.TV, "|-Телесеріали");
caps.Categories.AddCategoryMapping("193", NewznabStandardCategory.Movies, "|-Мультфільми");
caps.Categories.AddCategoryMapping("195", NewznabStandardCategory.TV, "|-Мультсеріали");
caps.Categories.AddCategoryMapping("194", NewznabStandardCategory.TVAnime, "|-Аніме");
caps.Categories.AddCategoryMapping("196", NewznabStandardCategory.Movies, "|-АртХаус");
caps.Categories.AddCategoryMapping("197", NewznabStandardCategory.Movies, "|-Короткометражні");
caps.Categories.AddCategoryMapping("225", NewznabStandardCategory.TVDocumentary, "Документальні фільми українською");
caps.Categories.AddCategoryMapping("21", NewznabStandardCategory.TVDocumentary, "|-Українські наукові документальні фільми");
caps.Categories.AddCategoryMapping("131", NewznabStandardCategory.TVDocumentary, "|-Українські історичні документальні фільми");
caps.Categories.AddCategoryMapping("226", NewznabStandardCategory.TVDocumentary, "|-BBC");
caps.Categories.AddCategoryMapping("227", NewznabStandardCategory.TVDocumentary, "|-Discovery");
caps.Categories.AddCategoryMapping("228", NewznabStandardCategory.TVDocumentary, "|-National Geographic");
caps.Categories.AddCategoryMapping("229", NewznabStandardCategory.TVDocumentary, "|-History Channel");
caps.Categories.AddCategoryMapping("230", NewznabStandardCategory.TVDocumentary, "|-Інші іноземні документальні фільми");
caps.Categories.AddCategoryMapping("119", NewznabStandardCategory.TVOther, "Телепередачі українською");
caps.Categories.AddCategoryMapping("18", NewznabStandardCategory.TVOther, "|-Музичне відео");
caps.Categories.AddCategoryMapping("132", NewznabStandardCategory.TVOther, "|-Телевізійні шоу та програми");
caps.Categories.AddCategoryMapping("157", NewznabStandardCategory.TVSport, "Український спорт");
caps.Categories.AddCategoryMapping("235", NewznabStandardCategory.TVSport, "|-Олімпіада");
caps.Categories.AddCategoryMapping("170", NewznabStandardCategory.TVSport, "|-Чемпіонати Європи з футболу");
caps.Categories.AddCategoryMapping("162", NewznabStandardCategory.TVSport, "|-Чемпіонати світу з футболу");
caps.Categories.AddCategoryMapping("166", NewznabStandardCategory.TVSport, "|-Чемпіонат та Кубок України з футболу");
caps.Categories.AddCategoryMapping("167", NewznabStandardCategory.TVSport, "|-Єврокубки");
caps.Categories.AddCategoryMapping("168", NewznabStandardCategory.TVSport, "|-Збірна України");
caps.Categories.AddCategoryMapping("169", NewznabStandardCategory.TVSport, "|-Закордонні чемпіонати");
caps.Categories.AddCategoryMapping("54", NewznabStandardCategory.TVSport, "|-Футбольне відео");
caps.Categories.AddCategoryMapping("158", NewznabStandardCategory.TVSport, "|-Баскетбол, хоккей, волейбол, гандбол, футзал");
caps.Categories.AddCategoryMapping("159", NewznabStandardCategory.TVSport, "|-Бокс, реслінг, бойові мистецтва");
caps.Categories.AddCategoryMapping("160", NewznabStandardCategory.TVSport, "|-Авто, мото");
caps.Categories.AddCategoryMapping("161", NewznabStandardCategory.TVSport, "|-Інший спорт, активний відпочинок");
// caps.Categories.AddCategoryMapping("136", NewznabStandardCategory.Other, "HD українською");
caps.Categories.AddCategoryMapping("96", NewznabStandardCategory.MoviesHD, "|-Фільми в HD");
caps.Categories.AddCategoryMapping("173", NewznabStandardCategory.TVHD, "|-Серіали в HD");
caps.Categories.AddCategoryMapping("139", NewznabStandardCategory.MoviesHD, "|-Мультфільми в HD");
caps.Categories.AddCategoryMapping("174", NewznabStandardCategory.TVHD, "|-Мультсеріали в HD");
caps.Categories.AddCategoryMapping("140", NewznabStandardCategory.TVDocumentary, "|-Документальні фільми в HD");
caps.Categories.AddCategoryMapping("120", NewznabStandardCategory.MoviesDVD, "DVD українською");
caps.Categories.AddCategoryMapping("66", NewznabStandardCategory.MoviesDVD, "|-Художні фільми та серіали в DVD");
caps.Categories.AddCategoryMapping("137", NewznabStandardCategory.MoviesDVD, "|-Мультфільми та мультсеріали в DVD");
caps.Categories.AddCategoryMapping("137", NewznabStandardCategory.TV, "|-Мультфільми та мультсеріали в DVD");
caps.Categories.AddCategoryMapping("138", NewznabStandardCategory.MoviesDVD, "|-Документальні фільми в DVD");
caps.Categories.AddCategoryMapping("237", NewznabStandardCategory.Movies, "Відео для мобільних (iOS, Android, Windows Phone)");
caps.Categories.AddCategoryMapping("33", NewznabStandardCategory.AudioVideo, "Звукові доріжки та субтитри");
caps.Categories.AddCategoryMapping("8", NewznabStandardCategory.Audio, "Українська музика (lossy)");
caps.Categories.AddCategoryMapping("23", NewznabStandardCategory.Audio, "|-Поп, Естрада");
caps.Categories.AddCategoryMapping("24", NewznabStandardCategory.Audio, "|-Джаз, Блюз");
caps.Categories.AddCategoryMapping("43", NewznabStandardCategory.Audio, "|-Етно, Фольклор, Народна, Бардівська");
caps.Categories.AddCategoryMapping("35", NewznabStandardCategory.Audio, "|-Інструментальна, Класична та неокласична");
caps.Categories.AddCategoryMapping("37", NewznabStandardCategory.Audio, "|-Рок, Метал, Альтернатива, Панк, СКА");
caps.Categories.AddCategoryMapping("36", NewznabStandardCategory.Audio, "|-Реп, Хіп-хоп, РнБ");
caps.Categories.AddCategoryMapping("38", NewznabStandardCategory.Audio, "|-Електронна музика");
caps.Categories.AddCategoryMapping("56", NewznabStandardCategory.Audio, "|-Невидане");
caps.Categories.AddCategoryMapping("98", NewznabStandardCategory.AudioLossless, "Українська музика (lossless)");
caps.Categories.AddCategoryMapping("100", NewznabStandardCategory.AudioLossless, "|-Поп, Естрада");
caps.Categories.AddCategoryMapping("101", NewznabStandardCategory.AudioLossless, "|-Джаз, Блюз");
caps.Categories.AddCategoryMapping("102", NewznabStandardCategory.AudioLossless, "|-Етно, Фольклор, Народна, Бардівська");
caps.Categories.AddCategoryMapping("103", NewznabStandardCategory.AudioLossless, "|-Інструментальна, Класична та неокласична");
caps.Categories.AddCategoryMapping("104", NewznabStandardCategory.AudioLossless, "|-Рок, Метал, Альтернатива, Панк, СКА");
caps.Categories.AddCategoryMapping("105", NewznabStandardCategory.AudioLossless, "|-Реп, Хіп-хоп, РнБ");
caps.Categories.AddCategoryMapping("106", NewznabStandardCategory.AudioLossless, "|-Електронна музика");
caps.Categories.AddCategoryMapping("11", NewznabStandardCategory.Books, "Друкована література");
caps.Categories.AddCategoryMapping("134", NewznabStandardCategory.Books, "|-Українська художня література (до 1991 р.)");
caps.Categories.AddCategoryMapping("177", NewznabStandardCategory.Books, "|-Українська художня література (після 1991 р.)");
caps.Categories.AddCategoryMapping("178", NewznabStandardCategory.Books, "|-Зарубіжна художня література");
caps.Categories.AddCategoryMapping("179", NewznabStandardCategory.Books, "|-Наукова література (гуманітарні дисципліни)");
caps.Categories.AddCategoryMapping("180", NewznabStandardCategory.Books, "|-Наукова література (природничі дисципліни)");
caps.Categories.AddCategoryMapping("183", NewznabStandardCategory.Books, "|-Навчальна та довідкова");
caps.Categories.AddCategoryMapping("181", NewznabStandardCategory.BooksMags, "|-Періодика");
caps.Categories.AddCategoryMapping("182", NewznabStandardCategory.Books, "|-Батькам та малятам");
caps.Categories.AddCategoryMapping("184", NewznabStandardCategory.BooksComics, "|-Графіка (комікси, манґа, BD та інше)");
caps.Categories.AddCategoryMapping("185", NewznabStandardCategory.AudioAudiobook, "Аудіокниги українською");
caps.Categories.AddCategoryMapping("135", NewznabStandardCategory.AudioAudiobook, "|-Українська художня література");
caps.Categories.AddCategoryMapping("186", NewznabStandardCategory.AudioAudiobook, "|-Зарубіжна художня література");
caps.Categories.AddCategoryMapping("187", NewznabStandardCategory.AudioAudiobook, "|-Історія, біографістика, спогади");
caps.Categories.AddCategoryMapping("189", NewznabStandardCategory.AudioAudiobook, "|-Сирий матеріал");
caps.Categories.AddCategoryMapping("9", NewznabStandardCategory.PC, "Windows");
caps.Categories.AddCategoryMapping("25", NewznabStandardCategory.PC, "|-Windows");
caps.Categories.AddCategoryMapping("199", NewznabStandardCategory.PC, "|-Офіс");
caps.Categories.AddCategoryMapping("200", NewznabStandardCategory.PC, "|-Антивіруси та безпека");
caps.Categories.AddCategoryMapping("201", NewznabStandardCategory.PC, "|-Мультимедія");
caps.Categories.AddCategoryMapping("202", NewznabStandardCategory.PC, "|-Утиліти, обслуговування, мережа");
caps.Categories.AddCategoryMapping("239", NewznabStandardCategory.PC, "Linux, Mac OS");
caps.Categories.AddCategoryMapping("26", NewznabStandardCategory.PC, "|-Linux");
caps.Categories.AddCategoryMapping("27", NewznabStandardCategory.PCMac, "|-Mac OS");
// caps.Categories.AddCategoryMapping("240", NewznabStandardCategory.PC, "Інші OS");
caps.Categories.AddCategoryMapping("211", NewznabStandardCategory.PCMobileAndroid, "|-Android");
caps.Categories.AddCategoryMapping("122", NewznabStandardCategory.PCMobileiOS, "|-iOS");
caps.Categories.AddCategoryMapping("40", NewznabStandardCategory.PCMobileOther, "|-Інші мобільні платформи");
// caps.Categories.AddCategoryMapping("241", NewznabStandardCategory.Other, "Інше");
// caps.Categories.AddCategoryMapping("203", NewznabStandardCategory.Other, "|-Інфодиски, електронні підручники, відеоуроки");
// caps.Categories.AddCategoryMapping("12", NewznabStandardCategory.Other, "|-Шпалери, фотографії та зображення");
// caps.Categories.AddCategoryMapping("249", NewznabStandardCategory.Other, "|-Веб-скрипти");
caps.Categories.AddCategoryMapping("10", NewznabStandardCategory.PCGames, "Ігри українською");
caps.Categories.AddCategoryMapping("28", NewznabStandardCategory.PCGames, "|-PC ігри");
caps.Categories.AddCategoryMapping("259", NewznabStandardCategory.PCGames, "|-Mac ігри");
caps.Categories.AddCategoryMapping("29", NewznabStandardCategory.PCGames, "|-Українізації, доповнення, патчі...");
caps.Categories.AddCategoryMapping("30", NewznabStandardCategory.PCGames, "|-Мобільні та консольні ігри");
caps.Categories.AddCategoryMapping("41", NewznabStandardCategory.PCMobileiOS, "|-iOS");
caps.Categories.AddCategoryMapping("212", NewznabStandardCategory.PCMobileAndroid, "|-Android");
caps.Categories.AddCategoryMapping("205", NewznabStandardCategory.PCGames, "Переклад ігор українською");
return caps;
}
}
public class TolokaRequestGenerator : IIndexerRequestGenerator
{
private readonly TolokaSettings _settings;
private readonly IndexerCapabilities _capabilities;
public TolokaRequestGenerator(TolokaSettings settings, IndexerCapabilities capabilities)
{
_settings = settings;
_capabilities = capabilities;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
var term = $"{searchCriteria.SanitizedSearchTerm}";
if (searchCriteria.Season is > 0)
{
term += $" Сезон {searchCriteria.Season}";
}
pageableRequests.Add(GetPagedRequests(term, searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var parameters = new List<KeyValuePair<string, string>>
{
{ "o", "1" },
{ "s", "2" },
{ "nm", term.IsNotNullOrWhiteSpace() ? term.Replace("-", " ") : "" }
};
var queryCats = _capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (queryCats.Any())
{
foreach (var cat in queryCats)
{
parameters.Add("f[]", $"{cat}");
}
}
var searchUrl = _settings.BaseUrl + "tracker.php";
if (parameters.Count > 0)
{
searchUrl += $"?{parameters.GetQueryString()}";
}
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class TolokaParser : IParseIndexerResponse
{
private readonly TolokaSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public TolokaParser(TolokaSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
var rows = dom.QuerySelectorAll("table.forumline > tbody > tr[class*=prow]");
foreach (var row in rows)
{
var downloadUrl = row.QuerySelector("td:nth-child(6) > a")?.GetAttribute("href");
// Expects moderation
if (downloadUrl == null)
{
continue;
}
var infoUrl = _settings.BaseUrl + row.QuerySelector("td:nth-child(3) > a")?.GetAttribute("href");
var title = row.QuerySelector("td:nth-child(3) > a").TextContent.Trim();
var categoryLink = row.QuerySelector("td:nth-child(2) > a").GetAttribute("href");
var cat = ParseUtil.GetArgumentFromQueryString(categoryLink, "f");
var categories = _categories.MapTrackerCatToNewznab(cat);
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(10) > b")?.TextContent);
var peers = seeders + ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(11) > b")?.TextContent.Trim());
// 2023-01-21
var added = row.QuerySelector("td:nth-child(13)").TextContent.Trim();
var release = new TorrentInfo
{
Guid = infoUrl,
InfoUrl = infoUrl,
DownloadUrl = _settings.BaseUrl + downloadUrl,
Title = CleanTitle(title, categories, _settings.StripCyrillicLetters),
Categories = categories,
Seeders = seeders,
Peers = peers,
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-child(7)")?.TextContent.Trim()),
Grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)")?.TextContent),
PublishDate = DateTimeUtil.FromFuzzyTime(added),
DownloadVolumeFactor = 1,
UploadVolumeFactor = 1,
MinimumRatio = 1,
MinimumSeedTime = 0
};
releaseInfos.Add(release);
}
return releaseInfos.ToArray();
}
private static bool IsAnyTvCategory(ICollection<IndexerCategory> category)
{
return category.Contains(NewznabStandardCategory.TV) || NewznabStandardCategory.TV.SubCategories.Any(subCategory => category.Contains(subCategory));
}
private static string CleanTitle(string title, ICollection<IndexerCategory> categories, bool stripCyrillicLetters = true)
{
var tvShowTitleRegex = new Regex(".+\\/\\s([^а-яА-я\\/]+)\\s\\/.+Сезон\\s*[:]*\\s+(\\d+).+(?:Серії|Епізод)+\\s*[:]*\\s+(\\d+-*\\d*).+,\\s+(.+)\\]\\s(.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
var stripCyrillicRegex = new Regex(@"(\([\p{IsCyrillic}\W]+\))|(^[\p{IsCyrillic}\W\d]+\/ )|([\p{IsCyrillic} \-]+,+)|([\p{IsCyrillic}]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
// https://www.fileformat.info/info/unicode/category/Pd/list.htm
title = Regex.Replace(title, "\\p{Pd}", "-", RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (IsAnyTvCategory(categories))
{
// extract season and episodes
title = tvShowTitleRegex.Replace(title, "$1 - S$2E$3 - rus $4 $5");
}
else if (stripCyrillicLetters)
{
title = stripCyrillicRegex.Replace(title, string.Empty);
}
title = Regex.Replace(title, @"\b-Rip\b", "Rip", RegexOptions.Compiled | RegexOptions.IgnoreCase);
title = Regex.Replace(title, @"\bHDTVRip\b", "HDTV", RegexOptions.Compiled | RegexOptions.IgnoreCase);
title = Regex.Replace(title, @"\bWEB-DLRip\b", "WEB-DL", RegexOptions.Compiled | RegexOptions.IgnoreCase);
title = Regex.Replace(title, @"\bWEBDLRip\b", "WEB-DL", RegexOptions.Compiled | RegexOptions.IgnoreCase);
title = Regex.Replace(title, @"\bWEBDL\b", "WEB-DL", RegexOptions.Compiled | RegexOptions.IgnoreCase);
return title.Trim(' ', '.', '-', '_', '|', '/', '\'');
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class TolokaSettings : UserPassTorrentBaseSettings
{
public TolokaSettings()
{
StripCyrillicLetters = true;
}
[FieldDefinition(4, Label = "Strip Cyrillic Letters", Type = FieldType.Checkbox)]
public bool StripCyrillicLetters { get; set; }
}
}

View File

@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var parser = new HtmlParser();
var dom = parser.ParseDocument(response.Content);
var errorMessage = dom.QuerySelector("td.embedded").TextContent.Trim();
var errorMessage = dom.QuerySelector("td.embedded")?.TextContent.Trim() ?? response.Content;
throw new IndexerAuthException(errorMessage);
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
@@ -13,15 +12,13 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class TorrentDay : TorrentIndexerBase<CookieTorrentBaseSettings>
public class TorrentDay : TorrentIndexerBase<TorrentDaySettings>
{
public override string Name => "TorrentDay";
public override string[] IndexerUrls => new string[]
public override string[] IndexerUrls => new[]
{
"https://torrentday.cool/",
"https://tday.love/",
@@ -46,7 +43,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new TorrentDayRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
return new TorrentDayRequestGenerator { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
@@ -64,21 +61,21 @@ namespace NzbDrone.Core.Indexers.Definitions
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
{
MovieSearchParam.Q, MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q
},
{
MusicSearchParam.Q
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q
}
{
BookSearchParam.Q
}
};
caps.Categories.AddCategoryMapping(29, NewznabStandardCategory.TVAnime, "Anime");
@@ -135,13 +132,9 @@ namespace NzbDrone.Core.Indexers.Definitions
public class TorrentDayRequestGenerator : IIndexerRequestGenerator
{
public CookieTorrentBaseSettings Settings { get; set; }
public TorrentDaySettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public TorrentDayRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
{
var searchUrl = Settings.BaseUrl + "t.json";
@@ -154,6 +147,12 @@ namespace NzbDrone.Core.Indexers.Definitions
var catStr = string.Join(";", cats);
searchUrl = searchUrl + "?" + catStr;
if (Settings.FreeLeechOnly)
{
searchUrl += ";free";
}
searchUrl += ";q=";
if (imdbId.IsNotNullOrWhiteSpace())
@@ -219,10 +218,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public class TorrentDayParser : IParseIndexerResponse
{
private readonly CookieTorrentBaseSettings _settings;
private readonly TorrentDaySettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public TorrentDayParser(CookieTorrentBaseSettings settings, IndexerCapabilitiesCategories categories)
public TorrentDayParser(TorrentDaySettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
@@ -275,4 +274,10 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class TorrentDaySettings : CookieTorrentBaseSettings
{
[FieldDefinition(3, Label = "FreeLeech Only", Type = FieldType.Checkbox, HelpText = "Search Freeleech torrents only")]
public bool FreeLeechOnly { get; set; }
}
}

View File

@@ -5,16 +5,28 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.TorrentPotato
{
public class TorrentPotatoSettingsValidator : NoAuthSettingsValidator<TorrentPotatoSettings>
{
public TorrentPotatoSettingsValidator()
{
RuleFor(c => c.User).NotEmpty();
RuleFor(c => c.Passkey).NotEmpty();
}
}
public class TorrentPotatoSettings : NoAuthTorrentBaseSettings
{
public TorrentPotatoSettings()
{
}
private static readonly TorrentPotatoSettingsValidator Validator = new ();
[FieldDefinition(2, Label = "Username", HelpText = "Indexer Username", Privacy = PrivacyLevel.UserName)]
public string User { get; set; }
[FieldDefinition(3, Label = "Passkey", HelpText = "Indexer Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Passkey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -323,7 +323,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class TorrentSyndikatSettingsValidator : AbstractValidator<TorrentSyndikatSettings>
public class TorrentSyndikatSettingsValidator : NoAuthSettingsValidator<TorrentSyndikatSettings>
{
public TorrentSyndikatSettingsValidator()
{
@@ -333,7 +333,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class TorrentSyndikatSettings : NoAuthTorrentBaseSettings
{
private static readonly TorrentSyndikatSettingsValidator Validator = new TorrentSyndikatSettingsValidator();
private static readonly TorrentSyndikatSettingsValidator Validator = new ();
public TorrentSyndikatSettings()
{

View File

@@ -41,14 +41,10 @@ namespace NzbDrone.Core.Indexers.Torznab
public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings
{
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();
public TorznabSettings()
{
}
private static readonly TorznabSettingsValidator Validator = new ();
[FieldDefinition(3)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new ();
public override NzbDroneValidationResult Validate()
{

View File

@@ -5,7 +5,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions.UNIT3D
{
public class Unit3dSettingsValidator : AbstractValidator<Unit3dSettings>
public class Unit3dSettingsValidator : NoAuthSettingsValidator<Unit3dSettings>
{
public Unit3dSettingsValidator()
{
@@ -15,11 +15,7 @@ namespace NzbDrone.Core.Indexers.Definitions.UNIT3D
public class Unit3dSettings : NoAuthTorrentBaseSettings
{
private static readonly Unit3dSettingsValidator Validator = new Unit3dSettingsValidator();
public Unit3dSettings()
{
}
private static readonly Unit3dSettingsValidator Validator = new ();
[FieldDefinition(2, Label = "API Key", HelpText = "Site API Key generated in My Security", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }

View File

@@ -4,9 +4,13 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions.Xthor
{
public class XthorSettingsValidator : NoAuthSettingsValidator<XthorSettings>
{
}
public class XthorSettings : NoAuthTorrentBaseSettings
{
private static readonly XthorSettingsValidator Validator = new XthorSettingsValidator();
private static readonly XthorSettingsValidator Validator = new ();
public XthorSettings()
{

View File

@@ -1,8 +0,0 @@
using FluentValidation;
namespace NzbDrone.Core.Indexers.Definitions.Xthor
{
public class XthorSettingsValidator : AbstractValidator<XthorSettings>
{
}
}

View File

@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Indexers
yield return new IndexerDefinition
{
Name = GetType().Name,
Name = Name ?? GetType().Name,
Enable = config.Validate().IsValid && SupportsRss,
Implementation = GetType().Name,
Settings = config

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Indexers
public class IndexerBaseSettings
{
private static readonly IndexerCommonSettingsValidator Validator = new IndexerCommonSettingsValidator();
private static readonly IndexerCommonSettingsValidator Validator = new ();
[FieldDefinition(1, Type = FieldType.Number, Label = "Query Limit", HelpText = "The number of queries within a rolling 24 hour period Prowlarr will allow to the site", Advanced = true)]
public int? QueryLimit { get; set; }

View File

@@ -1,6 +1,6 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers
{
@@ -8,19 +8,26 @@ namespace NzbDrone.Core.Indexers
{
public IndexerTorrentSettingsValidator(double seedRatioMinimum = 0.0, int seedTimeMinimum = 0, int seasonPackSeedTimeMinimum = 0)
{
RuleFor(c => c.AppMinimumSeeders).GreaterThan(0)
.When(c => c.AppMinimumSeeders.HasValue)
.WithMessage("Should be greater than zero");
RuleFor(c => c.SeedRatio).GreaterThan(0.0)
.When(c => c.SeedRatio.HasValue)
.AsWarning().WithMessage("Should be greater than zero");
.WithMessage("Should be greater than zero");
RuleFor(c => c.SeedTime).GreaterThan(0)
.When(c => c.SeedTime.HasValue)
.AsWarning().WithMessage("Should be greater than zero");
.WithMessage("Should be greater than zero");
RuleFor(c => c.PackSeedTime).GreaterThan(0)
.When(c => c.PackSeedTime.HasValue)
.WithMessage("Should be greater than zero");
if (seedRatioMinimum != 0.0)
{
RuleFor(c => c.SeedRatio).GreaterThanOrEqualTo(seedRatioMinimum)
.When(c => c.SeedRatio > 0.0)
.AsWarning()
.WithMessage($"Under {seedRatioMinimum} leads to H&R");
}
@@ -28,23 +35,32 @@ namespace NzbDrone.Core.Indexers
{
RuleFor(c => c.SeedTime).GreaterThanOrEqualTo(seedTimeMinimum)
.When(c => c.SeedTime > 0)
.AsWarning()
.WithMessage($"Under {seedTimeMinimum} leads to H&R");
}
if (seasonPackSeedTimeMinimum != 0)
{
RuleFor(c => c.PackSeedTime).GreaterThanOrEqualTo(seasonPackSeedTimeMinimum)
.When(c => c.PackSeedTime > 0)
.WithMessage($"Under {seasonPackSeedTimeMinimum} leads to H&R");
}
}
}
public class IndexerTorrentBaseSettings
{
private static readonly IndexerTorrentSettingsValidator Validator = new IndexerTorrentSettingsValidator();
private static readonly IndexerTorrentSettingsValidator Validator = new ();
[FieldDefinition(1, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is app's default", Advanced = true)]
[FieldDefinition(1, Type = FieldType.Number, Label = "Apps Minimum Seeders", HelpText = "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default", Advanced = true)]
public int? AppMinimumSeeders { get; set; }
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is app's default", Advanced = true)]
public double? SeedRatio { get; set; }
[FieldDefinition(2, Type = FieldType.Number, Label = "Seed Time", HelpText = "The time a torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)]
[FieldDefinition(3, Type = FieldType.Number, Label = "Seed Time", HelpText = "The time a torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)]
public int? SeedTime { get; set; }
[FieldDefinition(3, Type = FieldType.Number, Label = "Pack Seed Time", HelpText = "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)]
[FieldDefinition(4, Type = FieldType.Number, Label = "Pack Seed Time", HelpText = "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default", Unit = "minutes", Advanced = true)]
public int? PackSeedTime { get; set; }
}
}

View File

@@ -4,19 +4,19 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Settings
{
public class CookieBaseSettingsValidator : AbstractValidator<CookieTorrentBaseSettings>
{
public CookieBaseSettingsValidator()
{
RuleFor(c => c.Cookie).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
public class CookieTorrentBaseSettings : ITorrentIndexerSettings
{
public class CookieBaseSettingsValidator : AbstractValidator<CookieTorrentBaseSettings>
{
public CookieBaseSettingsValidator()
{
RuleFor(c => c.Cookie).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
private static readonly CookieBaseSettingsValidator Validator = new CookieBaseSettingsValidator();
private static readonly CookieBaseSettingsValidator Validator = new ();
public CookieTorrentBaseSettings()
{
@@ -30,10 +30,10 @@ namespace NzbDrone.Core.Indexers.Settings
public string Cookie { get; set; }
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new ();
public virtual NzbDroneValidationResult Validate()
{

View File

@@ -4,7 +4,8 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Settings
{
public class NoAuthSettingsValidator : AbstractValidator<NoAuthTorrentBaseSettings>
public class NoAuthSettingsValidator<T> : AbstractValidator<T>
where T : NoAuthTorrentBaseSettings
{
public NoAuthSettingsValidator()
{
@@ -15,16 +16,16 @@ namespace NzbDrone.Core.Indexers.Settings
public class NoAuthTorrentBaseSettings : ITorrentIndexerSettings
{
private static readonly NoAuthSettingsValidator Validator = new NoAuthSettingsValidator();
private static readonly NoAuthSettingsValidator<NoAuthTorrentBaseSettings> Validator = new ();
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
public string BaseUrl { get; set; }
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new ();
public virtual NzbDroneValidationResult Validate()
{

View File

@@ -4,20 +4,21 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Settings
{
public class UserPassBaseSettingsValidator<T> : AbstractValidator<T>
where T : UserPassTorrentBaseSettings
{
public UserPassBaseSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
public class UserPassTorrentBaseSettings : ITorrentIndexerSettings
{
public class UserPassBaseSettingsValidator : AbstractValidator<UserPassTorrentBaseSettings>
{
public UserPassBaseSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
private static readonly UserPassBaseSettingsValidator Validator = new UserPassBaseSettingsValidator();
private static readonly UserPassBaseSettingsValidator<UserPassTorrentBaseSettings> Validator = new ();
public UserPassTorrentBaseSettings()
{
@@ -35,12 +36,12 @@ namespace NzbDrone.Core.Indexers.Settings
public string Password { get; set; }
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
public IndexerBaseSettings BaseSettings { get; set; } = new ();
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new ();
public NzbDroneValidationResult Validate()
public virtual NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Apprise
{
public class Apprise : NotificationBase<AppriseSettings>
{
public override string Name => "Apprise";
public override string Link => "https://github.com/caronc/apprise";
private readonly IAppriseProxy _proxy;
public Apprise(IAppriseProxy proxy)
{
_proxy = proxy;
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(Settings, HEALTH_ISSUE_TITLE_BRANDED, $"{healthCheck.Message}");
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(Settings, APPLICATION_UPDATE_TITLE_BRANDED, $"{updateMessage.Message}");
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
return new ValidationResult(failures);
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Apprise
{
public interface IAppriseProxy
{
void SendNotification(AppriseSettings settings, string title, string message);
ValidationFailure Test(AppriseSettings settings);
}
public class AppriseProxy : IAppriseProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public AppriseProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void SendNotification(AppriseSettings settings, string title, string body)
{
var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.TrimEnd('/', ' ')).Post()
.AddFormParameter("title", title)
.AddFormParameter("body", body);
if (settings.ConfigurationKey.IsNotNullOrWhiteSpace())
{
requestBuilder
.Resource("/notify/{configurationKey}")
.SetSegment("configurationKey", settings.ConfigurationKey);
}
else if (settings.StatelessUrls.IsNotNullOrWhiteSpace())
{
requestBuilder
.Resource("/notify")
.AddFormParameter("urls", settings.StatelessUrls);
}
if (settings.Tags.Any())
{
requestBuilder.AddFormParameter("tag", settings.Tags.Join(","));
}
if (settings.AuthUsername.IsNotNullOrWhiteSpace() || settings.AuthPassword.IsNotNullOrWhiteSpace())
{
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.AuthUsername, settings.AuthPassword);
}
_httpClient.Execute(requestBuilder.Build());
}
public ValidationFailure Test(AppriseSettings settings)
{
const string title = "Prowlarr - Test Notification";
const string body = "Success! You have properly configured your apprise notification settings.";
try
{
SendNotification(settings, title, body);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, $"HTTP Auth credentials are invalid: {ex.Message}");
return new ValidationFailure("AuthUsername", $"HTTP Auth credentials are invalid: {ex.Message}");
}
_logger.Error(ex, "Unable to send test message. Server connection failed. Status code: {0}", ex.Message);
return new ValidationFailure("Url", $"Unable to connect to Apprise API. Please try again later. Status code: {ex.Message}");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message. Status code: {0}", ex.Message);
return new ValidationFailure("Url", $"Unable to send test message. Status code: {ex.Message}");
}
return null;
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Apprise
{
public class AppriseSettingsValidator : AbstractValidator<AppriseSettings>
{
public AppriseSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ConfigurationKey).NotEmpty()
.When(c => c.StatelessUrls.IsNullOrWhiteSpace())
.WithMessage("Use either Configuration Key or Stateless Urls");
RuleFor(c => c.ConfigurationKey).Matches("^[a-z0-9-]*$")
.WithMessage("Allowed characters a-z, 0-9 and -");
RuleFor(c => c.StatelessUrls).NotEmpty()
.When(c => c.ConfigurationKey.IsNullOrWhiteSpace())
.WithMessage("Use either Configuration Key or Stateless Urls");
RuleFor(c => c.StatelessUrls).Empty()
.When(c => c.ConfigurationKey.IsNotNullOrWhiteSpace())
.WithMessage("Use either Configuration Key or Stateless Urls");
RuleFor(c => c.Tags).Empty()
.When(c => c.StatelessUrls.IsNotNullOrWhiteSpace())
.WithMessage("Stateless Urls do not support tags");
}
}
public class AppriseSettings : IProviderConfig
{
private static readonly AppriseSettingsValidator Validator = new ();
public AppriseSettings()
{
Tags = Array.Empty<string>();
}
[FieldDefinition(1, Label = "Apprise Base URL", Type = FieldType.Url, Placeholder = "http://localhost:8000", HelpText = "Apprise server Base URL, including http(s):// and port if needed", HelpLink = "https://github.com/caronc/apprise-api")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "Apprise Configuration Key", Type = FieldType.Textbox, HelpText = "Configuration Key for the Persistent Storage Solution. Leave empty if Stateless Urls is used.", HelpLink = "https://github.com/caronc/apprise-api#persistent-storage-solution")]
public string ConfigurationKey { get; set; }
[FieldDefinition(3, Label = "Apprise Stateless Urls", Type = FieldType.Textbox, HelpText = "One or more URLs separated by commas identifying where the notification should be sent to. Leave empty if Persistent Storage is used.", HelpLink = "https://github.com/caronc/apprise#productivity-based-notifications")]
public string StatelessUrls { get; set; }
[FieldDefinition(4, Label = "Apprise Tags", Type = FieldType.Tag, HelpText = "Optionally notify only those tagged accordingly.")]
public IEnumerable<string> Tags { get; set; }
[FieldDefinition(5, Label = "Auth Username", Type = FieldType.Textbox, HelpText = "HTTP Basic Auth Username", Privacy = PrivacyLevel.UserName)]
public string AuthUsername { get; set; }
[FieldDefinition(6, Label = "Auth Password", Type = FieldType.Password, HelpText = "HTTP Basic Auth Password", Privacy = PrivacyLevel.Password)]
public string AuthPassword { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class Ntfy : NotificationBase<NtfySettings>
{
private readonly INtfyProxy _proxy;
public Ntfy(INtfyProxy proxy)
{
_proxy = proxy;
}
public override string Name => "ntfy.sh";
public override string Link => "https://ntfy.sh/";
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, message.Message, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
return new ValidationResult(failures);
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class NtfyException : NzbDroneException
{
public NtfyException(string message)
: base(message)
{
}
public NtfyException(string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
}
}
}

View File

@@ -0,0 +1,11 @@
namespace NzbDrone.Core.Notifications.Ntfy
{
public enum NtfyPriority
{
Min = 1,
Low = 2,
Default = 3,
High = 4,
Max = 5
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Ntfy
{
public interface INtfyProxy
{
void SendNotification(string title, string message, NtfySettings settings);
ValidationFailure Test(NtfySettings settings);
}
public class NtfyProxy : INtfyProxy
{
private const string DEFAULT_PUSH_URL = "https://ntfy.sh";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public NtfyProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void SendNotification(string title, string message, NtfySettings settings)
{
var error = false;
var serverUrl = settings.ServerUrl.IsNullOrWhiteSpace() ? NtfyProxy.DEFAULT_PUSH_URL : settings.ServerUrl;
foreach (var topic in settings.Topics)
{
var request = BuildTopicRequest(serverUrl, topic);
try
{
SendNotification(title, message, request, settings);
}
catch (NtfyException ex)
{
_logger.Error(ex, "Unable to send test message to {0}", topic);
error = true;
}
}
if (error)
{
throw new NtfyException("Unable to send Ntfy notifications to all topics");
}
}
private HttpRequestBuilder BuildTopicRequest(string serverUrl, string topic)
{
var trimServerUrl = serverUrl.TrimEnd('/');
var requestBuilder = new HttpRequestBuilder($"{trimServerUrl}/{topic}").Post();
return requestBuilder;
}
public ValidationFailure Test(NtfySettings settings)
{
try
{
const string title = "Prowlarr - Test Notification";
const string body = "This is a test message from Prowlarr";
SendNotification(title, body, settings);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized || ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Error(ex, "Authorization is required");
return new ValidationFailure("UserName", "Authorization is required");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ServerUrl", "Unable to send test message");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private void SendNotification(string title, string message, HttpRequestBuilder requestBuilder, NtfySettings settings)
{
try
{
requestBuilder.Headers.Add("X-Title", title);
requestBuilder.Headers.Add("X-Message", message);
requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString());
if (settings.Tags.Any())
{
requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(","));
}
if (!settings.ClickUrl.IsNullOrWhiteSpace())
{
requestBuilder.Headers.Add("X-Click", settings.ClickUrl);
}
var request = requestBuilder.Build();
if (!settings.UserName.IsNullOrWhiteSpace() && !settings.Password.IsNullOrWhiteSpace())
{
request.Credentials = new BasicNetworkCredential(settings.UserName, settings.Password);
}
_httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized || ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Error(ex, "Authorization is required");
throw;
}
throw new NtfyException("Unable to send text message: {0}", ex, ex.Message);
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Ntfy
{
public class NtfySettingsValidator : AbstractValidator<NtfySettings>
{
public NtfySettingsValidator()
{
RuleFor(c => c.Topics).NotEmpty();
RuleFor(c => c.Priority).InclusiveBetween(1, 5);
RuleFor(c => c.ServerUrl).IsValidUrl().When(c => !c.ServerUrl.IsNullOrWhiteSpace());
RuleFor(c => c.ClickUrl).IsValidUrl().When(c => !c.ClickUrl.IsNullOrWhiteSpace());
RuleFor(c => c.UserName).NotEmpty().When(c => !c.Password.IsNullOrWhiteSpace());
RuleFor(c => c.Password).NotEmpty().When(c => !c.UserName.IsNullOrWhiteSpace());
RuleForEach(c => c.Topics).NotEmpty().Matches("[a-zA-Z0-9_-]+").Must(c => !InvalidTopics.Contains(c)).WithMessage("Invalid topic");
}
private static List<string> InvalidTopics => new List<string> { "announcements", "app", "docs", "settings", "stats", "mytopic-rw", "mytopic-ro", "mytopic-wo" };
}
public class NtfySettings : IProviderConfig
{
private static readonly NtfySettingsValidator Validator = new NtfySettingsValidator();
public NtfySettings()
{
Topics = Array.Empty<string>();
Priority = 3;
}
[FieldDefinition(0, Label = "Server Url", Type = FieldType.Url, HelpLink = "https://ntfy.sh/docs/install/", HelpText = "Leave blank to use public server (https://ntfy.sh)")]
public string ServerUrl { get; set; }
[FieldDefinition(1, Label = "User Name", HelpText = "Optional Authorization", Privacy = PrivacyLevel.UserName)]
public string UserName { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Optional Password", Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(3, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(NtfyPriority))]
public int Priority { get; set; }
[FieldDefinition(4, Label = "Topics", HelpText = "List of Topics to send notifications to", Type = FieldType.Tag)]
public IEnumerable<string> Topics { get; set; }
[FieldDefinition(5, Label = "Ntfy Tags and Emojis", Type = FieldType.Tag, HelpText = "Optional list of tags or emojis to use", HelpLink = "https://ntfy.sh/docs/emojis/")]
public IEnumerable<string> Tags { get; set; }
[FieldDefinition(6, Label = "Click Url", Type = FieldType.Url, HelpText = "Optional link when user clicks notification")]
public string ClickUrl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Simplepush
{
public class Simplepush : NotificationBase<SimplepushSettings>
{
private readonly ISimplepushProxy _proxy;
public Simplepush(ISimplepushProxy proxy)
{
_proxy = proxy;
}
public override string Name => "Simplepush";
public override string Link => "https://simplepush.io/";
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
return new ValidationResult(failures);
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Simplepush
{
public interface ISimplepushProxy
{
void SendNotification(string title, string message, SimplepushSettings settings);
ValidationFailure Test(SimplepushSettings settings);
}
public class SimplepushProxy : ISimplepushProxy
{
private const string URL = "https://api.simplepush.io/send";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public SimplepushProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void SendNotification(string title, string message, SimplepushSettings settings)
{
var requestBuilder = new HttpRequestBuilder(URL).Post();
requestBuilder.AddFormParameter("key", settings.Key)
.AddFormParameter("event", settings.Event)
.AddFormParameter("title", title)
.AddFormParameter("msg", message);
var request = requestBuilder.Build();
_httpClient.Post(request);
}
public ValidationFailure Test(SimplepushSettings settings)
{
try
{
const string title = "Test Notification";
const string body = "This is a test message from Prowlarr";
SendNotification(title, body, settings);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
}
return null;
}
}
}

View File

@@ -0,0 +1,33 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Simplepush
{
public class SimplepushSettingsValidator : AbstractValidator<SimplepushSettings>
{
public SimplepushSettingsValidator()
{
RuleFor(c => c.Key).NotEmpty();
}
}
public class SimplepushSettings : IProviderConfig
{
private static readonly SimplepushSettingsValidator Validator = new SimplepushSettingsValidator();
[FieldDefinition(0, Label = "Key", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://simplepush.io/features")]
public string Key { get; set; }
[FieldDefinition(1, Label = "Event", HelpText = "Customize the behavior of push notifications", HelpLink = "https://simplepush.io/features")]
public string Event { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Key);
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -228,11 +228,20 @@ namespace NzbDrone.Host
private static IConfiguration GetConfiguration(StartupContext context)
{
var appFolder = new AppFolderInfo(context);
return new ConfigurationBuilder()
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
.AddEnvironmentVariables()
.Build();
var configPath = appFolder.GetConfigPath();
try
{
return new ConfigurationBuilder()
.AddXmlFile(configPath, optional: true, reloadOnChange: false)
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
.AddEnvironmentVariables()
.Build();
}
catch (InvalidDataException ex)
{
throw new InvalidConfigFileException($"{configPath} is corrupt or invalid. Please delete the config file and Prowlarr will recreate it.", ex);
}
}
private static string BuildUrl(string scheme, string bindAddress, int port)