mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-20 13:33:34 -07:00
New: Add tag support to indexers
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
77041a5401
commit
a26cbdf61f
29 changed files with 351 additions and 578 deletions
|
@ -43,6 +43,7 @@ function EditIndexerModalContent(props) {
|
|||
enableInteractiveSearch,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
tags,
|
||||
fields,
|
||||
priority,
|
||||
protocol,
|
||||
|
@ -168,18 +169,30 @@ function EditIndexerModalContent(props) {
|
|||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>DownloadClient</FormLabel>
|
||||
<FormLabel>{translate('DownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
|
||||
name="downloadClientId"
|
||||
helpText={'Specify which download client is used for grabs from this indexer'}
|
||||
helpText={translate('IndexerDownloadClientHelpText')}
|
||||
{...downloadClientId}
|
||||
includeAny={true}
|
||||
protocol={protocol.value}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
|
|
@ -4,6 +4,7 @@ import Card from 'Components/Card';
|
|||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
||||
|
@ -68,6 +69,8 @@ class Indexer extends Component {
|
|||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
tags,
|
||||
tagList,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
priority,
|
||||
|
@ -133,6 +136,11 @@ class Indexer extends Component {
|
|||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditIndexerModalOpen}
|
||||
|
@ -161,6 +169,8 @@ Indexer.propTypes = {
|
|||
enableRss: PropTypes.bool.isRequired,
|
||||
enableAutomaticSearch: PropTypes.bool.isRequired,
|
||||
enableInteractiveSearch: PropTypes.bool.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
supportsRss: PropTypes.bool.isRequired,
|
||||
supportsSearch: PropTypes.bool.isRequired,
|
||||
showPriority: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -54,6 +54,7 @@ class Indexers extends Component {
|
|||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
dispatchCloneIndexer,
|
||||
onConfirmDeleteIndexer,
|
||||
...otherProps
|
||||
|
@ -79,6 +80,7 @@ class Indexers extends Component {
|
|||
<Indexer
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
showPriority={showPriority}
|
||||
onCloneIndexerPress={this.onCloneIndexerPress}
|
||||
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
|
||||
|
@ -119,6 +121,7 @@ Indexers.propTypes = {
|
|||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchCloneIndexer: PropTypes.func.isRequired,
|
||||
onConfirmDeleteIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -4,13 +4,20 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import Indexers from './Indexers';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexers', sortByName),
|
||||
(indexers) => indexers
|
||||
createTagsSelector(),
|
||||
(indexers, tagList) => {
|
||||
return {
|
||||
...indexers,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ function TagDetailsModalContent(props) {
|
|||
importLists,
|
||||
notifications,
|
||||
releaseProfiles,
|
||||
indexers,
|
||||
onModalClose,
|
||||
onDeleteTagPress
|
||||
} = props;
|
||||
|
@ -41,7 +42,7 @@ function TagDetailsModalContent(props) {
|
|||
}
|
||||
|
||||
{
|
||||
!!artist.length &&
|
||||
artist.length ?
|
||||
<FieldSet legend={translate('Artists')}>
|
||||
{
|
||||
artist.map((item) => {
|
||||
|
@ -52,11 +53,12 @@ function TagDetailsModalContent(props) {
|
|||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!delayProfiles.length &&
|
||||
delayProfiles.length ?
|
||||
<FieldSet legend={translate('DelayProfile')}>
|
||||
{
|
||||
delayProfiles.map((item) => {
|
||||
|
@ -81,11 +83,12 @@ function TagDetailsModalContent(props) {
|
|||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!notifications.length &&
|
||||
notifications.length ?
|
||||
<FieldSet legend={translate('Connections')}>
|
||||
{
|
||||
notifications.map((item) => {
|
||||
|
@ -96,11 +99,12 @@ function TagDetailsModalContent(props) {
|
|||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!importLists.length &&
|
||||
importLists.length ?
|
||||
<FieldSet legend={translate('ImportLists')}>
|
||||
{
|
||||
importLists.map((item) => {
|
||||
|
@ -111,11 +115,12 @@ function TagDetailsModalContent(props) {
|
|||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!releaseProfiles.length &&
|
||||
releaseProfiles.length ?
|
||||
<FieldSet legend={translate('ReleaseProfiles')}>
|
||||
{
|
||||
releaseProfiles.map((item) => {
|
||||
|
@ -157,7 +162,24 @@ function TagDetailsModalContent(props) {
|
|||
);
|
||||
})
|
||||
}
|
||||
</FieldSet>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
indexers.length ?
|
||||
<FieldSet legend="Indexers">
|
||||
{
|
||||
indexers.map((item) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
|
@ -192,6 +214,7 @@ TagDetailsModalContent.propTypes = {
|
|||
importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteTagPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -69,6 +69,14 @@ function createMatchingReleaseProfilesSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
function createMatchingIndexersSelector() {
|
||||
return createSelector(
|
||||
(state, { indexerIds }) => indexerIds,
|
||||
(state) => state.settings.indexers.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMatchingArtistSelector(),
|
||||
|
@ -76,13 +84,15 @@ function createMapStateToProps() {
|
|||
createMatchingImportListsSelector(),
|
||||
createMatchingNotificationsSelector(),
|
||||
createMatchingReleaseProfilesSelector(),
|
||||
(artist, delayProfiles, importLists, notifications, releaseProfiles) => {
|
||||
createMatchingIndexersSelector(),
|
||||
(artist, delayProfiles, importLists, notifications, releaseProfiles, indexers) => {
|
||||
return {
|
||||
artist,
|
||||
delayProfiles,
|
||||
importLists,
|
||||
notifications,
|
||||
releaseProfiles
|
||||
releaseProfiles,
|
||||
indexers
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -57,6 +57,7 @@ class Tag extends Component {
|
|||
importListIds,
|
||||
notificationIds,
|
||||
restrictionIds,
|
||||
indexerIds,
|
||||
artistIds
|
||||
} = this.props;
|
||||
|
||||
|
@ -70,6 +71,7 @@ class Tag extends Component {
|
|||
importListIds.length ||
|
||||
notificationIds.length ||
|
||||
restrictionIds.length ||
|
||||
indexerIds.length ||
|
||||
artistIds.length
|
||||
);
|
||||
|
||||
|
@ -87,38 +89,50 @@ class Tag extends Component {
|
|||
isTagUsed &&
|
||||
<div>
|
||||
{
|
||||
!!artistIds.length &&
|
||||
artistIds.length ?
|
||||
<div>
|
||||
{artistIds.length} artists
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!delayProfileIds.length &&
|
||||
delayProfileIds.length ?
|
||||
<div>
|
||||
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!importListIds.length &&
|
||||
importListIds.length ?
|
||||
<div>
|
||||
{importListIds.length} import list{importListIds.length > 1 && 's'}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!notificationIds.length &&
|
||||
notificationIds.length ?
|
||||
<div>
|
||||
{notificationIds.length} connection{notificationIds.length > 1 && 's'}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!restrictionIds.length &&
|
||||
restrictionIds.length ?
|
||||
<div>
|
||||
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
{
|
||||
indexerIds.length ?
|
||||
<div>
|
||||
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -138,6 +152,7 @@ class Tag extends Component {
|
|||
importListIds={importListIds}
|
||||
notificationIds={notificationIds}
|
||||
restrictionIds={restrictionIds}
|
||||
indexerIds={indexerIds}
|
||||
isOpen={isDetailsModalOpen}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
onDeleteTagPress={this.onDeleteTagPress}
|
||||
|
@ -164,6 +179,7 @@ Tag.propTypes = {
|
|||
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -173,6 +189,7 @@ Tag.defaultProps = {
|
|||
importListIds: [],
|
||||
notificationIds: [],
|
||||
restrictionIds: [],
|
||||
indexerIds: [],
|
||||
artistIds: []
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, fetchImportLists, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchDelayProfiles, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
|
@ -29,7 +29,8 @@ const mapDispatchToProps = {
|
|||
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
||||
dispatchFetchImportLists: fetchImportLists,
|
||||
dispatchFetchNotifications: fetchNotifications,
|
||||
dispatchFetchReleaseProfiles: fetchReleaseProfiles
|
||||
dispatchFetchReleaseProfiles: fetchReleaseProfiles,
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
class MetadatasConnector extends Component {
|
||||
|
@ -43,7 +44,8 @@ class MetadatasConnector extends Component {
|
|||
dispatchFetchDelayProfiles,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchNotifications,
|
||||
dispatchFetchReleaseProfiles
|
||||
dispatchFetchReleaseProfiles,
|
||||
dispatchFetchIndexers
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTagDetails();
|
||||
|
@ -51,6 +53,7 @@ class MetadatasConnector extends Component {
|
|||
dispatchFetchImportLists();
|
||||
dispatchFetchNotifications();
|
||||
dispatchFetchReleaseProfiles();
|
||||
dispatchFetchIndexers();
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -70,7 +73,8 @@ MetadatasConnector.propTypes = {
|
|||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||
dispatchFetchReleaseProfiles: PropTypes.func.isRequired
|
||||
dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace Lidarr.Api.V1.Tags
|
|||
public List<int> ImportListIds { get; set; }
|
||||
public List<int> NotificationIds { get; set; }
|
||||
public List<int> RestrictionIds { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public List<int> ArtistIds { get; set; }
|
||||
}
|
||||
|
||||
|
@ -32,6 +33,7 @@ namespace Lidarr.Api.V1.Tags
|
|||
ImportListIds = model.ImportListIds,
|
||||
NotificationIds = model.NotificationIds,
|
||||
RestrictionIds = model.RestrictionIds,
|
||||
IndexerIds = model.IndexerIds,
|
||||
ArtistIds = model.ArtistIds
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
{
|
||||
[TestFixture]
|
||||
public class IndexerTagSpecificationFixture : CoreTest<IndexerTagSpecification>
|
||||
{
|
||||
private IndexerTagSpecification _specification;
|
||||
|
||||
private RemoteAlbum _parseResultMulti;
|
||||
private IndexerDefinition _fakeIndexerDefinition;
|
||||
private Artist _fakeArtist;
|
||||
private Album _firstAlbum;
|
||||
private Album _secondAlbum;
|
||||
private ReleaseInfo _fakeRelease;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_fakeIndexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Tags = new HashSet<int>()
|
||||
};
|
||||
|
||||
Mocker
|
||||
.GetMock<IIndexerFactory>()
|
||||
.Setup(m => m.Get(It.IsAny<int>()))
|
||||
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
|
||||
|
||||
Mocker
|
||||
.GetMock<IIndexerFactory>()
|
||||
.Setup(m => m.Get(1))
|
||||
.Returns(_fakeIndexerDefinition);
|
||||
|
||||
_specification = Mocker.Resolve<IndexerTagSpecification>();
|
||||
|
||||
_fakeArtist = Builder<Artist>.CreateNew()
|
||||
.With(c => c.Monitored = true)
|
||||
.With(c => c.Tags = new HashSet<int>())
|
||||
.Build();
|
||||
|
||||
_fakeRelease = new ReleaseInfo
|
||||
{
|
||||
IndexerId = 1
|
||||
};
|
||||
|
||||
_firstAlbum = new Album { Monitored = true };
|
||||
_secondAlbum = new Album { Monitored = true };
|
||||
|
||||
var doubleEpisodeList = new List<Album> { _firstAlbum, _secondAlbum };
|
||||
|
||||
_parseResultMulti = new RemoteAlbum
|
||||
{
|
||||
Artist = _fakeArtist,
|
||||
Albums = doubleEpisodeList,
|
||||
Release = _fakeRelease
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void indexer_and_series_without_tags_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int>();
|
||||
_fakeArtist.Tags = new HashSet<int>();
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void indexer_with_tags_series_without_tags_should_return_false()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 123 };
|
||||
_fakeArtist.Tags = new HashSet<int>();
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void indexer_without_tags_series_with_tags_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int>();
|
||||
_fakeArtist.Tags = new HashSet<int> { 123 };
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void indexer_with_tags_series_with_matching_tags_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 123, 456 };
|
||||
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void indexer_with_tags_series_with_different_tags_should_return_false()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void release_without_indexerid_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
|
||||
_fakeRelease.IndexerId = 0;
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void release_with_invalid_indexerid_should_return_true()
|
||||
{
|
||||
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
|
||||
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
|
||||
_fakeRelease.IndexerId = 2;
|
||||
|
||||
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.FileList;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Indexers.Omgwtfnzbs;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||
_indexers = new List<IIndexer>();
|
||||
|
||||
_indexers.Add(Mocker.Resolve<Newznab>());
|
||||
_indexers.Add(Mocker.Resolve<Omgwtfnzbs>());
|
||||
_indexers.Add(Mocker.Resolve<FileList>());
|
||||
|
||||
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Omgwtfnzbs;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OmgwtfnzbsFixture : CoreTest<Omgwtfnzbs>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "Omgwtfnzbs",
|
||||
Settings = new OmgwtfnzbsSettings()
|
||||
{
|
||||
ApiKey = "xxx",
|
||||
Username = "me@my.domain"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_recent_feed_from_omgwtfnzbs()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
|
||||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
|
||||
releaseInfo.DownloadUrl.Should().Be("http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone");
|
||||
releaseInfo.InfoUrl.Should().Be("http://omgwtfnzbs.org/details.php?id=OAl4g");
|
||||
releaseInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2012/12/17 23:30:13"));
|
||||
releaseInfo.Size.Should().Be(236822906);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Waffles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.WafflesTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class WafflesFixture : CoreTest<Waffles>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "Waffles",
|
||||
Settings = new WafflesSettings()
|
||||
{
|
||||
UserId = "xxx",
|
||||
RssPasskey = "123456789"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_recent_feed_from_waffles()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Waffles/waffles.xml");
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||
|
||||
var releases = Subject.FetchRecent();
|
||||
|
||||
releases.Should().HaveCount(15);
|
||||
|
||||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Coldplay - Kaleidoscope EP (FLAC HD) [2017-Web-FLAC-Lossless]");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
releaseInfo.DownloadUrl.Should().Be("https://waffles.ch/download.php/xxx/1166992/" +
|
||||
"Coldplay%20-%20Kaleidoscope%20EP%20%28FLAC%20HD%29%20%5B2017-Web-FLAC-Lossless%5D.torrent?passkey=123456789&uid=xxx&rss=1");
|
||||
releaseInfo.InfoUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1");
|
||||
releaseInfo.CommentUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1");
|
||||
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2017-07-16 09:51:54"));
|
||||
releaseInfo.Size.Should().Be(552668227);
|
||||
}
|
||||
}
|
||||
}
|
17
src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs
Normal file
17
src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(059)]
|
||||
public class add_indexer_tags : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Omgwtfnzbs'");
|
||||
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Waffles'");
|
||||
|
||||
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,8 +68,7 @@ namespace NzbDrone.Core.Datastore
|
|||
.Ignore(i => i.Enable)
|
||||
.Ignore(i => i.Protocol)
|
||||
.Ignore(i => i.SupportsRss)
|
||||
.Ignore(i => i.SupportsSearch)
|
||||
.Ignore(d => d.Tags);
|
||||
.Ignore(i => i.SupportsSearch);
|
||||
|
||||
Mapper.Entity<ImportListDefinition>("ImportLists").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
{
|
||||
public class IndexerTagSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public IndexerTagSpecification(Logger logger, IIndexerFactory indexerFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (subject.Release == null || subject.Artist?.Tags == null || subject.Release.IndexerId == 0)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
IndexerDefinition indexer;
|
||||
try
|
||||
{
|
||||
indexer = _indexerFactory.Get(subject.Release.IndexerId);
|
||||
}
|
||||
catch (ModelNotFoundException)
|
||||
{
|
||||
_logger.Debug("Indexer with id {0} does not exist, skipping indexer tags check", subject.Release.IndexerId);
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
// If indexer has tags, check that at least one of them is present on the series
|
||||
var indexerTags = indexer.Tags;
|
||||
|
||||
if (indexerTags.Any() && indexerTags.Intersect(subject.Artist.Tags).Empty())
|
||||
{
|
||||
_logger.Debug("Indexer {0} has tags. None of these are present on artist {1}. Rejecting", subject.Release.Indexer, subject.Artist);
|
||||
|
||||
return Decision.Reject("Artist tags do not match any of the indexer tags");
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -125,6 +125,9 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
_indexerFactory.InteractiveSearchEnabled() :
|
||||
_indexerFactory.AutomaticSearchEnabled();
|
||||
|
||||
// Filter indexers to untagged indexers and indexers with intersecting tags
|
||||
indexers = indexers.Where(i => i.Definition.Tags.Empty() || i.Definition.Tags.Intersect(criteriaBase.Artist.Tags).Any()).ToList();
|
||||
|
||||
var reports = new List<ReleaseInfo>();
|
||||
|
||||
_logger.ProgressInfo("Searching indexers for {0}. {1} active indexers", criteriaBase, indexers.Count);
|
||||
|
|
|
@ -47,7 +47,6 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws"));
|
||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
||||
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
|
||||
yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me"));
|
||||
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
|
||||
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
|
||||
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
||||
{
|
||||
public class Omgwtfnzbs : HttpIndexerBase<OmgwtfnzbsSettings>
|
||||
{
|
||||
public override string Name => "omgwtfnzbs";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
|
||||
public Omgwtfnzbs(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, indexerStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new OmgwtfnzbsRequestGenerator() { Settings = Settings };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new OmgwtfnzbsRssParser();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
||||
{
|
||||
public class OmgwtfnzbsRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public string BaseUrl { get; set; }
|
||||
public OmgwtfnzbsSettings Settings { get; set; }
|
||||
|
||||
public OmgwtfnzbsRequestGenerator()
|
||||
{
|
||||
BaseUrl = "https://rss.omgwtfnzbs.me/rss-download.php";
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(null));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}+{1}",
|
||||
searchCriteria.ArtistQuery,
|
||||
searchCriteria.AlbumQuery)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}",
|
||||
searchCriteria.ArtistQuery)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string query)
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
|
||||
// Category 22 is Music-FLAC, category 7 is Music-MP3
|
||||
url.AppendFormat("{0}?catid=22,7&user={1}&api={2}&eng=1&delay={3}", BaseUrl, Settings.Username, Settings.ApiKey, Settings.Delay);
|
||||
|
||||
if (query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
url = url.Replace("rss-download.php", "rss-search.php");
|
||||
url.AppendFormat("&search={0}", query);
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(url.ToString(), HttpAccept.Rss);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
||||
{
|
||||
public class OmgwtfnzbsRssParser : RssParser
|
||||
{
|
||||
public OmgwtfnzbsRssParser()
|
||||
{
|
||||
UseEnclosureUrl = true;
|
||||
UseEnclosureLength = true;
|
||||
}
|
||||
|
||||
protected override bool PreProcess(IndexerResponse indexerResponse)
|
||||
{
|
||||
var xdoc = LoadXmlDocument(indexerResponse);
|
||||
var notice = xdoc.Descendants("notice").FirstOrDefault();
|
||||
|
||||
if (notice == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!notice.Value.ContainsIgnoreCase("api"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ApiKeyException(notice.Value);
|
||||
}
|
||||
|
||||
protected override string GetInfoUrl(XElement item)
|
||||
{
|
||||
//Todo: Me thinks I need to parse details to get this...
|
||||
var match = Regex.Match(item.Description(),
|
||||
@"(?:\<b\>View NZB\:\<\/b\>\s\<a\shref\=\"")(?<URL>.+)(?:\""\starget)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups["URL"].Value;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
||||
{
|
||||
public class OmgwtfnzbsSettingsValidator : AbstractValidator<OmgwtfnzbsSettings>
|
||||
{
|
||||
public OmgwtfnzbsSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
RuleFor(c => c.Delay).GreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class OmgwtfnzbsSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly OmgwtfnzbsSettingsValidator Validator = new OmgwtfnzbsSettingsValidator();
|
||||
|
||||
public OmgwtfnzbsSettings()
|
||||
{
|
||||
Delay = 30;
|
||||
}
|
||||
|
||||
// Unused since Omg has a hardcoded url.
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Delay", HelpText = "Time in minutes to delay new nzbs before they appear on the RSS feed", Advanced = true)]
|
||||
public int Delay { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
|
||||
public int? EarlyReleaseLimit { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Waffles
|
||||
{
|
||||
public class Waffles : HttpIndexerBase<WafflesSettings>
|
||||
{
|
||||
public override string Name => "Waffles";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override int PageSize => 15;
|
||||
|
||||
public Waffles(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, indexerStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new WafflesRequestGenerator() { Settings = Settings };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new WafflesRssParser() { ParseSizeInDescription = true, ParseSeedersInDescription = true };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Waffles
|
||||
{
|
||||
public class WafflesRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public WafflesSettings Settings { get; set; }
|
||||
public int MaxPages { get; set; }
|
||||
|
||||
public WafflesRequestGenerator()
|
||||
{
|
||||
MaxPages = 5;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, null));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0} album:{1}", searchCriteria.ArtistQuery, searchCriteria.AlbumQuery)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0}", searchCriteria.ArtistQuery)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, string query)
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
|
||||
url.AppendFormat("{0}/browse.php?rss=1&c0=1&uid={1}&passkey={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey);
|
||||
|
||||
if (query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
url.AppendFormat(query);
|
||||
}
|
||||
|
||||
for (var page = 0; page < maxPages; page++)
|
||||
{
|
||||
yield return new IndexerRequest(string.Format("{0}&p={1}", url, page), HttpAccept.Rss);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Waffles
|
||||
{
|
||||
public class WafflesRssParser : TorrentRssParser
|
||||
{
|
||||
public const string ns = "{http://purl.org/rss/1.0/}";
|
||||
public const string dc = "{http://purl.org/dc/elements/1.1/}";
|
||||
|
||||
protected override bool PreProcess(IndexerResponse indexerResponse)
|
||||
{
|
||||
var xdoc = LoadXmlDocument(indexerResponse);
|
||||
var error = xdoc.Descendants("error").FirstOrDefault();
|
||||
|
||||
if (error == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var code = Convert.ToInt32(error.Attribute("code").Value);
|
||||
var errorMessage = error.Attribute("description").Value;
|
||||
|
||||
if (code >= 100 && code <= 199)
|
||||
{
|
||||
throw new ApiKeyException("Invalid Pass key");
|
||||
}
|
||||
|
||||
if (!indexerResponse.Request.Url.FullUri.Contains("passkey=") && errorMessage == "Missing parameter")
|
||||
{
|
||||
throw new ApiKeyException("Indexer requires an Pass key");
|
||||
}
|
||||
|
||||
if (errorMessage == "Request limit reached")
|
||||
{
|
||||
throw new RequestLimitReachedException("API limit reached");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, errorMessage);
|
||||
}
|
||||
|
||||
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
|
||||
{
|
||||
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
|
||||
|
||||
return torrentInfo;
|
||||
}
|
||||
|
||||
protected override string GetInfoUrl(XElement item)
|
||||
{
|
||||
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
|
||||
}
|
||||
|
||||
protected override string GetCommentUrl(XElement item)
|
||||
{
|
||||
return ParseUrl(item.TryGetValue("comments"));
|
||||
}
|
||||
|
||||
private static readonly Regex ParseSizeRegex = new Regex(@"(?:Size: )(?<value>\d+)<",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
protected override long GetSize(XElement item)
|
||||
{
|
||||
var match = ParseSizeRegex.Matches(item.Element("description").Value);
|
||||
|
||||
if (match.Count != 0)
|
||||
{
|
||||
var value = decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), CultureInfo.InvariantCulture);
|
||||
return (long)value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override DateTime GetPublishDate(XElement item)
|
||||
{
|
||||
var dateString = item.TryGetValue(dc + "date");
|
||||
|
||||
if (dateString.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date.");
|
||||
}
|
||||
|
||||
return XElementExtensions.ParseDate(dateString);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Waffles
|
||||
{
|
||||
public class WafflesSettingsValidator : AbstractValidator<WafflesSettings>
|
||||
{
|
||||
public WafflesSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.UserId).NotEmpty();
|
||||
RuleFor(c => c.RssPasskey).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class WafflesSettings : ITorrentIndexerSettings
|
||||
{
|
||||
private static readonly WafflesSettingsValidator Validator = new WafflesSettingsValidator();
|
||||
|
||||
public WafflesSettings()
|
||||
{
|
||||
BaseUrl = "https://www.waffles.ch";
|
||||
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Website URL")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "UserId", Privacy = PrivacyLevel.UserName)]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "RSS Passkey", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string RssPasskey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
|
||||
public int? EarlyReleaseLimit { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -269,6 +269,7 @@
|
|||
"IncludeUnknownArtistItemsHelpText": "Show items without a artist in the queue, this could include removed artists, movies or anything else in Lidarr's category",
|
||||
"IncludeUnmonitored": "Include Unmonitored",
|
||||
"Indexer": "Indexer",
|
||||
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
|
||||
"IndexerIdHelpText": "Specify what indexer the profile applies to",
|
||||
"IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed",
|
||||
"IndexerIdvalue0IncludeInPreferredWordsRenamingFormat": "Include in {Preferred Words} renaming format",
|
||||
|
@ -276,6 +277,7 @@
|
|||
"IndexerPriority": "Indexer Priority",
|
||||
"Indexers": "Indexers",
|
||||
"IndexerSettings": "Indexer Settings",
|
||||
"IndexerTagHelpText": "Only use this indexer for artist with at least one matching tag. Leave blank to use with all artists.",
|
||||
"InstanceName": "Instance Name",
|
||||
"InstanceNameHelpText": "Instance name in tab and for Syslog app name",
|
||||
"InteractiveSearch": "Interactive Search",
|
||||
|
@ -321,8 +323,8 @@
|
|||
"ManualImport": "Manual Import",
|
||||
"MarkAsFailed": "Mark as Failed",
|
||||
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
|
||||
"MassAlbumsSearchWarning": "Are you sure you want to search for all '{0}' missing albums?",
|
||||
"MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?",
|
||||
"MassAlbumsSearchWarning": "Are you sure you want to search for all '{0}' missing albums?",
|
||||
"MaximumLimits": "Maximum Limits",
|
||||
"MaximumSize": "Maximum Size",
|
||||
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",
|
||||
|
@ -550,9 +552,9 @@
|
|||
"SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.",
|
||||
"Settings": "Settings",
|
||||
"ShortDateFormat": "Short Date Format",
|
||||
"ShouldMonitorHelpText": "Monitor artists and albums added from this list",
|
||||
"ShouldMonitorExisting": "Monitor existing albums",
|
||||
"ShouldMonitorExistingHelpText": "Automatically monitor albums on this list which are already in Lidarr",
|
||||
"ShouldMonitorHelpText": "Monitor artists and albums added from this list",
|
||||
"ShouldSearch": "Search for New Items",
|
||||
"ShouldSearchHelpText": "Search indexers for newly added items. Use with caution for large lists.",
|
||||
"ShowAlbumCount": "Show Album Count",
|
||||
|
|
|
@ -13,12 +13,13 @@ namespace NzbDrone.Core.Tags
|
|||
public List<int> DelayProfileIds { get; set; }
|
||||
public List<int> ImportListIds { get; set; }
|
||||
public List<int> RootFolderIds { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
|
||||
public bool InUse
|
||||
{
|
||||
get
|
||||
{
|
||||
return ArtistIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any();
|
||||
return ArtistIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any() || IndexerIds.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Notifications;
|
||||
|
@ -33,6 +34,7 @@ namespace NzbDrone.Core.Tags
|
|||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
private readonly IIndexerFactory _indexerService;
|
||||
|
||||
public TagService(ITagRepository repo,
|
||||
IEventAggregator eventAggregator,
|
||||
|
@ -41,7 +43,8 @@ namespace NzbDrone.Core.Tags
|
|||
INotificationFactory notificationFactory,
|
||||
IReleaseProfileService releaseProfileService,
|
||||
IArtistService artistService,
|
||||
IRootFolderService rootFolderService)
|
||||
IRootFolderService rootFolderService,
|
||||
IIndexerFactory indexerService)
|
||||
{
|
||||
_repo = repo;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
@ -51,6 +54,7 @@ namespace NzbDrone.Core.Tags
|
|||
_releaseProfileService = releaseProfileService;
|
||||
_artistService = artistService;
|
||||
_rootFolderService = rootFolderService;
|
||||
_indexerService = indexerService;
|
||||
}
|
||||
|
||||
public Tag GetTag(int tagId)
|
||||
|
@ -79,6 +83,7 @@ namespace NzbDrone.Core.Tags
|
|||
var restrictions = _releaseProfileService.AllForTag(tagId);
|
||||
var artist = _artistService.AllForTag(tagId);
|
||||
var rootFolders = _rootFolderService.AllForTag(tagId);
|
||||
var indexers = _indexerService.AllForTag(tagId);
|
||||
|
||||
return new TagDetails
|
||||
{
|
||||
|
@ -89,7 +94,8 @@ namespace NzbDrone.Core.Tags
|
|||
NotificationIds = notifications.Select(c => c.Id).ToList(),
|
||||
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
|
||||
ArtistIds = artist.Select(c => c.Id).ToList(),
|
||||
RootFolderIds = rootFolders.Select(c => c.Id).ToList()
|
||||
RootFolderIds = rootFolders.Select(c => c.Id).ToList(),
|
||||
IndexerIds = indexers.Select(c => c.Id).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -102,6 +108,7 @@ namespace NzbDrone.Core.Tags
|
|||
var restrictions = _releaseProfileService.All();
|
||||
var artists = _artistService.GetAllArtists();
|
||||
var rootFolders = _rootFolderService.All();
|
||||
var indexers = _indexerService.All();
|
||||
|
||||
var details = new List<TagDetails>();
|
||||
|
||||
|
@ -116,7 +123,8 @@ namespace NzbDrone.Core.Tags
|
|||
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
ArtistIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList()
|
||||
RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue