mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
20 Commits
v1.1.0.232
...
v1.1.1.237
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cd9ad01c2 | ||
|
|
ce2f322478 | ||
|
|
0487309ee8 | ||
|
|
9862584611 | ||
|
|
6a00e0db90 | ||
|
|
c93831dd8b | ||
|
|
6546ba773c | ||
|
|
4c3484a898 | ||
|
|
8561b862f9 | ||
|
|
e1032fb0f5 | ||
|
|
4063219430 | ||
|
|
e008be8581 | ||
|
|
d6b379df64 | ||
|
|
27094ccf62 | ||
|
|
edf9473e9a | ||
|
|
a0d11e7e33 | ||
|
|
7729eb398a | ||
|
|
989564dbce | ||
|
|
c1f917f1ac | ||
|
|
4b7e47c397 |
@@ -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)'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
361
src/NzbDrone.Core/Indexers/Definitions/Anidex.cs
Normal file
361
src/NzbDrone.Core/Indexers/Definitions/Anidex.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
469
src/NzbDrone.Core/Indexers/Definitions/Toloka.cs
Normal file
469
src/NzbDrone.Core/Indexers/Definitions/Toloka.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Xthor
|
||||
{
|
||||
public class XthorSettingsValidator : AbstractValidator<XthorSettings>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
39
src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
Normal file
39
src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/NzbDrone.Core/Notifications/Apprise/AppriseProxy.cs
Normal file
91
src/NzbDrone.Core/Notifications/Apprise/AppriseProxy.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs
Normal file
70
src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs
Normal file
39
src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/NzbDrone.Core/Notifications/Ntfy/NtfyException.cs
Normal file
18
src/NzbDrone.Core/Notifications/Ntfy/NtfyException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/NzbDrone.Core/Notifications/Ntfy/NtfyPriority.cs
Normal file
11
src/NzbDrone.Core/Notifications/Ntfy/NtfyPriority.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace NzbDrone.Core.Notifications.Ntfy
|
||||
{
|
||||
public enum NtfyPriority
|
||||
{
|
||||
Min = 1,
|
||||
Low = 2,
|
||||
Default = 3,
|
||||
High = 4,
|
||||
Max = 5
|
||||
}
|
||||
}
|
||||
138
src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs
Normal file
138
src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs
Normal file
63
src/NzbDrone.Core/Notifications/Ntfy/NtfySettings.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs
Normal file
38
src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user