mirror of
https://github.com/Prowlarr/Prowlarr
synced 2025-12-06 08:34:28 +01:00
Newznab to Yml
This commit is contained in:
parent
38ba810ae8
commit
0c45eb68fa
22 changed files with 806 additions and 278 deletions
|
|
@ -15,14 +15,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
|
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
|
||||||
{
|
{
|
||||||
private NewznabSettings _settings;
|
private GenericNewznabSettings _settings;
|
||||||
private IndexerDefinition _definition;
|
private IndexerDefinition _definition;
|
||||||
private string _caps;
|
private string _caps;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
_settings = new NewznabSettings()
|
_settings = new GenericNewznabSettings()
|
||||||
{
|
{
|
||||||
BaseUrl = "http://indxer.local"
|
BaseUrl = "http://indxer.local"
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public void Setup()
|
||||||
|
|
||||||
_caps = new IndexerCapabilities();
|
_caps = new IndexerCapabilities();
|
||||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||||
.Returns(_caps);
|
.Returns(_caps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||||
{
|
{
|
||||||
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
public class NewznabRequestGeneratorFixture : CoreTest<GenericNewznabRequestGenerator>
|
||||||
{
|
{
|
||||||
private MovieSearchCriteria _movieSearchCriteria;
|
private MovieSearchCriteria _movieSearchCriteria;
|
||||||
private TvSearchCriteria _tvSearchCriteria;
|
private TvSearchCriteria _tvSearchCriteria;
|
||||||
|
|
@ -19,7 +19,7 @@ public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
Subject.Settings = new NewznabSettings()
|
Subject.Settings = new GenericNewznabSettings()
|
||||||
{
|
{
|
||||||
BaseUrl = "http://127.0.0.1:1234/",
|
BaseUrl = "http://127.0.0.1:1234/",
|
||||||
ApiKey = "abcd",
|
ApiKey = "abcd",
|
||||||
|
|
@ -41,7 +41,7 @@ public void SetUp()
|
||||||
_capabilities = new IndexerCapabilities();
|
_capabilities = new IndexerCapabilities();
|
||||||
|
|
||||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||||
.Returns(_capabilities);
|
.Returns(_capabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public void Setup()
|
||||||
|
|
||||||
_caps = new IndexerCapabilities();
|
_caps = new IndexerCapabilities();
|
||||||
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
Mocker.GetMock<INewznabCapabilitiesProvider>()
|
||||||
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
|
||||||
.Returns(_caps);
|
.Returns(_caps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/024_newznab_yml.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(24)]
|
||||||
|
public class newznab_yml : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Update.Table("Indexers").Set(new { Implementation = "GenericNewznab", ConfigContract = "GenericNewznabSettings" }).Where(new { Implementation = "Newznab" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,8 @@ namespace NzbDrone.Core.IndexerVersions
|
||||||
{
|
{
|
||||||
public interface IIndexerDefinitionUpdateService
|
public interface IIndexerDefinitionUpdateService
|
||||||
{
|
{
|
||||||
List<CardigannMetaDefinition> All();
|
List<IndexerMetaDefinition> All();
|
||||||
|
List<IndexerMetaDefinition> AllForImplementation(string implementation);
|
||||||
CardigannDefinition GetCachedDefinition(string fileKey);
|
CardigannDefinition GetCachedDefinition(string fileKey);
|
||||||
List<string> GetBlocklist();
|
List<string> GetBlocklist();
|
||||||
}
|
}
|
||||||
|
|
@ -28,8 +29,8 @@ public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, I
|
||||||
{
|
{
|
||||||
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
/* Update Service will fall back if version # does not exist for an indexer per Ta */
|
||||||
|
|
||||||
private const string DEFINITION_BRANCH = "master";
|
private const string DEFINITION_BRANCH = "newznab-yml";
|
||||||
private const int DEFINITION_VERSION = 7;
|
private const int DEFINITION_VERSION = 8;
|
||||||
|
|
||||||
//Used when moving yml to C#
|
//Used when moving yml to C#
|
||||||
private readonly List<string> _defintionBlocklist = new List<string>()
|
private readonly List<string> _defintionBlocklist = new List<string>()
|
||||||
|
|
@ -78,9 +79,9 @@ public IndexerDefinitionUpdateService(IHttpClient httpClient,
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CardigannMetaDefinition> All()
|
public List<IndexerMetaDefinition> All()
|
||||||
{
|
{
|
||||||
var indexerList = new List<CardigannMetaDefinition>();
|
var indexerList = new List<IndexerMetaDefinition>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +89,7 @@ public List<CardigannMetaDefinition> All()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
|
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
|
||||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
var response = _httpClient.Get<List<IndexerMetaDefinition>>(request);
|
||||||
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
|
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -111,6 +112,11 @@ public List<CardigannMetaDefinition> All()
|
||||||
return indexerList;
|
return indexerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<IndexerMetaDefinition> AllForImplementation(string implementation)
|
||||||
|
{
|
||||||
|
return All().Where(d => d.Implementation == implementation.ToLower()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public CardigannDefinition GetCachedDefinition(string fileKey)
|
public CardigannDefinition GetCachedDefinition(string fileKey)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(fileKey))
|
if (string.IsNullOrEmpty(fileKey))
|
||||||
|
|
@ -128,7 +134,7 @@ public List<string> GetBlocklist()
|
||||||
return _defintionBlocklist;
|
return _defintionBlocklist;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
private List<IndexerMetaDefinition> ReadDefinitionsFromDisk(List<IndexerMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
|
||||||
{
|
{
|
||||||
var indexerList = defs;
|
var indexerList = defs;
|
||||||
|
|
||||||
|
|
@ -145,7 +151,7 @@ private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMeta
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var definitionString = File.ReadAllText(file.FullName);
|
var definitionString = File.ReadAllText(file.FullName);
|
||||||
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
|
var definition = _deserializer.Deserialize<IndexerMetaDefinition>(definitionString);
|
||||||
|
|
||||||
definition.File = Path.GetFileNameWithoutExtension(file.Name);
|
definition.File = Path.GetFileNameWithoutExtension(file.Name);
|
||||||
|
|
||||||
|
|
@ -243,6 +249,11 @@ private CardigannDefinition CleanIndexerDefinition(CardigannDefinition definitio
|
||||||
definition.Login.Method = "form";
|
definition.Login.Method = "form";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.Search == null)
|
||||||
|
{
|
||||||
|
definition.Search = new SearchBlock();
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.Search.Paths == null)
|
if (definition.Search.Paths == null)
|
||||||
{
|
{
|
||||||
definition.Search.Paths = new List<SearchPathBlock>();
|
definition.Search.Paths = new List<SearchPathBlock>();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Indexers.Cardigann;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Cardigann
|
namespace NzbDrone.Core.IndexerVersions
|
||||||
{
|
{
|
||||||
public class CardigannMetaDefinition
|
public class IndexerMetaDefinition
|
||||||
{
|
{
|
||||||
public CardigannMetaDefinition()
|
public IndexerMetaDefinition()
|
||||||
{
|
{
|
||||||
Legacylinks = new List<string>();
|
Legacylinks = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +14,7 @@ public CardigannMetaDefinition()
|
||||||
public string File { get; set; }
|
public string File { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
public string Implementation { get; set; }
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
public string Encoding { get; set; }
|
public string Encoding { get; set; }
|
||||||
|
|
@ -78,7 +78,7 @@ public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
foreach (var def in _definitionService.All())
|
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
|
||||||
{
|
{
|
||||||
yield return GetDefinition(def);
|
yield return GetDefinition(def);
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ public Cardigann(IIndexerDefinitionUpdateService definitionService,
|
||||||
_generatorCache = cacheManager.GetRollingCache<CardigannRequestGenerator>(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5));
|
_generatorCache = cacheManager.GetRollingCache<CardigannRequestGenerator>(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerDefinition GetDefinition(CardigannMetaDefinition definition)
|
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
|
||||||
{
|
{
|
||||||
var defaultSettings = new List<SettingsField>
|
var defaultSettings = new List<SettingsField>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public CardigannSettingsValidator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CardigannSettings : NoAuthTorrentBaseSettings
|
public class CardigannSettings : NoAuthTorrentBaseSettings, IYmlIndexerSettings
|
||||||
{
|
{
|
||||||
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();
|
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();
|
||||||
|
|
||||||
|
|
|
||||||
195
src/NzbDrone.Core/Indexers/Definitions/Newznab/GenericNewznab.cs
Normal file
195
src/NzbDrone.Core/Indexers/Definitions/Newznab/GenericNewznab.cs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
{
|
||||||
|
public class GenericNewznab : UsenetIndexerBase<GenericNewznabSettings>
|
||||||
|
{
|
||||||
|
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||||
|
|
||||||
|
public override string Name => "Generic Newznab";
|
||||||
|
public override string[] IndexerUrls => GetBaseUrlFromSettings();
|
||||||
|
public override string Description => "Newznab is an API search specification for Usenet";
|
||||||
|
public override bool FollowRedirect => true;
|
||||||
|
public override bool SupportsRedirect => true;
|
||||||
|
|
||||||
|
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||||
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||||
|
|
||||||
|
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
|
||||||
|
|
||||||
|
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
|
||||||
|
|
||||||
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||||
|
{
|
||||||
|
return new GenericNewznabRequestGenerator(_capabilitiesProvider)
|
||||||
|
{
|
||||||
|
PageSize = PageSize,
|
||||||
|
Settings = Settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IParseIndexerResponse GetParser()
|
||||||
|
{
|
||||||
|
return new GenericNewznabRssParser(Settings.Categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetBaseUrlFromSettings()
|
||||||
|
{
|
||||||
|
var baseUrl = "";
|
||||||
|
|
||||||
|
if (Definition == null || Settings == null || Settings.Categories == null)
|
||||||
|
{
|
||||||
|
return new string[] { baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string[] { Settings.BaseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override GenericNewznabSettings GetDefaultBaseUrl(GenericNewznabSettings settings)
|
||||||
|
{
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerCapabilities GetCapabilitiesFromSettings()
|
||||||
|
{
|
||||||
|
var caps = new IndexerCapabilities();
|
||||||
|
|
||||||
|
if (Definition == null || Settings == null || Settings.Categories == null)
|
||||||
|
{
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var category in Settings.Categories)
|
||||||
|
{
|
||||||
|
caps.Categories.AddCategoryMapping(category.Name, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IndexerCapabilities GetCapabilities()
|
||||||
|
{
|
||||||
|
// Newznab uses different Caps per site, so we need to cache them to db on first indexer add to prevent issues with loading UI and pulling caps every time.
|
||||||
|
return _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return GetDefinition("Generic Newznab", GetSettings(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenericNewznab(INewznabCapabilitiesProvider capabilitiesProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
|
||||||
|
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
|
||||||
|
{
|
||||||
|
_capabilitiesProvider = capabilitiesProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexerDefinition GetDefinition(string name, GenericNewznabSettings settings)
|
||||||
|
{
|
||||||
|
return new IndexerDefinition
|
||||||
|
{
|
||||||
|
Enable = true,
|
||||||
|
Name = name,
|
||||||
|
Implementation = GetType().Name,
|
||||||
|
Settings = settings,
|
||||||
|
Protocol = DownloadProtocol.Usenet,
|
||||||
|
Privacy = IndexerPrivacy.Private,
|
||||||
|
SupportsRss = SupportsRss,
|
||||||
|
SupportsSearch = SupportsSearch,
|
||||||
|
SupportsRedirect = SupportsRedirect,
|
||||||
|
Capabilities = Capabilities
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenericNewznabSettings GetSettings(string url, string apiPath = null)
|
||||||
|
{
|
||||||
|
var settings = new GenericNewznabSettings { BaseUrl = url };
|
||||||
|
|
||||||
|
if (apiPath.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
settings.ApiPath = apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task Test(List<ValidationFailure> failures)
|
||||||
|
{
|
||||||
|
await base.Test(failures);
|
||||||
|
if (failures.HasErrors())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failures.AddIfNotNull(TestCapabilities());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static List<int> CategoryIds(IndexerCapabilitiesCategories categories)
|
||||||
|
{
|
||||||
|
var l = categories.GetTorznabCategoryTree().Select(c => c.Id).ToList();
|
||||||
|
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual ValidationFailure TestCapabilities()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
|
||||||
|
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.MovieSearchParams != null &&
|
||||||
|
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.TvSearchParams != null &&
|
||||||
|
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
||||||
|
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.MusicSearchParams != null &&
|
||||||
|
new[] { MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album }.Any(v => capabilities.MusicSearchParams.Contains(v)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.BookSearchParams != null &&
|
||||||
|
new[] { BookSearchParam.Q, BookSearchParam.Author, BookSearchParam.Title }.Any(v => capabilities.BookSearchParams.Contains(v)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationFailure(string.Empty, "This indexer does not support searching for tv, music, or movies :(. Tell your indexer staff to enable this or force add the indexer by disabling search, adding the indexer and then enabling it again.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
|
||||||
|
|
||||||
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using DryIoc;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
{
|
||||||
|
public class GenericNewznabRequestGenerator : IIndexerRequestGenerator
|
||||||
|
{
|
||||||
|
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
||||||
|
public int MaxPages { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public GenericNewznabSettings Settings { get; set; }
|
||||||
|
public ProviderDefinition Definition { get; set; }
|
||||||
|
|
||||||
|
public GenericNewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
|
||||||
|
{
|
||||||
|
_capabilitiesProvider = capabilitiesProvider;
|
||||||
|
|
||||||
|
MaxPages = 30;
|
||||||
|
PageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
|
||||||
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
var parameters = new NameValueCollection();
|
||||||
|
|
||||||
|
if (searchCriteria.TmdbId.HasValue && capabilities.MovieSearchTmdbAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("tmdbid", searchCriteria.TmdbId.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && capabilities.MovieSearchImdbAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("imdbid", searchCriteria.ImdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.TraktId.HasValue && capabilities.MovieSearchTraktAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("traktid", searchCriteria.TraktId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
|
||||||
|
if (parameters.Count == 0)
|
||||||
|
{
|
||||||
|
searchCriteria.SearchType = "search";
|
||||||
|
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.MovieSearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||||
|
parameters));
|
||||||
|
|
||||||
|
return pageableRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
|
||||||
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
var parameters = new NameValueCollection();
|
||||||
|
|
||||||
|
if (searchCriteria.Artist.IsNotNullOrWhiteSpace() && capabilities.MusicSearchArtistAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("artist", searchCriteria.Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Album.IsNotNullOrWhiteSpace() && capabilities.MusicSearchAlbumAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("album", searchCriteria.Album);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
|
||||||
|
if (parameters.Count == 0)
|
||||||
|
{
|
||||||
|
searchCriteria.SearchType = "search";
|
||||||
|
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.MusicSearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||||
|
parameters));
|
||||||
|
|
||||||
|
return pageableRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
|
||||||
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
var parameters = new NameValueCollection();
|
||||||
|
|
||||||
|
if (searchCriteria.TvdbId.HasValue && capabilities.TvSearchTvdbAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("tvdbid", searchCriteria.TvdbId.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.TmdbId.HasValue && capabilities.TvSearchTvdbAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("tmdbid", searchCriteria.TvdbId.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && capabilities.TvSearchImdbAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("imdbid", searchCriteria.ImdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.TvMazeId.HasValue && capabilities.TvSearchTvMazeAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("tvmazeid", searchCriteria.TvMazeId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.RId.HasValue && capabilities.TvSearchTvRageAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("rid", searchCriteria.RId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Season.HasValue && capabilities.TvSearchSeasonAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("season", NewznabifySeasonNumber(searchCriteria.Season.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && capabilities.TvSearchEpAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("ep", searchCriteria.Episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
|
||||||
|
if (parameters.Count == 0)
|
||||||
|
{
|
||||||
|
searchCriteria.SearchType = "search";
|
||||||
|
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.TvSearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||||
|
parameters));
|
||||||
|
|
||||||
|
return pageableRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
|
||||||
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
var parameters = new NameValueCollection();
|
||||||
|
|
||||||
|
if (searchCriteria.Author.IsNotNullOrWhiteSpace() && capabilities.BookSearchAuthorAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("author", searchCriteria.Author);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Title.IsNotNullOrWhiteSpace() && capabilities.BookSearchTitleAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("title", searchCriteria.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Workaround issue with Sphinx search returning garbage results on some indexers. If we don't use id parameters, fallback to t=search
|
||||||
|
if (parameters.Count == 0)
|
||||||
|
{
|
||||||
|
searchCriteria.SearchType = "search";
|
||||||
|
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.BookSearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
||||||
|
parameters));
|
||||||
|
|
||||||
|
return pageableRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||||
|
{
|
||||||
|
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
||||||
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
|
||||||
|
var parameters = new NameValueCollection();
|
||||||
|
|
||||||
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && capabilities.SearchAvailable)
|
||||||
|
{
|
||||||
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
|
}
|
||||||
|
|
||||||
|
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||||
|
|
||||||
|
return pageableRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||||
|
{
|
||||||
|
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
|
||||||
|
var categories = searchCriteria.Categories;
|
||||||
|
|
||||||
|
if (categories != null && categories.Any())
|
||||||
|
{
|
||||||
|
var categoriesQuery = string.Join(",", categories.Distinct());
|
||||||
|
baseUrl += string.Format("&cat={0}", categoriesQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.AdditionalParameters.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
baseUrl += Settings.AdditionalParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
baseUrl += "&apikey=" + Settings.ApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Limit.HasValue)
|
||||||
|
{
|
||||||
|
parameters.Add("limit", searchCriteria.Limit.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Offset.HasValue)
|
||||||
|
{
|
||||||
|
parameters.Add("offset", searchCriteria.Offset.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
|
||||||
|
request.HttpRequest.AllowAutoRedirect = true;
|
||||||
|
|
||||||
|
yield return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NewsnabifyTitle(string title)
|
||||||
|
{
|
||||||
|
return title.Replace("+", "%20");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary workaround for NNTMux considering season=0 -> null. '00' should work on existing newznab indexers.
|
||||||
|
private static string NewznabifySeasonNumber(int seasonNumber)
|
||||||
|
{
|
||||||
|
return seasonNumber == 0 ? "00" : seasonNumber.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||||
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,17 +8,17 @@
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Newznab
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
public class NewznabRssParser : RssParser
|
public class GenericNewznabRssParser : RssParser
|
||||||
{
|
{
|
||||||
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
|
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
|
||||||
|
|
||||||
private readonly NewznabSettings _settings;
|
private readonly List<IndexerCategory> _categories;
|
||||||
|
|
||||||
public NewznabRssParser(NewznabSettings settings)
|
public GenericNewznabRssParser(List<IndexerCategory> categories)
|
||||||
{
|
{
|
||||||
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
|
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
|
||||||
UseEnclosureUrl = true;
|
UseEnclosureUrl = true;
|
||||||
_settings = settings;
|
_categories = categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
|
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
|
||||||
|
|
@ -125,7 +125,7 @@ protected override ICollection<IndexerCategory> GetCategory(XElement item)
|
||||||
{
|
{
|
||||||
if (int.TryParse(cat, out var intCategory))
|
if (int.TryParse(cat, out var intCategory))
|
||||||
{
|
{
|
||||||
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
|
var indexerCat = _categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
|
||||||
|
|
||||||
if (indexerCat != null)
|
if (indexerCat != null)
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
{
|
||||||
|
public class GenericNewznabSettingsValidator : AbstractValidator<GenericNewznabSettings>
|
||||||
|
{
|
||||||
|
private static readonly string[] ApiKeyWhiteList =
|
||||||
|
{
|
||||||
|
"nzbs.org",
|
||||||
|
"nzb.su",
|
||||||
|
"dognzb.cr",
|
||||||
|
"nzbplanet.net",
|
||||||
|
"nzbid.org",
|
||||||
|
"nzbndx.com",
|
||||||
|
"nzbindex.in"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool ShouldHaveApiKey(GenericNewznabSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.BaseUrl == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public GenericNewznabSettingsValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||||
|
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
||||||
|
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
||||||
|
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
|
||||||
|
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
|
||||||
|
|
||||||
|
RuleFor(c => c.VipExpiration).Must(c => c.IsValidDate())
|
||||||
|
.When(c => c.VipExpiration.IsNotNullOrWhiteSpace())
|
||||||
|
.WithMessage("Correctly formatted date is required");
|
||||||
|
|
||||||
|
RuleFor(c => c.VipExpiration).Must(c => c.IsFutureDate())
|
||||||
|
.When(c => c.VipExpiration.IsNotNullOrWhiteSpace())
|
||||||
|
.WithMessage("Must be a future date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GenericNewznabSettings : IIndexerSettings
|
||||||
|
{
|
||||||
|
private static readonly GenericNewznabSettingsValidator Validator = new GenericNewznabSettingsValidator();
|
||||||
|
|
||||||
|
public GenericNewznabSettings()
|
||||||
|
{
|
||||||
|
ApiPath = "/api";
|
||||||
|
VipExpiration = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "URL")]
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
||||||
|
public string ApiPath { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||||
|
public string AdditionalParameters { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")]
|
||||||
|
public string VipExpiration { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(7)]
|
||||||
|
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||||
|
|
||||||
|
public List<IndexerCategory> Categories { get; set; }
|
||||||
|
|
||||||
|
// Field 8 is used by TorznabSettings MinimumSeeders
|
||||||
|
// If you need to add another field here, update TorznabSettings as well and this comment
|
||||||
|
public virtual NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Indexers.Cardigann;
|
||||||
|
using NzbDrone.Core.IndexerVersions;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
|
@ -16,10 +17,10 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
public class Newznab : UsenetIndexerBase<NewznabSettings>
|
public class Newznab : UsenetIndexerBase<NewznabSettings>
|
||||||
{
|
{
|
||||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
private readonly IIndexerDefinitionUpdateService _definitionService;
|
||||||
|
|
||||||
public override string Name => "Newznab";
|
public override string Name => "Newznab";
|
||||||
public override string[] IndexerUrls => GetBaseUrlFromSettings();
|
public override string[] IndexerUrls => new string[] { "" };
|
||||||
public override string Description => "Newznab is an API search specification for Usenet";
|
public override string Description => "Newznab is an API search specification for Usenet";
|
||||||
public override bool FollowRedirect => true;
|
public override bool FollowRedirect => true;
|
||||||
public override bool SupportsRedirect => true;
|
public override bool SupportsRedirect => true;
|
||||||
|
|
@ -27,130 +28,72 @@ public class Newznab : UsenetIndexerBase<NewznabSettings>
|
||||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||||
|
|
||||||
public override IndexerCapabilities Capabilities { get => GetCapabilitiesFromSettings(); protected set => base.Capabilities = value; }
|
|
||||||
|
|
||||||
public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings, Definition).LimitsDefault.Value;
|
|
||||||
|
|
||||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||||
{
|
{
|
||||||
return new NewznabRequestGenerator(_capabilitiesProvider)
|
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
|
||||||
|
|
||||||
|
return new NewznabRequestGenerator()
|
||||||
{
|
{
|
||||||
PageSize = PageSize,
|
PageSize = PageSize,
|
||||||
Settings = Settings
|
Settings = Settings,
|
||||||
|
Definition = defFile
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IParseIndexerResponse GetParser()
|
public override IParseIndexerResponse GetParser()
|
||||||
{
|
{
|
||||||
return new NewznabRssParser(Settings);
|
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
|
||||||
}
|
var capabilities = new IndexerCapabilities();
|
||||||
|
capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
|
||||||
|
capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
|
||||||
|
capabilities.MapYmlCategories(defFile);
|
||||||
|
|
||||||
public string[] GetBaseUrlFromSettings()
|
return new GenericNewznabRssParser(capabilities.Categories.GetTorznabCategoryList());
|
||||||
{
|
|
||||||
var baseUrl = "";
|
|
||||||
|
|
||||||
if (Definition == null || Settings == null || Settings.Categories == null)
|
|
||||||
{
|
|
||||||
return new string[] { baseUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
return new string[] { Settings.BaseUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override NewznabSettings GetDefaultBaseUrl(NewznabSettings settings)
|
|
||||||
{
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerCapabilities GetCapabilitiesFromSettings()
|
|
||||||
{
|
|
||||||
var caps = new IndexerCapabilities();
|
|
||||||
|
|
||||||
if (Definition == null || Settings == null || Settings.Categories == null)
|
|
||||||
{
|
|
||||||
return caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var category in Settings.Categories)
|
|
||||||
{
|
|
||||||
caps.Categories.AddCategoryMapping(category.Name, category);
|
|
||||||
}
|
|
||||||
|
|
||||||
return caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IndexerCapabilities GetCapabilities()
|
|
||||||
{
|
|
||||||
// Newznab uses different Caps per site, so we need to cache them to db on first indexer add to prevent issues with loading UI and pulling caps every time.
|
|
||||||
return _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"));
|
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
|
||||||
yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"));
|
{
|
||||||
yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"));
|
yield return GetDefinition(def);
|
||||||
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
|
}
|
||||||
yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"));
|
|
||||||
yield return GetDefinition("GingaDADDY", GetSettings("https://www.gingadaddy.com"));
|
|
||||||
yield return GetDefinition("Miatrix", GetSettings("https://www.miatrix.com"));
|
|
||||||
yield return GetDefinition("Newz-Complex", GetSettings("https://newz-complex.org/www"));
|
|
||||||
yield return GetDefinition("Newz69", GetSettings("https://newz69.keagaming.com"));
|
|
||||||
yield return GetDefinition("NinjaCentral", GetSettings("https://ninjacentral.co.za"));
|
|
||||||
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
|
|
||||||
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
|
|
||||||
yield return GetDefinition("NZBFinder", GetSettings("https://nzbfinder.ws"));
|
|
||||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
|
||||||
yield return GetDefinition("NzbNoob", GetSettings("https://www.nzbnoob.com"));
|
|
||||||
yield return GetDefinition("NZBNDX", GetSettings("https://www.nzbndx.com"));
|
|
||||||
yield return GetDefinition("NzbPlanet", GetSettings("https://api.nzbplanet.net"));
|
|
||||||
yield return GetDefinition("NZBStars", GetSettings("https://nzbstars.com"));
|
|
||||||
yield return GetDefinition("OZnzb", GetSettings("https://api.oznzb.com"));
|
|
||||||
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
|
|
||||||
yield return GetDefinition("SpotNZB", GetSettings("https://spotnzb.xyz"));
|
|
||||||
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
|
|
||||||
yield return GetDefinition("Usenet Crawler", GetSettings("https://www.usenet-crawler.com"));
|
|
||||||
yield return GetDefinition("Generic Newznab", GetSettings(""));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
|
public Newznab(IIndexerDefinitionUpdateService definitionService, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
|
||||||
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
|
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
|
||||||
{
|
{
|
||||||
_capabilitiesProvider = capabilitiesProvider;
|
_definitionService = definitionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerDefinition GetDefinition(string name, NewznabSettings settings)
|
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
|
||||||
{
|
{
|
||||||
return new IndexerDefinition
|
return new IndexerDefinition
|
||||||
{
|
{
|
||||||
Enable = true,
|
Enable = true,
|
||||||
Name = name,
|
Name = definition.Name,
|
||||||
|
Language = definition.Language,
|
||||||
|
Description = definition.Description,
|
||||||
Implementation = GetType().Name,
|
Implementation = GetType().Name,
|
||||||
Settings = settings,
|
IndexerUrls = definition.Links.ToArray(),
|
||||||
|
LegacyUrls = definition.Legacylinks.ToArray(),
|
||||||
|
Settings = new NewznabSettings { DefinitionFile = definition.File },
|
||||||
Protocol = DownloadProtocol.Usenet,
|
Protocol = DownloadProtocol.Usenet,
|
||||||
Privacy = IndexerPrivacy.Private,
|
Privacy = definition.Type switch
|
||||||
|
{
|
||||||
|
"private" => IndexerPrivacy.Private,
|
||||||
|
"public" => IndexerPrivacy.Public,
|
||||||
|
_ => IndexerPrivacy.SemiPrivate
|
||||||
|
},
|
||||||
SupportsRss = SupportsRss,
|
SupportsRss = SupportsRss,
|
||||||
SupportsSearch = SupportsSearch,
|
SupportsSearch = SupportsSearch,
|
||||||
SupportsRedirect = SupportsRedirect,
|
SupportsRedirect = SupportsRedirect,
|
||||||
Capabilities = Capabilities
|
Capabilities = new IndexerCapabilities()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private NewznabSettings GetSettings(string url, string apiPath = null)
|
|
||||||
{
|
|
||||||
var settings = new NewznabSettings { BaseUrl = url };
|
|
||||||
|
|
||||||
if (apiPath.IsNotNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
settings.ApiPath = apiPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task Test(List<ValidationFailure> failures)
|
protected override async Task Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
await base.Test(failures);
|
await base.Test(failures);
|
||||||
|
|
@ -158,61 +101,21 @@ protected override async Task Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
failures.AddIfNotNull(TestCapabilities());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static List<int> CategoryIds(IndexerCapabilitiesCategories categories)
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
{
|
{
|
||||||
var l = categories.GetTorznabCategoryTree().Select(c => c.Id).ToList();
|
if (action == "getUrls")
|
||||||
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual ValidationFailure TestCapabilities()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var devices = ((IndexerDefinition)Definition).IndexerUrls;
|
||||||
|
|
||||||
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
|
return new
|
||||||
{
|
{
|
||||||
return null;
|
options = devices.Select(d => new { Value = d, Name = d })
|
||||||
}
|
};
|
||||||
|
|
||||||
if (capabilities.MovieSearchParams != null &&
|
|
||||||
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (capabilities.TvSearchParams != null &&
|
|
||||||
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
|
|
||||||
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (capabilities.MusicSearchParams != null &&
|
|
||||||
new[] { MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album }.Any(v => capabilities.MusicSearchParams.Contains(v)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (capabilities.BookSearchParams != null &&
|
|
||||||
new[] { BookSearchParam.Q, BookSearchParam.Author, BookSearchParam.Title }.Any(v => capabilities.BookSearchParams.Contains(v)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ValidationFailure(string.Empty, "This indexer does not support searching for tv, music, or movies :(. Tell your indexer staff to enable this or force add the indexer by disabling search, adding the indexer and then enabling it again.");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Warn(ex, "Unable to connect to indexer: " + ex.Message);
|
|
||||||
|
|
||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
public interface INewznabCapabilitiesProvider
|
public interface INewznabCapabilitiesProvider
|
||||||
{
|
{
|
||||||
IndexerCapabilities GetCapabilities(NewznabSettings settings, ProviderDefinition definition);
|
IndexerCapabilities GetCapabilities(GenericNewznabSettings settings, ProviderDefinition definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
|
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
|
||||||
|
|
@ -30,7 +30,7 @@ public NewznabCapabilitiesProvider(ICacheManager cacheManager, IIndexerHttpClien
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
|
public IndexerCapabilities GetCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
|
||||||
{
|
{
|
||||||
var key = indexerSettings.ToJson();
|
var key = indexerSettings.ToJson();
|
||||||
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
|
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
|
||||||
|
|
@ -38,7 +38,7 @@ public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings, Prov
|
||||||
return capabilities;
|
return capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
|
private IndexerCapabilities FetchCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
|
||||||
{
|
{
|
||||||
var capabilities = new IndexerCapabilities();
|
var capabilities = new IndexerCapabilities();
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ private IndexerCapabilities ParseCapabilities(HttpResponse response)
|
||||||
throw new XmlException("Invalid XML").WithData(response);
|
throw new XmlException("Invalid XML").WithData(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
NewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
|
GenericNewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
|
||||||
|
|
||||||
var xmlRoot = xDoc.Element("caps");
|
var xmlRoot = xDoc.Element("caps");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
using DryIoc;
|
using DryIoc;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Indexers.Cardigann;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
@ -13,23 +14,20 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
public class NewznabRequestGenerator : IIndexerRequestGenerator
|
public class NewznabRequestGenerator : IIndexerRequestGenerator
|
||||||
{
|
{
|
||||||
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
|
|
||||||
public int MaxPages { get; set; }
|
public int MaxPages { get; set; }
|
||||||
public int PageSize { get; set; }
|
public int PageSize { get; set; }
|
||||||
public NewznabSettings Settings { get; set; }
|
public NewznabSettings Settings { get; set; }
|
||||||
public ProviderDefinition Definition { get; set; }
|
public CardigannDefinition Definition { get; set; }
|
||||||
|
|
||||||
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
|
public NewznabRequestGenerator()
|
||||||
{
|
{
|
||||||
_capabilitiesProvider = capabilitiesProvider;
|
|
||||||
|
|
||||||
MaxPages = 30;
|
MaxPages = 30;
|
||||||
PageSize = 100;
|
PageSize = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var capabilities = GetCapabilities();
|
||||||
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
var parameters = new NameValueCollection();
|
var parameters = new NameValueCollection();
|
||||||
|
|
@ -67,15 +65,14 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||||
parameters));
|
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var capabilities = GetCapabilities();
|
||||||
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
var parameters = new NameValueCollection();
|
var parameters = new NameValueCollection();
|
||||||
|
|
@ -108,15 +105,14 @@ public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||||
parameters));
|
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var capabilities = GetCapabilities();
|
||||||
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
var parameters = new NameValueCollection();
|
var parameters = new NameValueCollection();
|
||||||
|
|
@ -174,15 +170,14 @@ public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCrit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||||
parameters));
|
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var capabilities = GetCapabilities();
|
||||||
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
var parameters = new NameValueCollection();
|
var parameters = new NameValueCollection();
|
||||||
|
|
@ -215,15 +210,15 @@ public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchCriteria,
|
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||||
parameters));
|
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||||
{
|
{
|
||||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
|
var capabilities = GetCapabilities();
|
||||||
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
|
||||||
var parameters = new NameValueCollection();
|
var parameters = new NameValueCollection();
|
||||||
|
|
@ -233,15 +228,15 @@ public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchC
|
||||||
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
parameters.Add("q", NewsnabifyTitle(searchCriteria.SearchTerm));
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, IndexerCapabilities capabilities, NameValueCollection parameters)
|
||||||
{
|
{
|
||||||
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
|
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", ResolveSiteLink().TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
|
||||||
var categories = searchCriteria.Categories;
|
var categories = capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||||
|
|
||||||
if (categories != null && categories.Any())
|
if (categories != null && categories.Any())
|
||||||
{
|
{
|
||||||
|
|
@ -286,6 +281,34 @@ private static string NewznabifySeasonNumber(int seasonNumber)
|
||||||
return seasonNumber == 0 ? "00" : seasonNumber.ToString();
|
return seasonNumber == 0 ? "00" : seasonNumber.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected string ResolveSiteLink()
|
||||||
|
{
|
||||||
|
var settingsBaseUrl = Settings?.BaseUrl;
|
||||||
|
var defaultLink = Definition.Links.First();
|
||||||
|
|
||||||
|
if (settingsBaseUrl == null)
|
||||||
|
{
|
||||||
|
return defaultLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Definition?.Legacylinks?.Contains(settingsBaseUrl) ?? false)
|
||||||
|
{
|
||||||
|
return defaultLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexerCapabilities GetCapabilities()
|
||||||
|
{
|
||||||
|
var capabilities = new IndexerCapabilities();
|
||||||
|
|
||||||
|
capabilities.ParseYmlSearchModes(Definition.Caps.Modes);
|
||||||
|
capabilities.MapYmlCategories(Definition);
|
||||||
|
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,18 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Indexers.Settings;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Newznab
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
|
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
|
||||||
{
|
{
|
||||||
private static readonly string[] ApiKeyWhiteList =
|
|
||||||
{
|
|
||||||
"nzbs.org",
|
|
||||||
"nzb.su",
|
|
||||||
"dognzb.cr",
|
|
||||||
"nzbplanet.net",
|
|
||||||
"nzbid.org",
|
|
||||||
"nzbndx.com",
|
|
||||||
"nzbindex.in"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static bool ShouldHaveApiKey(NewznabSettings settings)
|
|
||||||
{
|
|
||||||
if (settings.BaseUrl == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
|
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
|
||||||
|
|
||||||
public NewznabSettingsValidator()
|
public NewznabSettingsValidator()
|
||||||
{
|
{
|
||||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
|
||||||
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
|
||||||
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
|
||||||
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
|
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
|
||||||
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
|
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
|
||||||
|
|
||||||
|
|
@ -51,7 +29,7 @@ public NewznabSettingsValidator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NewznabSettings : IIndexerSettings
|
public class NewznabSettings : IYmlIndexerSettings
|
||||||
{
|
{
|
||||||
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
|
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
|
||||||
|
|
||||||
|
|
@ -61,7 +39,7 @@ public NewznabSettings()
|
||||||
VipExpiration = "";
|
VipExpiration = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
[FieldDefinition(0, Label = "URL")]
|
[FieldDefinition(0, 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; }
|
public string BaseUrl { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
|
||||||
|
|
@ -76,6 +54,9 @@ public NewznabSettings()
|
||||||
[FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")]
|
[FieldDefinition(6, Label = "VIP Expiration", HelpText = "Enter date (yyyy-mm-dd) for VIP Expiration or blank, Prowlarr will notify 1 week from expiration of VIP")]
|
||||||
public string VipExpiration { get; set; }
|
public string VipExpiration { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(0, Hidden = HiddenType.Hidden)]
|
||||||
|
public string DefinitionFile { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(7)]
|
[FieldDefinition(7)]
|
||||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class Torznab : TorrentIndexerBase<TorznabSettings>
|
||||||
|
|
||||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||||
{
|
{
|
||||||
return new NewznabRequestGenerator(_capabilitiesProvider)
|
return new GenericNewznabRequestGenerator(_capabilitiesProvider)
|
||||||
{
|
{
|
||||||
PageSize = PageSize,
|
PageSize = PageSize,
|
||||||
Settings = Settings
|
Settings = Settings
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ public TorznabSettingsValidator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings
|
public class TorznabSettings : GenericNewznabSettings, ITorrentIndexerSettings
|
||||||
{
|
{
|
||||||
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();
|
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
using DryIoc.ImTools;
|
||||||
|
using NzbDrone.Core.Indexers.Cardigann;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers
|
namespace NzbDrone.Core.Indexers
|
||||||
{
|
{
|
||||||
|
|
@ -127,7 +129,7 @@ public IndexerCapabilities()
|
||||||
LimitsMax = 100;
|
LimitsMax = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ParseCardigannSearchModes(Dictionary<string, List<string>> modes)
|
public void ParseYmlSearchModes(Dictionary<string, List<string>> modes)
|
||||||
{
|
{
|
||||||
if (modes == null || !modes.Any())
|
if (modes == null || !modes.Any())
|
||||||
{
|
{
|
||||||
|
|
@ -169,6 +171,48 @@ public void ParseCardigannSearchModes(Dictionary<string, List<string>> modes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MapYmlCategories(CardigannDefinition defFile)
|
||||||
|
{
|
||||||
|
if (defFile.Caps.Categories != null)
|
||||||
|
{
|
||||||
|
foreach (var category in defFile.Caps.Categories)
|
||||||
|
{
|
||||||
|
var cat = NewznabStandardCategory.GetCatByName(category.Value);
|
||||||
|
|
||||||
|
if (cat == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Categories.AddCategoryMapping(category.Key, cat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defFile.Caps.Categorymappings != null)
|
||||||
|
{
|
||||||
|
foreach (var categorymapping in defFile.Caps.Categorymappings)
|
||||||
|
{
|
||||||
|
IndexerCategory torznabCat = null;
|
||||||
|
|
||||||
|
if (categorymapping.cat != null)
|
||||||
|
{
|
||||||
|
torznabCat = NewznabStandardCategory.GetCatByName(categorymapping.cat);
|
||||||
|
if (torznabCat == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
|
||||||
|
|
||||||
|
//if (categorymapping.Default)
|
||||||
|
//{
|
||||||
|
// DefaultCategories.Add(categorymapping.id);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ParseTvSearchParams(IEnumerable<string> paramsList)
|
public void ParseTvSearchParams(IEnumerable<string> paramsList)
|
||||||
{
|
{
|
||||||
if (paramsList == null)
|
if (paramsList == null)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Indexers.Cardigann;
|
using NzbDrone.Core.Indexers.Cardigann;
|
||||||
using NzbDrone.Core.Indexers.Newznab;
|
using NzbDrone.Core.Indexers.Newznab;
|
||||||
|
using NzbDrone.Core.Indexers.Settings;
|
||||||
using NzbDrone.Core.IndexerVersions;
|
using NzbDrone.Core.IndexerVersions;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
@ -50,11 +51,11 @@ public override List<IndexerDefinition> All()
|
||||||
|
|
||||||
foreach (var definition in definitions)
|
foreach (var definition in definitions)
|
||||||
{
|
{
|
||||||
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
|
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MapCardigannDefinition(definition);
|
MapYmlDefinition(definition);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -73,11 +74,11 @@ public override IndexerDefinition Get(int id)
|
||||||
{
|
{
|
||||||
var definition = base.Get(id);
|
var definition = base.Get(id);
|
||||||
|
|
||||||
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
|
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MapCardigannDefinition(definition);
|
MapYmlDefinition(definition);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -93,9 +94,9 @@ protected override List<IndexerDefinition> Active()
|
||||||
return base.Active().Where(c => c.Enable).ToList();
|
return base.Active().Where(c => c.Enable).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MapCardigannDefinition(IndexerDefinition definition)
|
private void MapYmlDefinition(IndexerDefinition definition)
|
||||||
{
|
{
|
||||||
var settings = (CardigannSettings)definition.Settings;
|
var settings = (IYmlIndexerSettings)definition.Settings;
|
||||||
var defFile = _definitionService.GetCachedDefinition(settings.DefinitionFile);
|
var defFile = _definitionService.GetCachedDefinition(settings.DefinitionFile);
|
||||||
definition.ExtraFields = defFile.Settings;
|
definition.ExtraFields = defFile.Settings;
|
||||||
|
|
||||||
|
|
@ -121,51 +122,9 @@ private void MapCardigannDefinition(IndexerDefinition definition)
|
||||||
_ => IndexerPrivacy.SemiPrivate
|
_ => IndexerPrivacy.SemiPrivate
|
||||||
};
|
};
|
||||||
definition.Capabilities = new IndexerCapabilities();
|
definition.Capabilities = new IndexerCapabilities();
|
||||||
definition.Capabilities.ParseCardigannSearchModes(defFile.Caps.Modes);
|
definition.Capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
|
||||||
definition.Capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
|
definition.Capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
|
||||||
MapCardigannCategories(definition, defFile);
|
definition.Capabilities.MapYmlCategories(defFile);
|
||||||
}
|
|
||||||
|
|
||||||
private void MapCardigannCategories(IndexerDefinition def, CardigannDefinition defFile)
|
|
||||||
{
|
|
||||||
if (defFile.Caps.Categories != null)
|
|
||||||
{
|
|
||||||
foreach (var category in defFile.Caps.Categories)
|
|
||||||
{
|
|
||||||
var cat = NewznabStandardCategory.GetCatByName(category.Value);
|
|
||||||
|
|
||||||
if (cat == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
def.Capabilities.Categories.AddCategoryMapping(category.Key, cat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defFile.Caps.Categorymappings != null)
|
|
||||||
{
|
|
||||||
foreach (var categorymapping in defFile.Caps.Categorymappings)
|
|
||||||
{
|
|
||||||
IndexerCategory torznabCat = null;
|
|
||||||
|
|
||||||
if (categorymapping.cat != null)
|
|
||||||
{
|
|
||||||
torznabCat = NewznabStandardCategory.GetCatByName(categorymapping.cat);
|
|
||||||
if (torznabCat == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def.Capabilities.Categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
|
|
||||||
|
|
||||||
//if (categorymapping.Default)
|
|
||||||
//{
|
|
||||||
// DefaultCategories.Add(categorymapping.id);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<IndexerDefinition> GetDefaultDefinitions()
|
public override IEnumerable<IndexerDefinition> GetDefaultDefinitions()
|
||||||
|
|
@ -178,7 +137,7 @@ public override IEnumerable<IndexerDefinition> GetDefaultDefinitions()
|
||||||
}
|
}
|
||||||
|
|
||||||
var definitions = provider.DefaultDefinitions
|
var definitions = provider.DefaultDefinitions
|
||||||
.Where(v => v.Name != null && (v.Name != typeof(Cardigann.Cardigann).Name || v.Name != typeof(Newznab.Newznab).Name || v.Name != typeof(Torznab.Torznab).Name));
|
.Where(v => v.Name != null && (v.Name != typeof(Cardigann.Cardigann).Name || v.Name != typeof(Newznab.Newznab).Name || v.Name != typeof(Newznab.GenericNewznab).Name || v.Name != typeof(Torznab.Torznab).Name));
|
||||||
|
|
||||||
foreach (IndexerDefinition definition in definitions)
|
foreach (IndexerDefinition definition in definitions)
|
||||||
{
|
{
|
||||||
|
|
@ -203,7 +162,7 @@ public override void SetProviderCharacteristics(IIndexer provider, IndexerDefini
|
||||||
definition.SupportsRedirect = provider.SupportsRedirect;
|
definition.SupportsRedirect = provider.SupportsRedirect;
|
||||||
|
|
||||||
//We want to use the definition Caps and Privacy for Cardigann instead of the provider.
|
//We want to use the definition Caps and Privacy for Cardigann instead of the provider.
|
||||||
if (definition.Implementation != typeof(Cardigann.Cardigann).Name)
|
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) == null)
|
||||||
{
|
{
|
||||||
definition.IndexerUrls = provider.IndexerUrls;
|
definition.IndexerUrls = provider.IndexerUrls;
|
||||||
definition.LegacyUrls = provider.LegacyUrls;
|
definition.LegacyUrls = provider.LegacyUrls;
|
||||||
|
|
@ -288,15 +247,15 @@ public override IndexerDefinition Create(IndexerDefinition definition)
|
||||||
|
|
||||||
SetProviderCharacteristics(provider, definition);
|
SetProviderCharacteristics(provider, definition);
|
||||||
|
|
||||||
if (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
|
if (definition.Implementation == typeof(Newznab.GenericNewznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
|
||||||
{
|
{
|
||||||
var settings = (NewznabSettings)definition.Settings;
|
var settings = (GenericNewznabSettings)definition.Settings;
|
||||||
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
|
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
|
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
|
||||||
{
|
{
|
||||||
MapCardigannDefinition(definition);
|
MapYmlDefinition(definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.Create(definition);
|
return base.Create(definition);
|
||||||
|
|
@ -310,13 +269,13 @@ public override void Update(IndexerDefinition definition)
|
||||||
|
|
||||||
if (definition.Enable && (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name))
|
if (definition.Enable && (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name))
|
||||||
{
|
{
|
||||||
var settings = (NewznabSettings)definition.Settings;
|
var settings = (GenericNewznabSettings)definition.Settings;
|
||||||
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
|
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
|
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
|
||||||
{
|
{
|
||||||
MapCardigannDefinition(definition);
|
MapYmlDefinition(definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Update(definition);
|
base.Update(definition);
|
||||||
|
|
|
||||||
13
src/NzbDrone.Core/Indexers/Settings/IYmlIndexerSettings.cs
Normal file
13
src/NzbDrone.Core/Indexers/Settings/IYmlIndexerSettings.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Settings
|
||||||
|
{
|
||||||
|
public interface IYmlIndexerSettings : IIndexerSettings
|
||||||
|
{
|
||||||
|
public string DefinitionFile { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue