Compare commits

..

17 Commits

Author SHA1 Message Date
Qstick
57dcd861a9 Fixed: Validation for nested settings not running
Prevents #1243
2022-12-18 23:20:46 -06:00
Qstick
dfe132cda2 Fixed: Retain direct Indexer properties not affiliated with Prowlarr
Fixes #1165
2022-12-18 21:46:14 -06:00
Qstick
a635820b48 New: Sync Indexers button on index page
Fixes #92
2022-12-18 21:33:30 -06:00
Qstick
d959e81efb Modify Nab tests to pass for additional parameters
Fixes #1236
2022-12-18 21:19:03 -06:00
Qstick
ac89cd636f New: Separate setting for Pack Seed Time 2022-12-18 20:52:07 -06:00
Qstick
50616f5c9e Fixed: Don't mess with options we don't set on full sync 2022-12-18 20:52:07 -06:00
Qstick
3f9cb2c6ea Fixed: String compare in arr Indexer equality 2022-12-18 20:52:06 -06:00
Qstick
b5aa85a548 New: (Nebulance) Convert to API 2022-12-18 18:12:18 -06:00
Qstick
0fa5127c83 Cleanup dev logging in UI 2022-12-18 12:56:03 -06:00
Weblate
4d137886bc Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 89.6% (416 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: benniblot <ben2004engler@gmail.com>
Co-authored-by: mhng98 <mark.groenewegen@hotmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2022-12-18 10:41:16 -06:00
Qstick
9dde041c99 New: Search by description on add indexer modal
Fixes #1000
2022-12-18 00:13:44 -06:00
Qstick
a8234c9ce0 Fixed: Refresh applicable healthchecks on bulk deletes 2022-12-18 00:02:59 -06:00
Qstick
9227efdb65 New: (FileList) Freeleech Only option
Fixes #1147
2022-12-17 23:34:08 -06:00
Qstick
fa923e658f Fixed: (Nyaa) Torrent Age in UI incorrect
Fixes #144
2022-12-17 23:14:56 -06:00
bakerboy448
364a5564ae update-no-results-msg 2022-12-17 21:34:06 -06:00
Qstick
9efd0b391e fixup! 2022-12-17 21:31:37 -06:00
Qstick
320161e051 New: Smarter Newznab category mapping 2022-12-17 21:31:37 -06:00
68 changed files with 675 additions and 1028 deletions

View File

@@ -123,7 +123,7 @@ class AddIndexerModalContent extends Component {
const filteredIndexers = indexers.filter((indexer) => {
const { filter, filterProtocols, filterLanguages, filterPrivacyLevels } = this.state;
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase())) {
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
return false;
}

View File

@@ -272,6 +272,7 @@ class IndexerIndex extends Component {
saveError,
isDeleting,
isTestingAll,
isSyncingIndexers,
deleteError,
onScroll,
onSortSelect,
@@ -309,6 +310,15 @@ class IndexerIndex extends Component {
onPress={this.onAddIndexerPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={this.props.onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
@@ -493,10 +503,12 @@ IndexerIndex.propTypes = {
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
isTestingAll: PropTypes.bool.isRequired,
isSyncingIndexers: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onTestAllPress: PropTypes.func.isRequired,
onAppIndexerSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};

View File

@@ -2,10 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions';
import { saveIndexerEditor, setMovieFilter, setMovieSort, setMovieTableOption } from 'Store/Actions/indexerIndexActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createIndexerClientSideCollectionItemsSelector from 'Store/Selectors/createIndexerClientSideCollectionItemsSelector';
import IndexerIndex from './IndexerIndex';
@@ -13,13 +16,16 @@ import IndexerIndex from './IndexerIndex';
function createMapStateToProps() {
return createSelector(
createIndexerClientSideCollectionItemsSelector('indexerIndex'),
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
createDimensionsSelector(),
(
indexers,
isSyncingIndexers,
dimensionsState
) => {
return {
...indexers,
isSyncingIndexers,
isSmallScreen: dimensionsState.isSmallScreen
};
}
@@ -46,6 +52,12 @@ function createMapDispatchToProps(dispatch, props) {
onTestAllPress() {
dispatch(testAllIndexers());
},
onAppIndexerSyncPress() {
dispatch(executeCommand({
name: commandNames.APP_INDEXER_SYNC
}));
}
};
}

View File

@@ -81,8 +81,6 @@ class EditDownloadClientModalContent extends Component {
message
} = item;
console.log(supportsCategories);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>

View File

@@ -14,8 +14,6 @@ function createLanguagesSelector() {
return createSelector(
(state) => state.localization,
(localization) => {
console.log(localization);
const items = localization.items;
if (!items) {

View File

@@ -113,8 +113,6 @@ export default {
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
console.log(saveData);
// we have to set id since not actually posting to server yet
if (!saveData.id) {
saveData.id = getNextId(getState().settings.downloadClientCategories.items);

View File

@@ -310,8 +310,6 @@ export const actionHandlers = handleThunks({
isGrabbing: true
}));
console.log(payload);
const promise = createAjaxRequest({
url: '/search/bulk',
method: 'POST',

View File

@@ -18,6 +18,7 @@
<subcat id="5030" name="SD"/>
<subcat id="5060" name="Sport"/>
<subcat id="5010" name="WEB-DL"/>
<subcat id="5999" name="Other"/>
</category>
<category id="7000" name="Other">
<subcat id="7010" name="Misc"/>

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Xml;
@@ -15,14 +16,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[TestFixture]
public class NewznabCapabilitiesProviderFixture : CoreTest<NewznabCapabilitiesProvider>
{
private GenericNewznabSettings _settings;
private NewznabSettings _settings;
private IndexerDefinition _definition;
private string _caps;
[SetUp]
public void SetUp()
{
_settings = new GenericNewznabSettings()
_settings = new NewznabSettings()
{
BaseUrl = "http://indxer.local"
};
@@ -70,6 +71,32 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
caps.LimitsMax.Value.Should().Be(60);
}
[Test]
public void should_map_different_categories()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTorznabCapsToTrackers(new int[] { NewznabStandardCategory.Books.Id });
bookCats.Count.Should().Be(2);
bookCats.Should().Contain("8000");
}
[Test]
public void should_map_by_name_when_available()
{
GivenCapsResponse(_caps);
var caps = Subject.GetCapabilities(_settings, _definition);
var bookCats = caps.Categories.MapTrackerCatToNewznab("5999");
bookCats.Count.Should().Be(2);
bookCats.First().Id.Should().Be(5050);
}
[Test]
public void should_use_default_pagesize_if_missing()
{

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}

View File

@@ -10,7 +10,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
public class NewznabRequestGeneratorFixture : CoreTest<GenericNewznabRequestGenerator>
public class NewznabRequestGeneratorFixture : CoreTest<NewznabRequestGenerator>
{
private MovieSearchCriteria _movieSearchCriteria;
private TvSearchCriteria _tvSearchCriteria;
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[SetUp]
public void SetUp()
{
Subject.Settings = new GenericNewznabSettings()
Subject.Settings = new NewznabSettings()
{
BaseUrl = "http://127.0.0.1:1234/",
ApiKey = "abcd",
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_capabilities = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_capabilities);
}

View File

@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
_caps = new IndexerCapabilities();
Mocker.GetMock<INewznabCapabilitiesProvider>()
.Setup(v => v.GetCapabilities(It.IsAny<GenericNewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Setup(v => v.GetCapabilities(It.IsAny<NewznabSettings>(), It.IsAny<IndexerDefinition>()))
.Returns(_caps);
}

View File

@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Lidarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
lidarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !lidarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
lidarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match
_lidarrV1Proxy.UpdateIndexer(lidarrIndexer, Settings);
}
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Lidarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _lidarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Lidarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<LidarrField>()
};
lidarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +200,7 @@ namespace NzbDrone.Core.Applications.Lidarr
if (lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
}

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; }
public List<LidarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Lidarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,12 @@ namespace NzbDrone.Core.Applications.Radarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
radarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !radarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
radarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// Update the indexer if it still has categories that match
_radarrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
}
@@ -159,6 +165,7 @@ namespace NzbDrone.Core.Applications.Radarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _radarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +182,11 @@ namespace NzbDrone.Core.Applications.Radarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<RadarrField>()
};
radarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; }
public List<RadarrField> Fields { get; set; }
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Applications.Radarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Readarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
readarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !readarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match
_readarrV1Proxy.UpdateIndexer(readarrIndexer, Settings);
}
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Readarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _readarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Readarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<ReadarrField>()
};
readarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -191,7 +196,7 @@ namespace NzbDrone.Core.Applications.Readarr
if (readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime") != null)
{
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Readarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,13 @@ namespace NzbDrone.Core.Applications.Sonarr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any() || indexer.Capabilities.Categories.SupportedCategories(Settings.AnimeSyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
sonarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !sonarrIndexer.Fields.Any(s => s.Name == f.Name)));
// Retain user settings not-affiliated with Prowlarr
sonarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
sonarrIndexer.SeasonSearchMaximumSingleEpisodeAge = remoteIndexer.SeasonSearchMaximumSingleEpisodeAge;
// Update the indexer if it still has categories that match
_sonarrV3Proxy.UpdateIndexer(sonarrIndexer, Settings);
}
@@ -159,6 +166,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _sonarrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "animeCategories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.seasonPackSeedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +183,11 @@ namespace NzbDrone.Core.Applications.Sonarr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<SonarrField>()
};
sonarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
@@ -189,7 +199,7 @@ namespace NzbDrone.Core.Applications.Sonarr
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value = 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.SeedTime;
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value = ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.PackSeedTime ?? ((ITorrentIndexerSettings)indexer.Settings).TorrentBaseSettings.SeedTime;
}
return sonarrIndexer;

View File

@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Applications.Sonarr
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public int? SeasonSearchMaximumSingleEpisodeAge { get; set; }
public HashSet<int> Tags { get; set; }
public List<SonarrField> Fields { get; set; }
@@ -34,7 +36,7 @@ namespace NzbDrone.Core.Applications.Sonarr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -126,6 +126,8 @@ namespace NzbDrone.Core.Applications.Whisparr
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
whisparrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => !whisparrIndexer.Fields.Any(s => s.Name == f.Name)));
// Update the indexer if it still has categories that match
_whisparrV3Proxy.UpdateIndexer(whisparrIndexer, Settings);
}
@@ -159,6 +161,7 @@ namespace NzbDrone.Core.Applications.Whisparr
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new string[] { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
@@ -175,9 +178,11 @@ namespace NzbDrone.Core.Applications.Whisparr
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
Fields = new List<WhisparrField>()
};
whisparrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Applications.Whisparr
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var apiPathCompare = apiPath.Equals(otherApiPath);
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);

View File

@@ -1,14 +0,0 @@
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" });
}
}
}

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerCheck : HealthCheckBase
{

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerLongTermStatusCheck : HealthCheckBase
{

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerStatusCheck : HealthCheckBase
{

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class IndexerVIPCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class IndexerVIPExpiredCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class NoDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderBulkDeletedEvent<IIndexer>))]
public class OutdatedDefinitionCheck : HealthCheckBase
{
private readonly IIndexerDefinitionUpdateService _indexerDefinitionUpdateService;

View File

@@ -19,8 +19,7 @@ namespace NzbDrone.Core.IndexerVersions
{
public interface IIndexerDefinitionUpdateService
{
List<IndexerMetaDefinition> All();
List<IndexerMetaDefinition> AllForImplementation(string implementation);
List<CardigannMetaDefinition> All();
CardigannDefinition GetCachedDefinition(string fileKey);
List<string> GetBlocklist();
}
@@ -29,8 +28,8 @@ namespace NzbDrone.Core.IndexerVersions
{
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "newznab-yml";
private const int DEFINITION_VERSION = 8;
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 7;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()
@@ -79,9 +78,9 @@ namespace NzbDrone.Core.IndexerVersions
_logger = logger;
}
public List<IndexerMetaDefinition> All()
public List<CardigannMetaDefinition> All()
{
var indexerList = new List<IndexerMetaDefinition>();
var indexerList = new List<CardigannMetaDefinition>();
try
{
@@ -89,7 +88,7 @@ namespace NzbDrone.Core.IndexerVersions
try
{
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<IndexerMetaDefinition>>(request);
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
indexerList = response.Resource.Where(i => !_defintionBlocklist.Contains(i.File)).ToList();
}
catch
@@ -112,11 +111,6 @@ namespace NzbDrone.Core.IndexerVersions
return indexerList;
}
public List<IndexerMetaDefinition> AllForImplementation(string implementation)
{
return All().Where(d => d.Implementation == implementation.ToLower()).ToList();
}
public CardigannDefinition GetCachedDefinition(string fileKey)
{
if (string.IsNullOrEmpty(fileKey))
@@ -134,7 +128,7 @@ namespace NzbDrone.Core.IndexerVersions
return _defintionBlocklist;
}
private List<IndexerMetaDefinition> ReadDefinitionsFromDisk(List<IndexerMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
private List<CardigannMetaDefinition> ReadDefinitionsFromDisk(List<CardigannMetaDefinition> defs, string path, SearchOption options = SearchOption.TopDirectoryOnly)
{
var indexerList = defs;
@@ -151,7 +145,7 @@ namespace NzbDrone.Core.IndexerVersions
try
{
var definitionString = File.ReadAllText(file.FullName);
var definition = _deserializer.Deserialize<IndexerMetaDefinition>(definitionString);
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
definition.File = Path.GetFileNameWithoutExtension(file.Name);
@@ -249,11 +243,6 @@ namespace NzbDrone.Core.IndexerVersions
definition.Login.Method = "form";
}
if (definition.Search == null)
{
definition.Search = new SearchBlock();
}
if (definition.Search.Paths == null)
{
definition.Search.Paths = new List<SearchPathBlock>();

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
get
{
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
foreach (var def in _definitionService.All())
{
yield return GetDefinition(def);
}
@@ -98,7 +98,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
_generatorCache = cacheManager.GetRollingCache<CardigannRequestGenerator>(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5));
}
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
private IndexerDefinition GetDefinition(CardigannMetaDefinition definition)
{
var defaultSettings = new List<SettingsField>
{

View File

@@ -1,11 +1,10 @@
using System.Collections.Generic;
using NzbDrone.Core.Indexers.Cardigann;
namespace NzbDrone.Core.IndexerVersions
namespace NzbDrone.Core.Indexers.Cardigann
{
public class IndexerMetaDefinition
public class CardigannMetaDefinition
{
public IndexerMetaDefinition()
public CardigannMetaDefinition()
{
Legacylinks = new List<string>();
}
@@ -14,7 +13,6 @@ namespace NzbDrone.Core.IndexerVersions
public string File { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Implementation { get; set; }
public string Type { get; set; }
public string Language { get; set; }
public string Encoding { get; set; }

View File

@@ -1,22 +1,14 @@
using System.Collections.Generic;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Cardigann
{
public class CardigannSettingsValidator : AbstractValidator<CardigannSettings>
public class CardigannSettings : NoAuthTorrentBaseSettings
{
public CardigannSettingsValidator()
{
}
}
public class CardigannSettings : NoAuthTorrentBaseSettings, IYmlIndexerSettings
{
private static readonly CardigannSettingsValidator Validator = new CardigannSettingsValidator();
public CardigannSettings()
{
ExtraFieldData = new Dictionary<string, object>();
@@ -26,10 +18,5 @@ namespace NzbDrone.Core.Indexers.Cardigann
public string DefinitionFile { get; set; }
public Dictionary<string, object> ExtraFieldData { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -109,6 +109,11 @@ namespace NzbDrone.Core.Indexers.FileList
var baseUrl = string.Format("{0}/api.php?action={1}&category={2}&username={3}&passkey={4}{5}", Settings.BaseUrl.TrimEnd('/'), searchType, categoriesQuery, Settings.Username.Trim(), Settings.Passkey.Trim(), parameters);
if (Settings.FreeleechOnly)
{
baseUrl += "&freeleech=1";
}
yield return new IndexerRequest(baseUrl, HttpAccept.Json);
}
}

View File

@@ -29,6 +29,9 @@ namespace NzbDrone.Core.Indexers.FileList
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Passkey { get; set; }
[FieldDefinition(4, Label = "Freeleech Only", HelpText = "Search Freeleech torrents only", Type = FieldType.Checkbox)]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Indexers.Headphones
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
}
}

View File

@@ -1,23 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using FluentValidation;
using Newtonsoft.Json;
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;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
@@ -25,7 +21,6 @@ namespace NzbDrone.Core.Indexers.Definitions
{
public override string Name => "Nebulance";
public override string[] IndexerUrls => new string[] { "https://nebulance.io/" };
private string LoginUrl => Settings.BaseUrl + "login.php";
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;
@@ -48,53 +43,13 @@ namespace NzbDrone.Core.Indexers.Definitions
return new NebulanceParser(Settings, Capabilities.Categories);
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true
};
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)
.AddFormParameter("twofa", Settings.TwoFactorAuth)
.AddFormParameter("keeplogged", "on")
.AddFormParameter("login", "Login")
.SetHeader("Content-Type", "multipart/form-data")
.Build();
var response = await ExecuteAuth(authLoginRequest);
cookies = response.GetCookies();
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
_logger.Debug("Nebulance authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
if (!httpResponse.Content.Contains("logout.php"))
{
return true;
}
return false;
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId
}
};
@@ -115,30 +70,16 @@ namespace NzbDrone.Core.Indexers.Definitions
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
{
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
var apiUrl = Settings.BaseUrl + "api.php";
var searchTerm = term;
var builder = new JsonRpcRequestBuilder(apiUrl)
.Call("getTorrents", Settings.ApiKey, parameters, results ?? 100, offset ?? 0);
if (!string.IsNullOrWhiteSpace(searchTerm))
{
searchTerm = Regex.Replace(searchTerm, @"[-._]", " ");
}
builder.SuppressHttpError = true;
var qc = new NameValueCollection
{
{ "action", "basic" },
{ "order_by", "time" },
{ "order_way", "desc" },
{ "searchtext", searchTerm }
};
searchUrl = searchUrl + "?" + qc.GetQueryString();
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
yield return new IndexerRequest(builder.Build());
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
@@ -159,7 +100,27 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
var queryParams = new NebulanceQuery
{
Age = ">0"
};
if (searchCriteria.SanitizedTvSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedTvSearchString + "%";
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.ImdbId, out var intImdb))
{
queryParams.Imdb = intImdb;
if (searchCriteria.EpisodeSearchString.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.EpisodeSearchString + "%";
}
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
return pageableRequests;
}
@@ -175,7 +136,17 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
var queryParams = new NebulanceQuery
{
Age = ">0"
};
if (searchCriteria.SanitizedSearchTerm.IsNotNullOrWhiteSpace())
{
queryParams.Name = "%" + searchCriteria.SanitizedSearchTerm + "%";
}
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
return pageableRequests;
}
@@ -199,60 +170,38 @@ namespace NzbDrone.Core.Indexers.Definitions
{
var torrentInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var document = parser.ParseDocument(indexerResponse.Content);
var rows = document.QuerySelectorAll(".torrent_table > tbody > tr[class^='torrent row']");
JsonRpcResponse<NebulanceTorrents> jsonResponse = new HttpResponse<JsonRpcResponse<NebulanceTorrents>>(indexerResponse.HttpResponse).Resource;
if (jsonResponse.Error != null || jsonResponse.Result == null)
{
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
}
if (jsonResponse.Result.Items.Count == 0)
{
return torrentInfos;
}
var rows = jsonResponse.Result.Items;
foreach (var row in rows)
{
var title = row.QuerySelector("a[data-src]").GetAttribute("data-src");
if (string.IsNullOrEmpty(title) || title == "0")
{
title = row.QuerySelector("a[data-src]").TextContent;
title = Regex.Replace(title, @"[\[\]\/]", "");
}
else
{
if (title.Length > 5 && title.Substring(title.Length - 5).Contains("."))
{
title = title.Remove(title.LastIndexOf(".", StringComparison.Ordinal));
}
}
var posterStr = row.QuerySelector("img")?.GetAttribute("src");
Uri.TryCreate(posterStr, UriKind.Absolute, out var poster);
var details = _settings.BaseUrl + row.QuerySelector("a[data-src]").GetAttribute("href");
var link = _settings.BaseUrl + row.QuerySelector("a[href*='action=download']").GetAttribute("href");
var qColSize = row.QuerySelector("td:nth-child(3)");
var size = ParseUtil.GetBytes(qColSize.Children[0].TextContent);
var files = ParseUtil.CoerceInt(qColSize.Children[1].TextContent.Split(':')[1].Trim());
var qPublishdate = row.QuerySelector("td:nth-child(4) span");
var publishDateStr = qPublishdate.GetAttribute("title");
var publishDate = !string.IsNullOrEmpty(publishDateStr) && publishDateStr.Contains(",")
? DateTime.ParseExact(publishDateStr, "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture)
: DateTime.ParseExact(qPublishdate.TextContent.Trim(), "MMM dd yyyy, HH:mm", CultureInfo.InvariantCulture);
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(5)").TextContent);
var seeds = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
var release = new TorrentInfo
{
Title = title,
Title = row.ReleaseTitle,
Guid = details,
InfoUrl = details,
PosterUrl = poster?.AbsoluteUri ?? null,
DownloadUrl = link,
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(title) },
Size = size,
Files = files,
PublishDate = publishDate,
Grabs = grabs,
Seeders = seeds,
Peers = seeds + leechers,
PosterUrl = row.Banner,
DownloadUrl = row.Download,
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
Size = ParseUtil.CoerceLong(row.Size),
Files = row.FileList.Length,
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal),
Grabs = ParseUtil.CoerceInt(row.Snatch),
Seeders = ParseUtil.CoerceInt(row.Seed),
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
MinimumRatio = 0, // ratioless
MinimumSeedTime = 86400, // 24 hours
DownloadVolumeFactor = 0, // ratioless tracker
@@ -268,14 +217,69 @@ namespace NzbDrone.Core.Indexers.Definitions
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class NebulanceSettings : UserPassTorrentBaseSettings
public class NebulanceSettings : NoAuthTorrentBaseSettings
{
public NebulanceSettings()
{
TwoFactorAuth = "";
ApiKey = "";
}
[FieldDefinition(4, Label = "Two Factor Auth", HelpText = "Two-Factor Auth")]
public string TwoFactorAuth { get; set; }
[FieldDefinition(4, Label = "API Key", HelpText = "API Key from User Settings > Api Keys. Key must have List and Download permissions")]
public string ApiKey { get; set; }
}
public class NebulanceQuery
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Id { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Time { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Age { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Tvmaze { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Imdb { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Hash { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string[] Tags { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Category { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Series { get; set; }
public NebulanceQuery Clone()
{
return MemberwiseClone() as NebulanceQuery;
}
}
public class NebulanceTorrent
{
[JsonProperty(PropertyName = "rls_name")]
public string ReleaseTitle { get; set; }
public string Title { get; set; }
public string Size { get; set; }
public string Seed { get; set; }
public string Leech { get; set; }
public string Snatch { get; set; }
public string Download { get; set; }
[JsonProperty(PropertyName = "file_list")]
public string[] FileList { get; set; }
[JsonProperty(PropertyName = "series_banner")]
public string Banner { get; set; }
[JsonProperty(PropertyName = "group_id")]
public string TorrentId { get; set; }
[JsonProperty(PropertyName = "rls_utc")]
public string PublishDateUtc { get; set; }
}
public class NebulanceTorrents
{
public List<NebulanceTorrent> Items { get; set; }
public int Results { get; set; }
}
}

View File

@@ -1,195 +0,0 @@
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");
}
}
}
}

View File

@@ -1,292 +0,0 @@
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; }
}
}

View File

@@ -1,91 +0,0 @@
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));
}
}
}

View File

@@ -5,10 +5,9 @@ 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.Indexers.Cardigann;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -17,10 +16,10 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public class Newznab : UsenetIndexerBase<NewznabSettings>
{
private readonly IIndexerDefinitionUpdateService _definitionService;
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public override string Name => "Newznab";
public override string[] IndexerUrls => new string[] { "" };
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;
@@ -28,72 +27,130 @@ namespace NzbDrone.Core.Indexers.Newznab
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()
{
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
return new NewznabRequestGenerator()
return new NewznabRequestGenerator(_capabilitiesProvider)
{
PageSize = PageSize,
Settings = Settings,
Definition = defFile
Settings = Settings
};
}
public override IParseIndexerResponse GetParser()
{
var defFile = _definitionService.GetCachedDefinition(Settings.DefinitionFile);
var capabilities = new IndexerCapabilities();
capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
capabilities.MapYmlCategories(defFile);
return new NewznabRssParser(Settings, Definition, _capabilitiesProvider);
}
return new GenericNewznabRssParser(capabilities.Categories.GetTorznabCategoryList());
public string[] GetBaseUrlFromSettings()
{
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
{
get
{
foreach (var def in _definitionService.AllForImplementation(GetType().Name))
{
yield return GetDefinition(def);
}
yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"));
yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"));
yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"));
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(IIndexerDefinitionUpdateService definitionService, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, IValidateNzbs nzbValidationService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, nzbValidationService, logger)
{
_definitionService = definitionService;
_capabilitiesProvider = capabilitiesProvider;
}
private IndexerDefinition GetDefinition(IndexerMetaDefinition definition)
private IndexerDefinition GetDefinition(string name, NewznabSettings settings)
{
return new IndexerDefinition
{
Enable = true,
Name = definition.Name,
Language = definition.Language,
Description = definition.Description,
Name = name,
Implementation = GetType().Name,
IndexerUrls = definition.Links.ToArray(),
LegacyUrls = definition.Legacylinks.ToArray(),
Settings = new NewznabSettings { DefinitionFile = definition.File },
Settings = settings,
Protocol = DownloadProtocol.Usenet,
Privacy = definition.Type switch
{
"private" => IndexerPrivacy.Private,
"public" => IndexerPrivacy.Public,
_ => IndexerPrivacy.SemiPrivate
},
Privacy = IndexerPrivacy.Private,
SupportsRss = SupportsRss,
SupportsSearch = SupportsSearch,
SupportsRedirect = SupportsRedirect,
Capabilities = new IndexerCapabilities()
Capabilities = Capabilities
};
}
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)
{
await base.Test(failures);
@@ -101,21 +158,61 @@ namespace NzbDrone.Core.Indexers.Newznab
{
return;
}
failures.AddIfNotNull(TestCapabilities());
}
public override object RequestAction(string action, IDictionary<string, string> query)
protected static List<int> CategoryIds(IndexerCapabilitiesCategories categories)
{
if (action == "getUrls")
var l = categories.GetTorznabCategoryTree().Select(c => c.Id).ToList();
return l;
}
protected virtual ValidationFailure TestCapabilities()
{
try
{
var devices = ((IndexerDefinition)Definition).IndexerUrls;
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
return new
if (capabilities.SearchParams != null && capabilities.SearchParams.Contains(SearchParam.Q))
{
options = devices.Select(d => new { Value = d, Name = d })
};
}
return null;
}
return null;
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
{
return null;
}
if (capabilities.TvSearchParams != null &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, 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");
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml;
using System.Xml.Linq;
@@ -14,7 +15,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public interface INewznabCapabilitiesProvider
{
IndexerCapabilities GetCapabilities(GenericNewznabSettings settings, ProviderDefinition definition);
IndexerCapabilities GetCapabilities(NewznabSettings settings, ProviderDefinition definition);
}
public class NewznabCapabilitiesProvider : INewznabCapabilitiesProvider
@@ -30,7 +31,7 @@ namespace NzbDrone.Core.Indexers.Newznab
_logger = logger;
}
public IndexerCapabilities GetCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
public IndexerCapabilities GetCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
{
var key = indexerSettings.ToJson();
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings, definition), TimeSpan.FromDays(7));
@@ -38,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Newznab
return capabilities;
}
private IndexerCapabilities FetchCapabilities(GenericNewznabSettings indexerSettings, ProviderDefinition definition)
private IndexerCapabilities FetchCapabilities(NewznabSettings indexerSettings, ProviderDefinition definition)
{
var capabilities = new IndexerCapabilities();
@@ -96,7 +97,7 @@ namespace NzbDrone.Core.Indexers.Newznab
throw new XmlException("Invalid XML").WithData(response);
}
GenericNewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
NewznabRssParser.CheckError(xDoc, new IndexerResponse(new IndexerRequest(response.Request), response));
var xmlRoot = xDoc.Element("caps");
@@ -221,27 +222,59 @@ namespace NzbDrone.Core.Indexers.Newznab
{
foreach (var xmlCategory in xmlCategories.Elements("category"))
{
var cat = new IndexerCategory
var parentName = xmlCategory.Attribute("name").Value;
var parentId = int.Parse(xmlCategory.Attribute("id").Value);
var mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => parentName.ToLower().Contains(x.Name.ToLower()));
if (mappedCat == null)
{
Id = int.Parse(xmlCategory.Attribute("id").Value),
Name = xmlCategory.Attribute("name").Value,
Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty
};
// Try by parent id if name fails
mappedCat = NewznabStandardCategory.ParentCats.FirstOrDefault(x => x.Id == parentId);
}
if (mappedCat == null)
{
// Fallback to Other
mappedCat = NewznabStandardCategory.Other;
}
foreach (var xmlSubcat in xmlCategory.Elements("subcat"))
{
var subCat = new IndexerCategory
{
Id = int.Parse(xmlSubcat.Attribute("id").Value),
Name = xmlSubcat.Attribute("name").Value,
Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty
};
var subName = xmlSubcat.Attribute("name").Value;
var subId = int.Parse(xmlSubcat.Attribute("id").Value);
cat.SubCategories.Add(subCat);
capabilities.Categories.AddCategoryMapping(subCat.Name, subCat);
var mappingName = $"{mappedCat.Name}/{subName}";
var mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == mappingName.ToLower());
if (mappedSubCat == null)
{
// Try by child id if name fails
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Id == subId);
}
if (mappedSubCat == null && mappedCat.Id != NewznabStandardCategory.Other.Id)
{
// Try by Parent/Other if parent is not other
mappedSubCat = NewznabStandardCategory.AllCats.FirstOrDefault(x => x.Name.ToLower() == $"{mappedCat.Name.ToLower()}/other");
}
if (mappedSubCat == null)
{
// Fallback to Misc Other
mappedSubCat = NewznabStandardCategory.OtherMisc;
}
if (mappedSubCat != null)
{
capabilities.Categories.AddCategoryMapping(subId, mappedSubCat, $"{parentName}/{subName}");
}
}
capabilities.Categories.AddCategoryMapping(cat.Name, cat);
if (mappedCat != null)
{
capabilities.Categories.AddCategoryMapping(parentId, mappedCat, parentName);
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Linq;
using DryIoc;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
@@ -14,20 +13,23 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabRequestGenerator : IIndexerRequestGenerator
{
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public int MaxPages { get; set; }
public int PageSize { get; set; }
public NewznabSettings Settings { get; set; }
public CardigannDefinition Definition { get; set; }
public ProviderDefinition Definition { get; set; }
public NewznabRequestGenerator()
public NewznabRequestGenerator(INewznabCapabilitiesProvider capabilitiesProvider)
{
_capabilitiesProvider = capabilitiesProvider;
MaxPages = 30;
PageSize = 100;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var capabilities = GetCapabilities();
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -65,14 +67,16 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var capabilities = GetCapabilities();
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -105,14 +109,16 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var capabilities = GetCapabilities();
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -170,14 +176,16 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var capabilities = GetCapabilities();
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -210,15 +218,16 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
pageableRequests.Add(GetPagedRequests(searchCriteria, capabilities, parameters));
pageableRequests.Add(GetPagedRequests(searchCriteria,
capabilities,
parameters));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var capabilities = GetCapabilities();
var capabilities = _capabilitiesProvider.GetCapabilities(Settings, Definition);
var pageableRequests = new IndexerPageableRequestChain();
var parameters = new NameValueCollection();
@@ -235,7 +244,7 @@ namespace NzbDrone.Core.Indexers.Newznab
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, IndexerCapabilities capabilities, NameValueCollection parameters)
{
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", ResolveSiteLink().TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
var baseUrl = string.Format("{0}{1}?t={2}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchCriteria.SearchType);
var categories = capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
if (categories != null && categories.Any())
@@ -281,34 +290,6 @@ namespace NzbDrone.Core.Indexers.Newznab
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 Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

View File

@@ -5,20 +5,25 @@ using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Newznab
{
public class GenericNewznabRssParser : RssParser
public class NewznabRssParser : RssParser
{
public const string ns = "{http://www.newznab.com/DTD/2010/feeds/attributes/}";
private readonly List<IndexerCategory> _categories;
private readonly NewznabSettings _settings;
private readonly ProviderDefinition _definition;
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public GenericNewznabRssParser(List<IndexerCategory> categories)
public NewznabRssParser(NewznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
{
PreferredEnclosureMimeTypes = UsenetEnclosureMimeTypes;
UseEnclosureUrl = true;
_categories = categories;
_settings = settings;
_definition = definition;
_capabilitiesProvider = capabilitiesProvider;
}
public static void CheckError(XDocument xdoc, IndexerResponse indexerResponse)
@@ -118,19 +123,17 @@ namespace NzbDrone.Core.Indexers.Newznab
protected override ICollection<IndexerCategory> GetCategory(XElement item)
{
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
var cats = TryGetMultipleNewznabAttributes(item, "category");
var results = new List<IndexerCategory>();
foreach (var cat in cats)
{
if (int.TryParse(cat, out var intCategory))
{
var indexerCat = _categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
if (indexerCat != null)
{
results.Add(indexerCat);
}
if (indexerCat != null)
{
results.AddRange(indexerCat);
}
}

View File

@@ -4,18 +4,41 @@ using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Newznab
{
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);
public NewznabSettingsValidator()
{
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
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());
@@ -29,7 +52,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
public class NewznabSettings : IYmlIndexerSettings
public class NewznabSettings : IIndexerSettings
{
private static readonly NewznabSettingsValidator Validator = new NewznabSettingsValidator();
@@ -39,7 +62,7 @@ namespace NzbDrone.Core.Indexers.Newznab
VipExpiration = "";
}
[FieldDefinition(0, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
[FieldDefinition(0, Label = "URL")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)]
@@ -54,9 +77,6 @@ namespace NzbDrone.Core.Indexers.Newznab
[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(0, Hidden = HiddenType.Hidden)]
public string DefinitionFile { get; set; }
[FieldDefinition(7)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();

View File

@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Torznab
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new GenericNewznabRequestGenerator(_capabilitiesProvider)
return new NewznabRequestGenerator(_capabilitiesProvider)
{
PageSize = PageSize,
Settings = Settings
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Torznab
public override IParseIndexerResponse GetParser()
{
return new TorznabRssParser(Settings);
return new TorznabRssParser(Settings, Definition, _capabilitiesProvider);
}
public string[] GetBaseUrlFromSettings()
@@ -157,13 +157,13 @@ namespace NzbDrone.Core.Indexers.Torznab
}
if (capabilities.MovieSearchParams != null &&
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
new[] { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId, MovieSearchParam.TraktId }.Any(v => capabilities.MovieSearchParams.Contains(v)))
{
return null;
}
if (capabilities.TvSearchParams != null &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.ImdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;

View File

@@ -4,7 +4,9 @@ using System.Linq;
using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Torznab
{
@@ -13,10 +15,15 @@ namespace NzbDrone.Core.Indexers.Torznab
public const string ns = "{http://torznab.com/schemas/2015/feed}";
private readonly TorznabSettings _settings;
public TorznabRssParser(TorznabSettings settings)
private readonly ProviderDefinition _definition;
private readonly INewznabCapabilitiesProvider _capabilitiesProvider;
public TorznabRssParser(TorznabSettings settings, ProviderDefinition definition, INewznabCapabilitiesProvider capabilitiesProvider)
{
UseEnclosureUrl = true;
_settings = settings;
_definition = definition;
_capabilitiesProvider = capabilitiesProvider;
}
protected override bool PreProcess(IndexerResponse indexerResponse)
@@ -157,19 +164,17 @@ namespace NzbDrone.Core.Indexers.Torznab
protected override ICollection<IndexerCategory> GetCategory(XElement item)
{
var capabilities = _capabilitiesProvider.GetCapabilities(_settings, _definition);
var cats = TryGetMultipleNewznabAttributes(item, "category");
var results = new List<IndexerCategory>();
foreach (var cat in cats)
{
if (int.TryParse(cat, out var intCategory))
{
var indexerCat = _settings.Categories?.FirstOrDefault(c => c.Id == intCategory) ?? null;
var indexerCat = capabilities.Categories.MapTrackerCatToNewznab(cat);
if (indexerCat != null)
{
results.Add(indexerCat);
}
if (indexerCat != null)
{
results.AddRange(indexerCat);
}
}

View File

@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers.Torznab
public TorznabSettingsValidator()
{
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.ApiPath).ValidUrlBase("/api");
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
@@ -37,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Torznab
}
}
public class TorznabSettings : GenericNewznabSettings, ITorrentIndexerSettings
public class TorznabSettings : NewznabSettings, ITorrentIndexerSettings
{
private static readonly TorznabSettingsValidator Validator = new TorznabSettingsValidator();

View File

@@ -448,7 +448,7 @@ namespace NzbDrone.Core.Indexers
if (releases.Releases.Empty())
{
return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer or your indexer category settings.");
return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer, your indexer category settings, or other indexer settings such as search freeleech only etc.");
}
}
catch (IndexerAuthException ex)

View File

@@ -1,5 +1,7 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers
{
@@ -7,6 +9,9 @@ namespace NzbDrone.Core.Indexers
{
public IndexerCommonSettingsValidator()
{
RuleFor(c => c.QueryLimit).GreaterThan(0).When(c => c.QueryLimit.HasValue).WithMessage("Should be greater than zero");
RuleFor(c => c.GrabLimit).GreaterThan(0).When(c => c.GrabLimit.HasValue).WithMessage("Should be greater than zero");
}
}

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using DryIoc.ImTools;
using NzbDrone.Core.Indexers.Cardigann;
namespace NzbDrone.Core.Indexers
{
@@ -129,7 +127,7 @@ namespace NzbDrone.Core.Indexers
LimitsMax = 100;
}
public void ParseYmlSearchModes(Dictionary<string, List<string>> modes)
public void ParseCardigannSearchModes(Dictionary<string, List<string>> modes)
{
if (modes == null || !modes.Any())
{
@@ -171,48 +169,6 @@ namespace NzbDrone.Core.Indexers
}
}
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)
{
if (paramsList == null)

View File

@@ -7,10 +7,10 @@ using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.Indexers
{
@@ -18,7 +18,6 @@ namespace NzbDrone.Core.Indexers
{
List<IIndexer> Enabled(bool filterBlockedIndexers = true);
List<IIndexer> AllProviders(bool filterBlockedIndexers = true);
void DeleteIndexers(List<int> indexerIds);
}
public class IndexerFactory : ProviderFactory<IIndexer, IndexerDefinition>, IIndexerFactory
@@ -51,11 +50,11 @@ namespace NzbDrone.Core.Indexers
foreach (var definition in definitions)
{
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
{
try
{
MapYmlDefinition(definition);
MapCardigannDefinition(definition);
}
catch
{
@@ -74,11 +73,11 @@ namespace NzbDrone.Core.Indexers
{
var definition = base.Get(id);
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
{
try
{
MapYmlDefinition(definition);
MapCardigannDefinition(definition);
}
catch
{
@@ -94,9 +93,9 @@ namespace NzbDrone.Core.Indexers
return base.Active().Where(c => c.Enable).ToList();
}
private void MapYmlDefinition(IndexerDefinition definition)
private void MapCardigannDefinition(IndexerDefinition definition)
{
var settings = (IYmlIndexerSettings)definition.Settings;
var settings = (CardigannSettings)definition.Settings;
var defFile = _definitionService.GetCachedDefinition(settings.DefinitionFile);
definition.ExtraFields = defFile.Settings;
@@ -122,9 +121,51 @@ namespace NzbDrone.Core.Indexers
_ => IndexerPrivacy.SemiPrivate
};
definition.Capabilities = new IndexerCapabilities();
definition.Capabilities.ParseYmlSearchModes(defFile.Caps.Modes);
definition.Capabilities.ParseCardigannSearchModes(defFile.Caps.Modes);
definition.Capabilities.SupportsRawSearch = defFile.Caps.Allowrawsearch;
definition.Capabilities.MapYmlCategories(defFile);
MapCardigannCategories(definition, 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()
@@ -137,7 +178,7 @@ namespace NzbDrone.Core.Indexers
}
var definitions = provider.DefaultDefinitions
.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));
.Where(v => v.Name != null && (v.Name != typeof(Cardigann.Cardigann).Name || v.Name != typeof(Newznab.Newznab).Name || v.Name != typeof(Torznab.Torznab).Name));
foreach (IndexerDefinition definition in definitions)
{
@@ -162,7 +203,7 @@ namespace NzbDrone.Core.Indexers
definition.SupportsRedirect = provider.SupportsRedirect;
//We want to use the definition Caps and Privacy for Cardigann instead of the provider.
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) == null)
if (definition.Implementation != typeof(Cardigann.Cardigann).Name)
{
definition.IndexerUrls = provider.IndexerUrls;
definition.LegacyUrls = provider.LegacyUrls;
@@ -215,18 +256,6 @@ namespace NzbDrone.Core.Indexers
}
}
public void DeleteIndexers(List<int> indexerIds)
{
var indexersToDelete = _providerRepository.Get(indexerIds).ToList();
_providerRepository.DeleteMany(indexerIds);
foreach (var indexer in indexersToDelete)
{
_logger.Info("Deleted indexer {0}", indexer.Name);
}
}
public override ValidationResult Test(IndexerDefinition definition)
{
var result = base.Test(definition);
@@ -247,15 +276,15 @@ namespace NzbDrone.Core.Indexers
SetProviderCharacteristics(provider, definition);
if (definition.Implementation == typeof(Newznab.GenericNewznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
if (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name)
{
var settings = (GenericNewznabSettings)definition.Settings;
var settings = (NewznabSettings)definition.Settings;
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
}
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
{
MapYmlDefinition(definition);
MapCardigannDefinition(definition);
}
return base.Create(definition);
@@ -269,13 +298,13 @@ namespace NzbDrone.Core.Indexers
if (definition.Enable && (definition.Implementation == typeof(Newznab.Newznab).Name || definition.Implementation == typeof(Torznab.Torznab).Name))
{
var settings = (GenericNewznabSettings)definition.Settings;
var settings = (NewznabSettings)definition.Settings;
settings.Categories = _newznabCapabilitiesProvider.GetCapabilities(settings, definition)?.Categories.GetTorznabCategoryList() ?? null;
}
if (definition.Settings.GetType().GetInterface(nameof(IYmlIndexerSettings)) != null)
if (definition.Implementation == typeof(Cardigann.Cardigann).Name)
{
MapYmlDefinition(definition);
MapCardigannDefinition(definition);
}
base.Update(definition);

View File

@@ -43,5 +43,8 @@ namespace NzbDrone.Core.Indexers
[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)]
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)]
public int? PackSeedTime { get; set; }
}
}

View File

@@ -11,6 +11,8 @@ namespace NzbDrone.Core.Indexers.Settings
public CookieBaseSettingsValidator()
{
RuleFor(c => c.Cookie).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
@@ -27,10 +29,10 @@ namespace NzbDrone.Core.Indexers.Settings
[FieldDefinition(2, Label = "Cookie", HelpText = "Site Cookie", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Cookie { get; set; }
[FieldDefinition(3)]
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
[FieldDefinition(4)]
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public virtual NzbDroneValidationResult Validate()

View File

@@ -1,13 +0,0 @@
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; }
}
}

View File

@@ -6,6 +6,11 @@ namespace NzbDrone.Core.Indexers.Settings
{
public class NoAuthSettingsValidator : AbstractValidator<NoAuthTorrentBaseSettings>
{
public NoAuthSettingsValidator()
{
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
public class NoAuthTorrentBaseSettings : ITorrentIndexerSettings
@@ -15,10 +20,10 @@ namespace NzbDrone.Core.Indexers.Settings
[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(2)]
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
[FieldDefinition(3)]
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public virtual NzbDroneValidationResult Validate()

View File

@@ -12,6 +12,8 @@ namespace NzbDrone.Core.Indexers.Settings
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
RuleFor(x => x.BaseSettings).SetValidator(new IndexerCommonSettingsValidator());
RuleFor(x => x.TorrentBaseSettings).SetValidator(new IndexerTorrentSettingsValidator());
}
}
@@ -32,10 +34,10 @@ namespace NzbDrone.Core.Indexers.Settings
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4)]
[FieldDefinition(10)]
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
[FieldDefinition(5)]
[FieldDefinition(11)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public NzbDroneValidationResult Validate()

View File

@@ -141,7 +141,7 @@
"BackupRetentionHelpText": "Automatische Backups, die älter als die Aufbewahrungsfrist sind, werden automatisch gelöscht",
"Backups": "Backups",
"BindAddress": "Adresse binden",
"BindAddressHelpText": "Gültige IP-Adresse, \"localhost\" oder \"*\" für alle Netzwerke",
"BindAddressHelpText": "Gültige IP Adresse oder \"*\" für alle Netzwerke",
"Branch": "Git-Branch",
"BypassProxyForLocalAddresses": "Proxy für lokale Adressen umgehen",
"CertificateValidation": "Zertifikat Validierung",

View File

@@ -35,7 +35,7 @@
"Fixed": "Korjattu",
"FocusSearchBox": "Kohdista hakukenttä",
"ForMoreInformationOnTheIndividualDownloadClients": "Lue lisää lataustyökalusta painamalla 'Lisätietoja'.",
"HideAdvanced": "Piilota lisäasetukset",
"HideAdvanced": "Piilota edistyneet",
"History": "Historia",
"MIA": "Puuttuu",
"New": "Uusi",

View File

@@ -54,7 +54,7 @@
"BranchUpdateMechanism": "A külső frissítési mechanizmus által használt ágazat",
"BranchUpdate": "Ágazattípus a Prowlarr frissítéseihez",
"Branch": "Ágazat",
"BindAddressHelpText": "Érvényes IP-cím, localhost, vagy „*” minden interfészhez",
"BindAddressHelpText": "Érvényes IP-cím, localhost vagy '*' minden interfészhez",
"BindAddress": "Kapcsolási Cím",
"BeforeUpdate": "Alkalmazásfrissítés előtt",
"Backups": "Biztonsági Mentés",

View File

@@ -419,5 +419,6 @@
"LastExecution": "Laatste Uitvoering",
"Queued": "Afwachtend",
"Categories": "Categorieën",
"AddSyncProfile": "Synchronisatieprofiel toevoegen"
"AddSyncProfile": "Synchronisatieprofiel toevoegen",
"AudioSearch": "auditief zoeken"
}

View File

@@ -79,7 +79,7 @@
"Backups": "Backups",
"BeforeUpdate": "Antes da atualização",
"BindAddress": "Endereço de vínculo",
"BindAddressHelpText": "Endereço IP4 válido ou \"*\" para todas as interfaces",
"BindAddressHelpText": "Endereço IP válido, localhost ou '*' para todas as interfaces",
"Branch": "Ramificação",
"BranchUpdate": "Ramificação para atualização do Prowlarr",
"BypassProxyForLocalAddresses": "Ignorar proxy para endereços locais",
@@ -447,7 +447,7 @@
"AddSyncProfile": "Adicionar Perfil de Sincronização",
"MinimumSeeders": "Mínimo de Seeders",
"MinimumSeedersHelpText": "Semeadores mínimos exigidos pelo aplicativo para o indexador baixar",
"ThemeHelpText": "Altere o tema da IU do Prowlarr, inspirado em {0}",
"ThemeHelpText": "Alterar o tema da interface do usuário do aplicativo, o tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por {0}",
"InstanceName": "Nome da instância",
"InstanceNameHelpText": "Nome da instância na guia e para o nome do aplicativo Syslog",
"Duration": "Duração",
@@ -462,5 +462,9 @@
"NextExecution": "Próxima Execução",
"Started": "Iniciado",
"ApplicationLongTermStatusCheckAllClientMessage": "Todos os aplicativos estão indisponíveis devido a falhas por mais de 6 horas",
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicativos indisponíveis devido a falhas por mais de 6 horas: {0}"
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicativos indisponíveis devido a falhas por mais de 6 horas: {0}",
"AreYouSureYouWantToDeleteCategory": "Tem certeza de que deseja excluir a categoria mapeada?",
"DeleteClientCategory": "Excluir Categoria de Cliente de Download",
"DownloadClientCategory": "Categoria de Download do Cliente",
"MappedCategories": "Categorias Mapeadas"
}

View File

@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Parser
if (DateTimeRoutines.TryParseDateOrTime(
str, dtFormat, out DateTimeRoutines.ParsedDateTime dt))
{
return dt.DateTime;
return dt.DateTime.ToUniversalTime();
}
throw new InvalidDateException($"FromFuzzyTime parsing failed for string {str}");

View File

@@ -0,0 +1,16 @@
using System.Collections;
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.ThingiProvider.Events
{
public class ProviderBulkDeletedEvent<TProvider> : IEvent
{
public IEnumerable<int> ProviderIds { get; private set; }
public ProviderBulkDeletedEvent(IEnumerable<int> ids)
{
ProviderIds = ids;
}
}
}

View File

@@ -14,6 +14,7 @@ namespace NzbDrone.Core.ThingiProvider
void Update(TProviderDefinition definition);
void Update(IEnumerable<TProviderDefinition> definitions);
void Delete(int id);
void Delete(IEnumerable<int> ids);
IEnumerable<TProviderDefinition> GetDefaultDefinitions();
IEnumerable<TProviderDefinition> GetPresetDefinitions(TProviderDefinition providerDefinition);
void SetProviderCharacteristics(TProviderDefinition definition);

View File

@@ -121,6 +121,13 @@ namespace NzbDrone.Core.ThingiProvider
_eventAggregator.PublishEvent(new ProviderDeletedEvent<TProvider>(id));
}
public void Delete(IEnumerable<int> ids)
{
_providerRepository.DeleteMany(ids);
_eventAggregator.PublishEvent(new ProviderBulkDeletedEvent<TProvider>(ids));
}
public TProvider GetInstance(TProviderDefinition definition)
{
var type = GetImplementation(definition);

View File

@@ -11,13 +11,13 @@ namespace Prowlarr.Api.V1.Indexers
[V1ApiController("indexer/editor")]
public class IndexerEditorController : Controller
{
private readonly IIndexerFactory _indexerService;
private readonly IIndexerFactory _indexerFactory;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IndexerResourceMapper _resourceMapper;
public IndexerEditorController(IIndexerFactory indexerService, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
public IndexerEditorController(IIndexerFactory indexerFactory, IManageCommandQueue commandQueueManager, IndexerResourceMapper resourceMapper)
{
_indexerService = indexerService;
_indexerFactory = indexerFactory;
_commandQueueManager = commandQueueManager;
_resourceMapper = resourceMapper;
}
@@ -25,7 +25,7 @@ namespace Prowlarr.Api.V1.Indexers
[HttpPut]
public IActionResult SaveAll(IndexerEditorResource resource)
{
var indexersToUpdate = _indexerService.AllProviders(false).Select(x => (IndexerDefinition)x.Definition).Where(d => resource.IndexerIds.Contains(d.Id));
var indexersToUpdate = _indexerFactory.AllProviders(false).Select(x => (IndexerDefinition)x.Definition).Where(d => resource.IndexerIds.Contains(d.Id));
foreach (var indexer in indexersToUpdate)
{
@@ -59,13 +59,13 @@ namespace Prowlarr.Api.V1.Indexers
}
}
_indexerService.Update(indexersToUpdate);
_indexerFactory.Update(indexersToUpdate);
var indexers = _indexerService.All();
var indexers = _indexerFactory.All();
foreach (var definition in indexers)
{
_indexerService.SetProviderCharacteristics(definition);
_indexerFactory.SetProviderCharacteristics(definition);
}
return Accepted(_resourceMapper.ToResource(indexers));
@@ -74,7 +74,7 @@ namespace Prowlarr.Api.V1.Indexers
[HttpDelete]
public object DeleteIndexers([FromBody] IndexerEditorResource resource)
{
_indexerService.DeleteIndexers(resource.IndexerIds);
_indexerFactory.Delete(resource.IndexerIds);
return new { };
}