mirror of
https://github.com/lidarr/Lidarr
synced 2025-03-15 16:26:34 +00:00
New: Plugin support
This commit is contained in:
parent
e6388cab94
commit
5d441f4f81
171 changed files with 3275 additions and 837 deletions
frontend/src
Activity
App
Commands
Components
Filter/Builder
FilterBuilderModalContent.jsFilterBuilderModalContentConnector.jsFilterBuilderRow.jsProtocolFilterBuilderRowValue.jsProtocolFilterBuilderRowValueConnector.js
Page/Sidebar
SignalRConnector.jsTagList.jsHelpers
InteractiveSearch
Settings
DownloadClients/DownloadClients
Indexers/Indexers
Profiles/Delay
DelayProfile.cssDelayProfile.css.d.tsDelayProfile.jsDelayProfileItem.jsDelayProfiles.cssDelayProfiles.css.d.tsDelayProfiles.jsDownloadProtocolItem.cssDownloadProtocolItem.css.d.tsDownloadProtocolItem.jsDownloadProtocolItemDragPreview.cssDownloadProtocolItemDragPreview.css.d.tsDownloadProtocolItemDragPreview.jsDownloadProtocolItemDragSource.cssDownloadProtocolItemDragSource.css.d.tsDownloadProtocolItemDragSource.jsDownloadProtocolItems.cssDownloadProtocolItems.css.d.tsDownloadProtocolItems.jsEditDelayProfileModalContent.jsEditDelayProfileModalContentConnector.js
Tags/Details
Store/Actions
System/Plugins
src
Lidarr.Api.V1
Blocklist
Commands
DownloadClient
Indexers
Profiles/Delay
DelayProfileController.csDelayProfileProtocolItemResource.csDelayProfileResource.csDelayProfileSchemaController.cs
ProviderResource.csQueue
System/Plugins
NzbDrone.Common.Test
NzbDrone.Common
Composition
Extensions
Reflection
NzbDrone.Core.Test
Blocklisting
DecisionEngineTests
AlreadyImportedSpecificationFixture.csMinimumAgeSpecificationFixture.csPrioritizeDownloadDecisionFixture.csProtocolSpecificationFixture.csRawDiskSpecificationFixture.csRetentionSpecificationFixture.cs
RssSync
Download
DownloadApprovedReportsTests
DownloadClientProviderFixture.csDownloadServiceFixture.csTrackedDownloads
HealthCheck/Checks
HistoryTests
IndexerTests
FileListTests
GazelleTests
HeadphonesTests
IPTorrentsTests
IntegrationTests
NewznabTests
NyaaTests
RedactedTests
SeedConfigProviderFixture.csTestIndexer.cs
|
@ -46,7 +46,7 @@ class BlocklistDetailsModal extends Component {
|
|||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
data={protocol.replace('DownloadProtocol', '')}
|
||||
/>
|
||||
|
||||
{
|
||||
|
|
|
@ -4,7 +4,8 @@ import Label from 'Components/Label';
|
|||
import styles from './ProtocolLabel.css';
|
||||
|
||||
function ProtocolLabel({ protocol }) {
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
const strippedName = protocol.replace('DownloadProtocol', '').toLowerCase();
|
||||
const protocolName = strippedName === 'usenet' ? 'nzb' : strippedName;
|
||||
|
||||
return (
|
||||
<Label className={styles[protocol]}>
|
||||
|
|
|
@ -27,6 +27,7 @@ import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
|||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import PluginsConnector from 'System/Plugins/PluginsConnector';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
|
@ -251,6 +252,11 @@ function AppRoutes(props) {
|
|||
component={Updates}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/plugins"
|
||||
component={PluginsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/events"
|
||||
component={LogsTableConnector}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const DELETE_LOG_FILES = 'DeleteLogFiles';
|
|||
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
|
||||
export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan';
|
||||
export const ALBUM_SEARCH = 'AlbumSearch';
|
||||
export const INSTALL_PLUGIN = 'InstallPlugin';
|
||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||
export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch';
|
||||
export const MOVE_ARTIST = 'MoveArtist';
|
||||
|
@ -22,3 +23,4 @@ export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
|
|||
export const RSS_SYNC = 'RssSync';
|
||||
export const SEASON_SEARCH = 'AlbumSearch';
|
||||
export const ARTIST_SEARCH = 'ArtistSearch';
|
||||
export const UNINSTALL_PLUGIN = 'UninstallPlugin';
|
||||
|
|
|
@ -37,6 +37,13 @@ class FilterBuilderModalContent extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchDownloadClients();
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
|
@ -222,9 +229,12 @@ FilterBuilderModalContent.propTypes = {
|
|||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
dispatchFetchDownloadClients: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteCustomFilter, saveCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import { fetchDownloadClients, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -9,7 +10,11 @@ function createMapStateToProps() {
|
|||
(state, { id }) => id,
|
||||
(state) => state.customFilters.isSaving,
|
||||
(state) => state.customFilters.saveError,
|
||||
(customFilters, id, isSaving, saveError) => {
|
||||
(state) => state.settings.downloadClients.isPopulated,
|
||||
(state) => state.settings.indexers.isPopulated,
|
||||
(customFilters, id, isSaving, saveError, downloadClientsPopulated, indexersPopulated) => {
|
||||
const isPopulated = downloadClientsPopulated && indexersPopulated;
|
||||
|
||||
if (id) {
|
||||
const customFilter = customFilters.find((c) => c.id === id);
|
||||
|
||||
|
@ -19,7 +24,8 @@ function createMapStateToProps() {
|
|||
filters: customFilter.filters,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
saveError,
|
||||
isPopulated
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -28,7 +34,8 @@ function createMapStateToProps() {
|
|||
filters: [],
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
saveError,
|
||||
isPopulated
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -36,7 +43,9 @@ function createMapStateToProps() {
|
|||
|
||||
const mapDispatchToProps = {
|
||||
onSaveCustomFilterPress: saveCustomFilter,
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter,
|
||||
dispatchFetchDownloadClients: fetchDownloadClients,
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);
|
||||
|
|
|
@ -13,7 +13,7 @@ import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuild
|
|||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
|
||||
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValueConnector';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'torrent', name: 'Torrent' },
|
||||
{ id: 'usenet', name: 'Usenet' }
|
||||
];
|
||||
|
||||
function ProtocolFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProtocolFilterBuilderRowValue;
|
|
@ -0,0 +1,30 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.downloadClients,
|
||||
(state) => state.settings.indexers,
|
||||
(downloadClients, indexers) => {
|
||||
const protocols = Array.from(new Set([
|
||||
...downloadClients.items.map((i) => i.protocol),
|
||||
...indexers.items.map((i) => i.protocol)
|
||||
]));
|
||||
|
||||
console.log(protocols);
|
||||
const tagList = protocols.map((protocol) => {
|
||||
return {
|
||||
id: protocol,
|
||||
name: protocol.replace('DownloadProtocol', '')
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -157,6 +157,10 @@ const links = [
|
|||
title: () => translate('Updates'),
|
||||
to: '/system/updates'
|
||||
},
|
||||
{
|
||||
title: () => translate('Plugins'),
|
||||
to: '/system/plugins'
|
||||
},
|
||||
{
|
||||
title: () => translate('Events'),
|
||||
to: '/system/events'
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { clearMessages, setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import { removeItem, update, updateItem } from 'Store/Actions/baseActions';
|
||||
import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions';
|
||||
|
@ -40,6 +40,7 @@ const mapDispatchToProps = {
|
|||
dispatchFetchCommands: fetchCommands,
|
||||
dispatchUpdateCommand: updateCommand,
|
||||
dispatchFinishCommand: finishCommand,
|
||||
dispatchClearMessages: clearMessages,
|
||||
dispatchSetAppValue: setAppValue,
|
||||
dispatchSetVersion: setVersion,
|
||||
dispatchUpdate: update,
|
||||
|
@ -340,6 +341,7 @@ class SignalRConnector extends Component {
|
|||
const {
|
||||
dispatchFetchCommands,
|
||||
dispatchFetchArtist,
|
||||
dispatchClearMessages,
|
||||
dispatchSetAppValue
|
||||
} = this.props;
|
||||
|
||||
|
@ -353,7 +355,9 @@ class SignalRConnector extends Component {
|
|||
// Repopulate the page (if a repopulator is set) to ensure things
|
||||
// are in sync after reconnecting.
|
||||
dispatchFetchArtist();
|
||||
dispatchClearMessages();
|
||||
dispatchFetchCommands();
|
||||
|
||||
repopulatePage();
|
||||
};
|
||||
|
||||
|
@ -382,6 +386,7 @@ SignalRConnector.propTypes = {
|
|||
dispatchFetchCommands: PropTypes.func.isRequired,
|
||||
dispatchUpdateCommand: PropTypes.func.isRequired,
|
||||
dispatchFinishCommand: PropTypes.func.isRequired,
|
||||
dispatchClearMessages: PropTypes.func.isRequired,
|
||||
dispatchSetAppValue: PropTypes.func.isRequired,
|
||||
dispatchSetVersion: PropTypes.func.isRequired,
|
||||
dispatchUpdate: PropTypes.func.isRequired,
|
||||
|
|
|
@ -5,14 +5,14 @@ import sortByProp from 'Utilities/Array/sortByProp';
|
|||
import Label from './Label';
|
||||
import styles from './TagList.css';
|
||||
|
||||
function TagList({ tags, tagList }) {
|
||||
function TagList({ className, tags, tagList }) {
|
||||
const sortedTags = tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'));
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
<div className={className}>
|
||||
{
|
||||
sortedTags.map((tag) => {
|
||||
return (
|
||||
|
@ -30,8 +30,13 @@ function TagList({ tags, tagList }) {
|
|||
}
|
||||
|
||||
TagList.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
TagList.defaultProps = {
|
||||
className: styles.tags
|
||||
};
|
||||
|
||||
export default TagList;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
||||
export const DELAY_PROFILE = 'delayProfile';
|
||||
export const DOWNLOAD_PROTOCOL_ITEM = 'downloadProtocolItem';
|
||||
export const TABLE_COLUMN = 'tableColumn';
|
||||
|
|
|
@ -171,7 +171,7 @@ class InteractiveSearchRow extends Component {
|
|||
|
||||
<TableRowCell className={styles.peers}>
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
protocol === 'TorrentDownloadProtocol' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
|
|
|
@ -13,6 +13,19 @@ import translate from 'Utilities/String/translate';
|
|||
import AddDownloadClientItem from './AddDownloadClientItem';
|
||||
import styles from './AddDownloadClientModalContent.css';
|
||||
|
||||
function mapDownloadClients(clients, onDownloadClientSelect) {
|
||||
return clients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
implementation={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class AddDownloadClientModalContent extends Component {
|
||||
|
||||
//
|
||||
|
@ -25,6 +38,7 @@ class AddDownloadClientModalContent extends Component {
|
|||
schemaError,
|
||||
usenetDownloadClients,
|
||||
torrentDownloadClients,
|
||||
otherDownloadClients,
|
||||
onDownloadClientSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
@ -64,16 +78,7 @@ class AddDownloadClientModalContent extends Component {
|
|||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
usenetDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
implementation={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
mapDownloadClients(usenetDownloadClients, onDownloadClientSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
@ -81,19 +86,22 @@ class AddDownloadClientModalContent extends Component {
|
|||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
torrentDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
implementation={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
mapDownloadClients(torrentDownloadClients, onDownloadClientSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
{
|
||||
otherDownloadClients.length ?
|
||||
<FieldSet legend="Other">
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
mapDownloadClients(otherDownloadClients, onDownloadClientSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
@ -115,6 +123,7 @@ AddDownloadClientModalContent.propTypes = {
|
|||
schemaError: PropTypes.object,
|
||||
usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
otherDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDownloadClientSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -17,15 +17,18 @@ function createMapStateToProps() {
|
|||
schema
|
||||
} = downloadClients;
|
||||
|
||||
const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' });
|
||||
const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' });
|
||||
const usenetDownloadClients = _.filter(schema, { protocol: 'UsenetDownloadProtocol' });
|
||||
const torrentDownloadClients = _.filter(schema, { protocol: 'TorrentDownloadProtocol' });
|
||||
const otherDownloadClients = _.filter(schema, (x) => x.protocol !== 'UsenetDownloadProtocol' &&
|
||||
x.protocol !== 'TorrentDownloadProtocol');
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetDownloadClients,
|
||||
torrentDownloadClients
|
||||
torrentDownloadClients,
|
||||
otherDownloadClients
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,6 +13,19 @@ import translate from 'Utilities/String/translate';
|
|||
import AddIndexerItem from './AddIndexerItem';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
function mapIndexers(indexers, onIndexerSelect) {
|
||||
return indexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
|
@ -25,6 +38,7 @@ class AddIndexerModalContent extends Component {
|
|||
schemaError,
|
||||
usenetIndexers,
|
||||
torrentIndexers,
|
||||
otherIndexers,
|
||||
onIndexerSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
@ -64,16 +78,7 @@ class AddIndexerModalContent extends Component {
|
|||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
usenetIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
mapIndexers(usenetIndexers, onIndexerSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
@ -81,19 +86,22 @@ class AddIndexerModalContent extends Component {
|
|||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
torrentIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
mapIndexers(torrentIndexers, onIndexerSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
{
|
||||
otherIndexers.length ?
|
||||
<FieldSet legend="Other">
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
mapIndexers(otherIndexers, onIndexerSelect)
|
||||
}
|
||||
</div>
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
@ -115,6 +123,7 @@ AddIndexerModalContent.propTypes = {
|
|||
schemaError: PropTypes.object,
|
||||
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
otherIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -17,15 +17,18 @@ function createMapStateToProps() {
|
|||
schema
|
||||
} = indexers;
|
||||
|
||||
const usenetIndexers = _.filter(schema, { protocol: 'usenet' });
|
||||
const torrentIndexers = _.filter(schema, { protocol: 'torrent' });
|
||||
const usenetIndexers = _.filter(schema, { protocol: 'UsenetDownloadProtocol' });
|
||||
const torrentIndexers = _.filter(schema, { protocol: 'TorrentDownloadProtocol' });
|
||||
const otherIndexers = _.filter(schema, (x) => x.protocol !== 'UsenetDownloadProtocol' &&
|
||||
x.protocol !== 'TorrentDownloadProtocol');
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetIndexers,
|
||||
torrentIndexers
|
||||
torrentIndexers,
|
||||
otherIndexers
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
line-height: 30px;
|
||||
}
|
||||
|
||||
.column {
|
||||
.name {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.fillcolumn {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'column': string;
|
||||
'delayProfile': string;
|
||||
'dragHandle': string;
|
||||
'dragIcon': string;
|
||||
'editButton': string;
|
||||
'fillcolumn': string;
|
||||
'isDragging': string;
|
||||
'name': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -6,28 +6,11 @@ import Link from 'Components/Link/Link';
|
|||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DelayProfileItem from './DelayProfileItem';
|
||||
import EditDelayProfileModalConnector from './EditDelayProfileModalConnector';
|
||||
import styles from './DelayProfile.css';
|
||||
|
||||
function getDelay(enabled, delay) {
|
||||
if (!enabled) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (!delay) {
|
||||
return 'No Delay';
|
||||
}
|
||||
|
||||
if (delay === 1) {
|
||||
return '1 Minute';
|
||||
}
|
||||
|
||||
// TODO: use better units of time than just minutes
|
||||
return `${delay} Minutes`;
|
||||
}
|
||||
|
||||
class DelayProfile extends Component {
|
||||
|
||||
//
|
||||
|
@ -74,25 +57,14 @@ class DelayProfile extends Component {
|
|||
render() {
|
||||
const {
|
||||
id,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
preferredProtocol,
|
||||
usenetDelay,
|
||||
torrentDelay,
|
||||
name,
|
||||
items,
|
||||
tags,
|
||||
tagList,
|
||||
isDragging,
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
let preferred = titleCase(translate('PreferProtocol', { preferredProtocol }));
|
||||
|
||||
if (!enableUsenet) {
|
||||
preferred = translate('OnlyTorrent');
|
||||
} else if (!enableTorrent) {
|
||||
preferred = translate('OnlyUsenet');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -100,11 +72,26 @@ class DelayProfile extends Component {
|
|||
isDragging && styles.isDragging
|
||||
)}
|
||||
>
|
||||
<div className={styles.column}>{preferred}</div>
|
||||
<div className={styles.column}>{getDelay(enableUsenet, usenetDelay)}</div>
|
||||
<div className={styles.column}>{getDelay(enableTorrent, torrentDelay)}</div>
|
||||
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<div className={styles.fillcolumn}>
|
||||
{
|
||||
items.map((x) => {
|
||||
return (
|
||||
<DelayProfileItem
|
||||
key={x.protocol}
|
||||
name={x.name}
|
||||
allowed={x.allowed}
|
||||
delay={x.delay}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
className={styles.fillcolumn}
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
@ -153,11 +140,8 @@ class DelayProfile extends Component {
|
|||
|
||||
DelayProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
enableUsenet: PropTypes.bool.isRequired,
|
||||
enableTorrent: PropTypes.bool.isRequired,
|
||||
preferredProtocol: PropTypes.string.isRequired,
|
||||
usenetDelay: PropTypes.number.isRequired,
|
||||
torrentDelay: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
|
|
43
frontend/src/Settings/Profiles/Delay/DelayProfileItem.js
Normal file
43
frontend/src/Settings/Profiles/Delay/DelayProfileItem.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getDelay(item) {
|
||||
if (!item.allowed) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (!item.delay) {
|
||||
return 'No Delay';
|
||||
}
|
||||
|
||||
if (item.delay === 1) {
|
||||
return '1 Minute';
|
||||
}
|
||||
|
||||
// TODO: use better units of time than just minutes
|
||||
return `${item.delay} Minutes`;
|
||||
}
|
||||
|
||||
function DelayProfileItem(props) {
|
||||
const {
|
||||
name,
|
||||
allowed
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Label
|
||||
kind={allowed ? kinds.INFO : kinds.DANGER}
|
||||
>
|
||||
{name}: {getDelay(props)}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
DelayProfileItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default DelayProfileItem;
|
|
@ -12,14 +12,18 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.column {
|
||||
.name {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
.fillcolumn {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
|
||||
.addDelayProfile {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'addButton': string;
|
||||
'addDelayProfile': string;
|
||||
'column': string;
|
||||
'delayProfiles': string;
|
||||
'delayProfilesHeader': string;
|
||||
'fillcolumn': string;
|
||||
'horizontalScroll': string;
|
||||
'tags': string;
|
||||
'name': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -82,18 +82,10 @@ class DelayProfiles extends Component {
|
|||
>
|
||||
<div>
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>
|
||||
{translate('PreferredProtocol')}
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
{translate('UsenetDelay')}
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
{translate('TorrentDelay')}
|
||||
</div>
|
||||
<div className={styles.tags}>
|
||||
{translate('Tags')}
|
||||
</div>
|
||||
<div className={styles.name}>{translate('Name')}</div>
|
||||
<div className={styles.fillcolumn}>{translate('Protocols')}</div>
|
||||
<div className={styles.fillcolumn}>{translate('Tags')}</div>
|
||||
<div className={styles.actions} />
|
||||
</div>
|
||||
|
||||
<div className={styles.delayProfiles}>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
.qualityProfileItem {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
|
||||
&.isInGroup {
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
.checkInputContainer {
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.checkInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.delayContainer {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.delayInput {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
background-color: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.qualityNameContainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
margin-left: 2px;
|
||||
font-weight: normal;
|
||||
line-height: $qualityProfileItemHeight;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qualityName {
|
||||
&.notAllowed {
|
||||
color: #c6c6c6;
|
||||
}
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dragIcon {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.isDragging {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.isPreview {
|
||||
.qualityName {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
19
frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css.d.ts
vendored
Normal file
19
frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'checkInput': string;
|
||||
'checkInputContainer': string;
|
||||
'delayContainer': string;
|
||||
'delayInput': string;
|
||||
'dragHandle': string;
|
||||
'dragIcon': string;
|
||||
'isDragging': string;
|
||||
'isInGroup': string;
|
||||
'isPreview': string;
|
||||
'notAllowed': string;
|
||||
'qualityName': string;
|
||||
'qualityNameContainer': string;
|
||||
'qualityProfileItem': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
113
frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.js
Normal file
113
frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './DownloadProtocolItem.css';
|
||||
|
||||
class DownloadProtocolItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
const {
|
||||
protocol,
|
||||
onDownloadProtocolItemFieldChange
|
||||
} = this.props;
|
||||
|
||||
onDownloadProtocolItemFieldChange(protocol, name, value);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isPreview,
|
||||
name,
|
||||
allowed,
|
||||
delay,
|
||||
isDragging,
|
||||
isOverCurrent,
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItem,
|
||||
isDragging && styles.isDragging,
|
||||
isPreview && styles.isPreview,
|
||||
isOverCurrent && styles.isOverCurrent
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.qualityNameContainer}
|
||||
>
|
||||
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
containerClassName={styles.checkInputContainer}
|
||||
name={'allowed'}
|
||||
value={allowed}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
||||
<div className={classNames(
|
||||
styles.qualityName,
|
||||
!allowed && styles.notAllowed
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<NumberInput
|
||||
containerClassName={styles.delayContainer}
|
||||
className={styles.delayInput}
|
||||
name={'delay'}
|
||||
value={delay}
|
||||
min={0}
|
||||
max={9999999}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
||||
{
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
title="Create group"
|
||||
name={icons.REORDER}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProtocolItem.propTypes = {
|
||||
isPreview: PropTypes.bool,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
isOverCurrent: PropTypes.bool.isRequired,
|
||||
connectDragSource: PropTypes.func,
|
||||
onDownloadProtocolItemFieldChange: PropTypes.func
|
||||
};
|
||||
|
||||
DownloadProtocolItem.defaultProps = {
|
||||
isPreview: false,
|
||||
isOverCurrent: false,
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
};
|
||||
|
||||
export default DownloadProtocolItem;
|
|
@ -0,0 +1,4 @@
|
|||
.dragPreview {
|
||||
width: 480px;
|
||||
opacity: 0.75;
|
||||
}
|
7
frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css.d.ts
vendored
Normal file
7
frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'dragPreview': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,89 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||
import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
|
||||
import dimensions from 'Styles/Variables/dimensions.js';
|
||||
import DownloadProtocolItem from './DownloadProtocolItem';
|
||||
import styles from './DownloadProtocolItemDragPreview.css';
|
||||
|
||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||
const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
|
||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||
|
||||
function collectDragLayer(monitor) {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
currentOffset: monitor.getSourceClientOffset()
|
||||
};
|
||||
}
|
||||
|
||||
class DownloadProtocolItemDragPreview extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
item,
|
||||
itemType,
|
||||
currentOffset
|
||||
} = this.props;
|
||||
|
||||
if (!currentOffset || itemType !== DOWNLOAD_PROTOCOL_ITEM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The offset is shifted because the drag handle is on the right edge of the
|
||||
// list item and the preview is wider than the drag handle.
|
||||
|
||||
const { x, y } = currentOffset;
|
||||
const handleOffset = formGroupSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
WebkitTransform: transform,
|
||||
msTransform: transform,
|
||||
transform
|
||||
};
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
allowed,
|
||||
delay
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<DragPreviewLayer>
|
||||
<div
|
||||
className={styles.dragPreview}
|
||||
style={style}
|
||||
>
|
||||
<DownloadProtocolItem
|
||||
isPreview={true}
|
||||
id={id}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
delay={delay}
|
||||
isDragging={false}
|
||||
/>
|
||||
</div>
|
||||
</DragPreviewLayer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProtocolItemDragPreview.propTypes = {
|
||||
item: PropTypes.object,
|
||||
itemType: PropTypes.string,
|
||||
currentOffset: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default DragLayer(collectDragLayer)(DownloadProtocolItemDragPreview);
|
|
@ -0,0 +1,18 @@
|
|||
.downloadProtocolItemDragSource {
|
||||
padding: $qualityProfileItemDragSourcePadding 0;
|
||||
}
|
||||
|
||||
.downloadProtocolItemPlaceholder {
|
||||
width: 100%;
|
||||
height: $qualityProfileItemHeight;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.downloadProtocolItemPlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.downloadProtocolItemPlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
10
frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css.d.ts
vendored
Normal file
10
frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'downloadProtocolItemDragSource': string;
|
||||
'downloadProtocolItemPlaceholder': string;
|
||||
'downloadProtocolItemPlaceholderAfter': string;
|
||||
'downloadProtocolItemPlaceholderBefore': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,188 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
|
||||
import DownloadProtocolItem from './DownloadProtocolItem';
|
||||
import styles from './DownloadProtocolItemDragSource.css';
|
||||
|
||||
const downloadProtocolItemDragSource = {
|
||||
beginDrag(props) {
|
||||
const {
|
||||
index,
|
||||
protocol,
|
||||
name,
|
||||
allowed,
|
||||
delay
|
||||
} = props;
|
||||
|
||||
return {
|
||||
index,
|
||||
protocol,
|
||||
name,
|
||||
allowed,
|
||||
delay
|
||||
};
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
props.onDownloadProtocolItemDragEnd(monitor.didDrop());
|
||||
}
|
||||
};
|
||||
|
||||
const downloadProtocolItemDropTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const {
|
||||
index: dragIndex
|
||||
} = monitor.getItem();
|
||||
|
||||
const dropIndex = props.index;
|
||||
|
||||
// Use childNodeIndex to select the correct node to get the middle of so
|
||||
// we don't bounce between above and below causing rapid setState calls.
|
||||
const childNodeIndex = component.props.isOverCurrent && component.props.isDraggingUp ? 1 :0;
|
||||
const componentDOMNode = findDOMNode(component).children[childNodeIndex];
|
||||
const hoverBoundingRect = componentDOMNode.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// If we're hovering over a child don't trigger on the parent
|
||||
if (!monitor.isOver({ shallow: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show targets for dropping on self
|
||||
if (dragIndex === dropIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dropPosition = null;
|
||||
|
||||
// Determine drop position based on position over target
|
||||
if (hoverClientY > hoverMiddleY) {
|
||||
dropPosition = 'below';
|
||||
} else if (hoverClientY < hoverMiddleY) {
|
||||
dropPosition = 'above';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onDownloadProtocolItemDragMove({
|
||||
dragIndex,
|
||||
dropIndex,
|
||||
dropPosition
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function collectDragSource(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
isOverCurrent: monitor.isOver({ shallow: true })
|
||||
};
|
||||
}
|
||||
|
||||
class DownloadProtocolItemDragSource extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
protocol,
|
||||
name,
|
||||
allowed,
|
||||
delay,
|
||||
index,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
isDraggingDown,
|
||||
isOverCurrent,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
onDownloadProtocolItemFieldChange
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||
const isAfter = !isDragging && isDraggingDown && isOverCurrent;
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={classNames(
|
||||
styles.downloadProtocolItemDragSource,
|
||||
isBefore && styles.isDraggingUp,
|
||||
isAfter && styles.isDraggingDown
|
||||
)}
|
||||
>
|
||||
{
|
||||
isBefore &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.downloadProtocolItemPlaceholder,
|
||||
styles.downloadProtocolItemPlaceholderBefore
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
<DownloadProtocolItem
|
||||
protocol={protocol}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
delay={delay}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isOverCurrent={isOverCurrent}
|
||||
connectDragSource={connectDragSource}
|
||||
onDownloadProtocolItemFieldChange={onDownloadProtocolItemFieldChange}
|
||||
/>
|
||||
|
||||
{
|
||||
isAfter &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.downloadProtocolItemPlaceholder,
|
||||
styles.downloadProtocolItemPlaceholderAfter
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProtocolItemDragSource.propTypes = {
|
||||
protocol: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
isDraggingDown: PropTypes.bool,
|
||||
isOverCurrent: PropTypes.bool,
|
||||
connectDragSource: PropTypes.func,
|
||||
connectDropTarget: PropTypes.func,
|
||||
onDownloadProtocolItemFieldChange: PropTypes.func.isRequired,
|
||||
onDownloadProtocolItemDragMove: PropTypes.func.isRequired,
|
||||
onDownloadProtocolItemDragEnd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
DOWNLOAD_PROTOCOL_ITEM,
|
||||
downloadProtocolItemDropTarget,
|
||||
collectDropTarget
|
||||
)(DragSource(
|
||||
DOWNLOAD_PROTOCOL_ITEM,
|
||||
downloadProtocolItemDragSource,
|
||||
collectDragSource
|
||||
)(DownloadProtocolItemDragSource));
|
|
@ -0,0 +1,24 @@
|
|||
.qualities {
|
||||
margin-top: 10px;
|
||||
transition: min-height 200ms;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.headerDelay {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
margin-right: 40px;
|
||||
padding-left: 16px;
|
||||
width: 150px;
|
||||
}
|
10
frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css.d.ts
vendored
Normal file
10
frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'headerContainer': string;
|
||||
'headerDelay': string;
|
||||
'headerTitle': string;
|
||||
'qualities': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
150
frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.js
Normal file
150
frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Measure from 'Components/Measure';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DownloadProtocolItemDragPreview from './DownloadProtocolItemDragPreview';
|
||||
import DownloadProtocolItemDragSource from './DownloadProtocolItemDragSource';
|
||||
import styles from './DownloadProtocolItems.css';
|
||||
|
||||
class DownloadProtocolItems extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ height }) => {
|
||||
this.setState({ height });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dropIndex,
|
||||
dropPosition,
|
||||
items,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
height
|
||||
} = this.state;
|
||||
|
||||
const isDragging = dropIndex !== null;
|
||||
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||
|
||||
return (
|
||||
<FormGroup size={sizes.SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Download Protocols
|
||||
</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text="Protocols higher in the list are more preferred. Only checked protocols are allowed"
|
||||
/>
|
||||
|
||||
{
|
||||
errors.map((error, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={error.message}
|
||||
isError={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
warnings.map((warning, index) => {
|
||||
return (
|
||||
<FormInputHelpText
|
||||
key={index}
|
||||
text={warning.message}
|
||||
isWarning={true}
|
||||
isCheckInput={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Measure
|
||||
whitelist={['height']}
|
||||
includeMargin={false}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<div
|
||||
className={styles.qualities}
|
||||
style={{ minHeight: `${height}px` }}
|
||||
>
|
||||
<div className={styles.headerContainer}>
|
||||
<div className={styles.headerTitle}>
|
||||
Protocol
|
||||
</div>
|
||||
<div className={styles.headerDelay}>
|
||||
Delay (minutes)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
items.map(({ protocol, name, allowed, delay }, index) => {
|
||||
return (
|
||||
<DownloadProtocolItemDragSource
|
||||
key={protocol}
|
||||
protocol={protocol}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
delay={delay}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<DownloadProtocolItemDragPreview />
|
||||
</div>
|
||||
</Measure>
|
||||
</div>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProtocolItems.propTypes = {
|
||||
dragIndex: PropTypes.number,
|
||||
dropIndex: PropTypes.number,
|
||||
dropPosition: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
DownloadProtocolItems.defaultProps = {
|
||||
errors: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
export default DownloadProtocolItems;
|
|
@ -12,49 +12,22 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { boolSettingShape, numberSettingShape, stringSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DownloadProtocolItems from './DownloadProtocolItems';
|
||||
import styles from './EditDelayProfileModalContent.css';
|
||||
|
||||
const protocolOptions = [
|
||||
{
|
||||
key: 'preferUsenet',
|
||||
get value() {
|
||||
return translate('PreferUsenet');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'preferTorrent',
|
||||
get value() {
|
||||
return translate('PreferTorrent');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'onlyUsenet',
|
||||
get value() {
|
||||
return translate('OnlyUsenet');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'onlyTorrent',
|
||||
get value() {
|
||||
return translate('OnlyTorrent');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function EditDelayProfileModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
protocol,
|
||||
onInputChange,
|
||||
onProtocolChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteDelayProfilePress,
|
||||
|
@ -62,10 +35,8 @@ function EditDelayProfileModalContent(props) {
|
|||
} = props;
|
||||
|
||||
const {
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay,
|
||||
name,
|
||||
items,
|
||||
bypassIfHighestQuality,
|
||||
bypassIfAboveCustomFormatScore,
|
||||
minimumCustomFormatScore,
|
||||
|
@ -94,54 +65,29 @@ function EditDelayProfileModalContent(props) {
|
|||
}
|
||||
|
||||
{
|
||||
!isFetching && !error ?
|
||||
!isFetching && isPopulated && !error ?
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PreferredProtocol')}</FormLabel>
|
||||
<FormGroup size={sizes.SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
{translate('Name')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="protocol"
|
||||
value={protocol}
|
||||
values={protocolOptions}
|
||||
helpText={translate('ProtocolHelpText')}
|
||||
onChange={onProtocolChange}
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
enableUsenet.value ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('UsenetDelay')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="usenetDelay"
|
||||
unit="minutes"
|
||||
{...usenetDelay}
|
||||
helpText={translate('UsenetDelayHelpText')}
|
||||
onChange={onInputChange}
|
||||
<div className={styles.formGroupWrapper}>
|
||||
<DownloadProtocolItems
|
||||
items={items.value}
|
||||
errors={items.errors}
|
||||
warnings={items.warnings}
|
||||
{...otherProps}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
enableTorrent.value ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('TorrentDelay')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="torrentDelay"
|
||||
unit="minutes"
|
||||
{...torrentDelay}
|
||||
helpText={translate('TorrentDelayHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BypassIfHighestQuality')}</FormLabel>
|
||||
|
@ -189,8 +135,10 @@ function EditDelayProfileModalContent(props) {
|
|||
{translate('DefaultDelayProfileArtist')}
|
||||
</Alert> :
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
<FormGroup size={sizes.SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
|
@ -237,13 +185,11 @@ function EditDelayProfileModalContent(props) {
|
|||
}
|
||||
|
||||
const delayProfileShape = {
|
||||
enableUsenet: PropTypes.shape(boolSettingShape).isRequired,
|
||||
enableTorrent: PropTypes.shape(boolSettingShape).isRequired,
|
||||
usenetDelay: PropTypes.shape(numberSettingShape).isRequired,
|
||||
torrentDelay: PropTypes.shape(numberSettingShape).isRequired,
|
||||
bypassIfHighestQuality: PropTypes.shape(boolSettingShape).isRequired,
|
||||
bypassIfAboveCustomFormatScore: PropTypes.shape(boolSettingShape).isRequired,
|
||||
minimumCustomFormatScore: PropTypes.shape(numberSettingShape).isRequired,
|
||||
name: PropTypes.shape(stringSettingShape).isRequired,
|
||||
items: PropTypes.object.isRequired,
|
||||
order: PropTypes.shape(numberSettingShape),
|
||||
tags: PropTypes.shape(tagSettingShape).isRequired
|
||||
};
|
||||
|
@ -251,13 +197,12 @@ const delayProfileShape = {
|
|||
EditDelayProfileModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onProtocolChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteDelayProfilePress: PropTypes.func
|
||||
|
|
|
@ -3,77 +3,15 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { fetchDelayProfileSchema, saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDelayProfileModalContent from './EditDelayProfileModalContent';
|
||||
|
||||
const newDelayProfile = {
|
||||
enableUsenet: true,
|
||||
enableTorrent: true,
|
||||
preferredProtocol: 'usenet',
|
||||
usenetDelay: 0,
|
||||
torrentDelay: 0,
|
||||
bypassIfHighestQuality: false,
|
||||
bypassIfAboveCustomFormatScore: false,
|
||||
minimumCustomFormatScore: 0,
|
||||
tags: []
|
||||
};
|
||||
|
||||
function createDelayProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.delayProfiles,
|
||||
(id, delayProfiles) => {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges,
|
||||
items
|
||||
} = delayProfiles;
|
||||
|
||||
const profile = id ? _.find(items, { id }) : newDelayProfile;
|
||||
const settings = selectSettings(profile, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDelayProfileSelector(),
|
||||
createProviderSettingsSelector('delayProfiles'),
|
||||
(delayProfile) => {
|
||||
const enableUsenet = delayProfile.item.enableUsenet.value;
|
||||
const enableTorrent = delayProfile.item.enableTorrent.value;
|
||||
const preferredProtocol = delayProfile.item.preferredProtocol.value;
|
||||
let protocol = 'preferUsenet';
|
||||
|
||||
if (preferredProtocol === 'usenet') {
|
||||
protocol = 'preferUsenet';
|
||||
} else {
|
||||
protocol = 'preferTorrent';
|
||||
}
|
||||
|
||||
if (!enableUsenet) {
|
||||
protocol = 'onlyTorrent';
|
||||
}
|
||||
|
||||
if (!enableTorrent) {
|
||||
protocol = 'onlyUsenet';
|
||||
}
|
||||
|
||||
return {
|
||||
protocol,
|
||||
...delayProfile
|
||||
};
|
||||
}
|
||||
|
@ -81,6 +19,7 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDelayProfileSchema,
|
||||
setDelayProfileValue,
|
||||
saveDelayProfile
|
||||
};
|
||||
|
@ -90,14 +29,19 @@ class EditDelayProfileModalContentConnector extends Component {
|
|||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
dragIndex: null,
|
||||
dropIndex: null,
|
||||
dropPosition: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
Object.keys(newDelayProfile).forEach((name) => {
|
||||
this.props.setDelayProfileValue({
|
||||
name,
|
||||
value: newDelayProfile[name]
|
||||
});
|
||||
});
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchDelayProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,35 +58,77 @@ class EditDelayProfileModalContentConnector extends Component {
|
|||
this.props.setDelayProfileValue({ name, value });
|
||||
};
|
||||
|
||||
onProtocolChange = ({ value }) => {
|
||||
switch (value) {
|
||||
case 'preferUsenet':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' });
|
||||
break;
|
||||
case 'preferTorrent':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' });
|
||||
break;
|
||||
case 'onlyUsenet':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: false });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' });
|
||||
break;
|
||||
case 'onlyTorrent':
|
||||
this.props.setDelayProfileValue({ name: 'enableUsenet', value: false });
|
||||
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true });
|
||||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' });
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unknown protocol option: ${value}`);
|
||||
onSavePress = () => {
|
||||
this.props.saveDelayProfile({ id: this.props.id });
|
||||
};
|
||||
|
||||
onDownloadProtocolItemFieldChange = (protocol, name, value) => {
|
||||
const delayProfile = _.cloneDeep(this.props.item);
|
||||
const items = delayProfile.items.value;
|
||||
const item = _.find(delayProfile.items.value, (i) => i.protocol === protocol);
|
||||
|
||||
item[name] = value;
|
||||
|
||||
this.props.setDelayProfileValue({
|
||||
name: 'items',
|
||||
value: items
|
||||
});
|
||||
};
|
||||
|
||||
onDownloadProtocolItemDragMove = ({ dragIndex, dropIndex, dropPosition }) => {
|
||||
if (
|
||||
(dropPosition === 'below' && dropIndex + 1 === dragIndex) ||
|
||||
(dropPosition === 'above' && dropIndex - 1 === dragIndex)
|
||||
) {
|
||||
if (
|
||||
this.state.dragIndex != null &&
|
||||
this.state.dropIndex != null &&
|
||||
this.state.dropPosition != null
|
||||
) {
|
||||
this.setState({
|
||||
dragIndex: null,
|
||||
dropIndex: null,
|
||||
dropPosition: null
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.dragIndex !== dragIndex ||
|
||||
this.state.dropIndex !== dropIndex ||
|
||||
this.state.dropPosition !== dropPosition) {
|
||||
this.setState({
|
||||
dragIndex,
|
||||
dropIndex,
|
||||
dropPosition
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveDelayProfile({ id: this.props.id });
|
||||
onDownloadProtocolItemDragEnd = (didDrop) => {
|
||||
const {
|
||||
dragIndex,
|
||||
dropIndex
|
||||
} = this.state;
|
||||
|
||||
if (didDrop && dropIndex !== null) {
|
||||
const delayProfile = _.cloneDeep(this.props.item);
|
||||
const items = delayProfile.items.value;
|
||||
const item = items.splice(dragIndex, 1)[0];
|
||||
|
||||
items.splice(dropIndex, 0, item);
|
||||
|
||||
this.props.setDelayProfileValue({
|
||||
name: 'items',
|
||||
value: items
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dragIndex: null,
|
||||
dropIndex: null
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
|
@ -151,10 +137,13 @@ class EditDelayProfileModalContentConnector extends Component {
|
|||
render() {
|
||||
return (
|
||||
<EditDelayProfileModalContent
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onProtocolChange={this.onProtocolChange}
|
||||
onDownloadProtocolItemFieldChange={this.onDownloadProtocolItemFieldChange}
|
||||
onDownloadProtocolItemDragMove={this.onDownloadProtocolItemDragMove}
|
||||
onDownloadProtocolItemDragEnd={this.onDownloadProtocolItemDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -162,9 +151,11 @@ class EditDelayProfileModalContentConnector extends Component {
|
|||
|
||||
EditDelayProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
fetchDelayProfileSchema: PropTypes.func.isRequired,
|
||||
setDelayProfileValue: PropTypes.func.isRequired,
|
||||
saveDelayProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.delayProfile {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-bottom: 10px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
flex: 1 0 auto;
|
||||
}
|
9
frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.css.d.ts
vendored
Normal file
9
frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.css.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'delayProfile': string;
|
||||
'name': string;
|
||||
'tags': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -1,35 +1,41 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import DelayProfileItem from 'Settings/Profiles/Delay/DelayProfileItem';
|
||||
import styles from './TagDetailsDelayProfile.css';
|
||||
|
||||
function TagDetailsDelayProfile(props) {
|
||||
const {
|
||||
preferredProtocol,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay
|
||||
name: profileName,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Protocol: {titleCase(preferredProtocol)}
|
||||
<div
|
||||
className={styles.delayProfile}
|
||||
>
|
||||
<div
|
||||
className={styles.name}
|
||||
>
|
||||
{profileName}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.tags}>
|
||||
{
|
||||
enableUsenet ?
|
||||
`Usenet Delay: ${usenetDelay}` :
|
||||
'Usenet disabled'
|
||||
}
|
||||
</div>
|
||||
items.map((item) => {
|
||||
const {
|
||||
protocol,
|
||||
name,
|
||||
allowed
|
||||
} = item;
|
||||
|
||||
<div>
|
||||
{
|
||||
enableTorrent ?
|
||||
`Torrent Delay: ${torrentDelay}` :
|
||||
'Torrents disabled'
|
||||
return (
|
||||
<DelayProfileItem
|
||||
key={protocol}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,11 +43,8 @@ function TagDetailsDelayProfile(props) {
|
|||
}
|
||||
|
||||
TagDetailsDelayProfile.propTypes = {
|
||||
preferredProtocol: PropTypes.string.isRequired,
|
||||
enableUsenet: PropTypes.bool.isRequired,
|
||||
enableTorrent: PropTypes.bool.isRequired,
|
||||
usenetDelay: PropTypes.number.isRequired,
|
||||
torrentDelay: PropTypes.number.isRequired
|
||||
name: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default TagDetailsDelayProfile;
|
||||
|
|
|
@ -13,7 +13,7 @@ function TagDetailsModal(props) {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.SMALL}
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -65,21 +65,15 @@ function TagDetailsModalContent(props) {
|
|||
delayProfiles.map((item) => {
|
||||
const {
|
||||
id,
|
||||
preferredProtocol,
|
||||
enableUsenet,
|
||||
enableTorrent,
|
||||
usenetDelay,
|
||||
torrentDelay
|
||||
name,
|
||||
items
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<TagDetailsDelayProfile
|
||||
key={id}
|
||||
preferredProtocol={preferredProtocol}
|
||||
enableUsenet={enableUsenet}
|
||||
enableTorrent={enableTorrent}
|
||||
usenetDelay={usenetDelay}
|
||||
torrentDelay={torrentDelay}
|
||||
name={name}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
|
@ -52,6 +52,10 @@ export default {
|
|||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {},
|
||||
items: [],
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
|
|
|
@ -55,6 +55,7 @@ export const defaultState = {
|
|||
|
||||
export const SHOW_MESSAGE = 'app/showMessage';
|
||||
export const HIDE_MESSAGE = 'app/hideMessage';
|
||||
export const CLEAR_MESSAGES = 'app/clearMessages';
|
||||
export const SAVE_DIMENSIONS = 'app/saveDimensions';
|
||||
export const SET_VERSION = 'app/setVersion';
|
||||
export const SET_APP_VALUE = 'app/setAppValue';
|
||||
|
@ -72,6 +73,7 @@ export const setIsSidebarVisible = createAction(SET_IS_SIDEBAR_VISIBLE);
|
|||
export const setAppValue = createAction(SET_APP_VALUE);
|
||||
export const showMessage = createAction(SHOW_MESSAGE);
|
||||
export const hideMessage = createAction(HIDE_MESSAGE);
|
||||
export const clearMessages = createAction(CLEAR_MESSAGES);
|
||||
export const pingServer = createThunk(PING_SERVER);
|
||||
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
|
||||
|
||||
|
@ -192,6 +194,14 @@ export const reducers = createHandleActions({
|
|||
return updateSectionState(state, messagesSection, newState);
|
||||
},
|
||||
|
||||
[CLEAR_MESSAGES]: function(state) {
|
||||
const newState = getSectionState(state, messagesSection);
|
||||
|
||||
newState.items = [];
|
||||
|
||||
return updateSectionState(state, messagesSection, newState);
|
||||
},
|
||||
|
||||
[SET_APP_VALUE]: function(state, { payload }) {
|
||||
const newState = Object.assign(getSectionState(state, section), payload);
|
||||
|
||||
|
|
|
@ -70,6 +70,13 @@ export const defaultState = {
|
|||
items: []
|
||||
},
|
||||
|
||||
plugins: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
},
|
||||
|
||||
logs: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
|
@ -199,6 +206,8 @@ export const DELETE_BACKUP = 'system/backups/deleteBackup';
|
|||
|
||||
export const FETCH_UPDATES = 'system/updates/fetchUpdates';
|
||||
|
||||
export const FETCH_INSTALLED_PLUGINS = 'system/plugins/fetchInstalledPlugins';
|
||||
|
||||
export const FETCH_LOGS = 'system/logs/fetchLogs';
|
||||
export const GOTO_FIRST_LOGS_PAGE = 'system/logs/gotoLogsFirstPage';
|
||||
export const GOTO_PREVIOUS_LOGS_PAGE = 'system/logs/gotoLogsPreviousPage';
|
||||
|
@ -233,6 +242,8 @@ export const deleteBackup = createThunk(DELETE_BACKUP);
|
|||
|
||||
export const fetchUpdates = createThunk(FETCH_UPDATES);
|
||||
|
||||
export const fetchInstalledPlugins = createThunk(FETCH_INSTALLED_PLUGINS);
|
||||
|
||||
export const fetchLogs = createThunk(FETCH_LOGS);
|
||||
export const gotoLogsFirstPage = createThunk(GOTO_FIRST_LOGS_PAGE);
|
||||
export const gotoLogsPreviousPage = createThunk(GOTO_PREVIOUS_LOGS_PAGE);
|
||||
|
@ -326,6 +337,7 @@ export const actionHandlers = handleThunks({
|
|||
[DELETE_BACKUP]: createRemoveItemHandler(backupsSection, '/system/backup'),
|
||||
|
||||
[FETCH_UPDATES]: createFetchHandler('system.updates', '/update'),
|
||||
[FETCH_INSTALLED_PLUGINS]: createFetchHandler('system.plugins', '/system/plugins'),
|
||||
[FETCH_LOG_FILES]: createFetchHandler('system.logFiles', '/log/file'),
|
||||
[FETCH_UPDATE_LOG_FILES]: createFetchHandler('system.updateLogFiles', '/log/file/update'),
|
||||
|
||||
|
|
11
frontend/src/System/Plugins/PluginRow.css
Normal file
11
frontend/src/System/Plugins/PluginRow.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.version {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
8
frontend/src/System/Plugins/PluginRow.css.d.ts
vendored
Normal file
8
frontend/src/System/Plugins/PluginRow.css.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'version': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
79
frontend/src/System/Plugins/PluginRow.js
Normal file
79
frontend/src/System/Plugins/PluginRow.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import styles from './PluginRow.css';
|
||||
|
||||
class PluginRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInstallPluginPress = () => {
|
||||
this.props.onInstallPluginPress(this.props.githubUrl);
|
||||
};
|
||||
|
||||
onUninstallPluginPress = () => {
|
||||
this.props.onUninstallPluginPress(this.props.githubUrl);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
owner,
|
||||
installedVersion,
|
||||
availableVersion,
|
||||
updateAvailable,
|
||||
isInstallingPlugin,
|
||||
isUninstallingPlugin
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{name}</TableRowCell>
|
||||
<TableRowCell>{owner}</TableRowCell>
|
||||
<TableRowCell className={styles.version}>{installedVersion}</TableRowCell>
|
||||
<TableRowCell className={styles.version}>{availableVersion}</TableRowCell>
|
||||
<TableRowCell
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
updateAvailable &&
|
||||
<SpinnerIconButton
|
||||
name={icons.UPDATE}
|
||||
kind={kinds.DEFAULT}
|
||||
isSpinning={isInstallingPlugin}
|
||||
onPress={this.onInstallPluginPress}
|
||||
/>
|
||||
}
|
||||
<SpinnerIconButton
|
||||
name={icons.DELETE}
|
||||
kind={kinds.DEFAULT}
|
||||
isSpinning={isUninstallingPlugin}
|
||||
onPress={this.onUninstallPluginPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PluginRow.propTypes = {
|
||||
githubUrl: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
owner: PropTypes.string.isRequired,
|
||||
installedVersion: PropTypes.string.isRequired,
|
||||
availableVersion: PropTypes.string.isRequired,
|
||||
updateAvailable: PropTypes.bool.isRequired,
|
||||
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||
onInstallPluginPress: PropTypes.func.isRequired,
|
||||
isUninstallingPlugin: PropTypes.bool.isRequired,
|
||||
onUninstallPluginPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PluginRow;
|
6
frontend/src/System/Plugins/Plugins.css
Normal file
6
frontend/src/System/Plugins/Plugins.css
Normal file
|
@ -0,0 +1,6 @@
|
|||
.loading {
|
||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin-top: 5px;
|
||||
margin-left: auto;
|
||||
}
|
168
frontend/src/System/Plugins/Plugins.js
Normal file
168
frontend/src/System/Plugins/Plugins.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import PluginRow from './PluginRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'owner',
|
||||
label: 'Owner',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'installedVersion',
|
||||
label: 'Installed Version',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'availableVersion',
|
||||
label: 'Available Version',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class Plugins extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
repoUrl: ''
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
|
||||
onInstallPluginPress = () => {
|
||||
this.props.onInstallPluginPress(this.state.repoUrl);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isInstallingPlugin,
|
||||
onInstallPluginPress,
|
||||
isUninstallingPlugin,
|
||||
onUninstallPluginPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
repoUrl
|
||||
} = this.state;
|
||||
|
||||
const noPlugins = isPopulated && !error && !items.length;
|
||||
|
||||
return (
|
||||
<PageContent title="Plugins">
|
||||
<PageContentBody>
|
||||
<Form>
|
||||
<FieldSet legend="Install New Plugin">
|
||||
<FormGroup>
|
||||
<FormLabel>GitHub URL</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="repoUrl"
|
||||
helpText="URL to GitHub repository containing plugin"
|
||||
helpLink="https://wiki.servarr.com/Lidarr_FAQ#How_do_I_install_plugins"
|
||||
value={repoUrl}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingPlugin}
|
||||
onPress={this.onInstallPluginPress}
|
||||
>
|
||||
Install
|
||||
</SpinnerButton>
|
||||
</FieldSet>
|
||||
</Form>
|
||||
<FieldSet legend="Installed Plugins">
|
||||
{
|
||||
!isPopulated && !error &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && noPlugins &&
|
||||
<div>No plugins are installed</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !noPlugins &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((plugin) => {
|
||||
return (
|
||||
<PluginRow
|
||||
key={plugin.githubUrl}
|
||||
{...plugin}
|
||||
isInstallingPlugin={isInstallingPlugin}
|
||||
isUninstallingPlugin={isUninstallingPlugin}
|
||||
onInstallPluginPress={onInstallPluginPress}
|
||||
onUninstallPluginPress={onUninstallPluginPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Plugins.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||
onInstallPluginPress: PropTypes.func.isRequired,
|
||||
isUninstallingPlugin: PropTypes.bool.isRequired,
|
||||
onUninstallPluginPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Plugins;
|
95
frontend/src/System/Plugins/PluginsConnector.js
Normal file
95
frontend/src/System/Plugins/PluginsConnector.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
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 { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchInstalledPlugins } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Plugins from './Plugins';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.plugins,
|
||||
createCommandExecutingSelector(commandNames.INSTALL_PLUGIN),
|
||||
createCommandExecutingSelector(commandNames.UNINSTALL_PLUGIN),
|
||||
(
|
||||
plugins,
|
||||
isInstallingPlugin,
|
||||
isUninstallingPlugin
|
||||
) => {
|
||||
return {
|
||||
...plugins,
|
||||
isInstallingPlugin,
|
||||
isUninstallingPlugin
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchInstalledPlugins: fetchInstalledPlugins,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class PluginsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
this.repopulate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.dispatchFetchInstalledPlugins();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInstallPluginPress = (url) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.INSTALL_PLUGIN,
|
||||
githubUrl: url
|
||||
});
|
||||
};
|
||||
|
||||
onUninstallPluginPress = (url) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.UNINSTALL_PLUGIN,
|
||||
githubUrl: url
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Plugins
|
||||
onInstallPluginPress={this.onInstallPluginPress}
|
||||
onUninstallPluginPress={this.onUninstallPluginPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PluginsConnector.propTypes = {
|
||||
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);
|
|
@ -4,7 +4,6 @@ using Lidarr.Api.V1.Artist;
|
|||
using Lidarr.Api.V1.CustomFormats;
|
||||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace Lidarr.Api.V1.Blocklist
|
||||
|
@ -17,7 +16,7 @@ namespace Lidarr.Api.V1.Blocklist
|
|||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
|
|
|
@ -66,8 +66,9 @@ namespace Lidarr.Api.V1.Commands
|
|||
? CommandPriority.High
|
||||
: CommandPriority.Normal;
|
||||
|
||||
var command = STJson.Deserialize(body, commandType) as Command;
|
||||
dynamic command = STJson.Deserialize(body, commandType);
|
||||
|
||||
command.Trigger = CommandTrigger.Manual;
|
||||
command.SuppressMessages = !command.SendUpdatesToClient;
|
||||
command.SendUpdatesToClient = true;
|
||||
command.ClientUserAgent = Request.Headers["UserAgent"];
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace Lidarr.Api.V1.DownloadClient
|
||||
{
|
||||
public class DownloadClientResource : ProviderResource<DownloadClientResource>
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public bool RemoveCompletedDownloads { get; set; }
|
||||
public bool RemoveFailedDownloads { get; set; }
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Lidarr.Api.V1.Indexers
|
|||
public bool EnableInteractiveSearch { get; set; }
|
||||
public bool SupportsRss { get; set; }
|
||||
public bool SupportsSearch { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int DownloadClientId { get; set; }
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace Lidarr.Api.V1.Indexers
|
|||
public string InfoHash { get; set; }
|
||||
public int? Seeders { get; set; }
|
||||
public int? Leechers { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
|
||||
// Sent when queuing an unknown release
|
||||
|
@ -125,7 +125,7 @@ namespace Lidarr.Api.V1.Indexers
|
|||
{
|
||||
ReleaseInfo model;
|
||||
|
||||
if (resource.Protocol == DownloadProtocol.Torrent)
|
||||
if (resource.Protocol == nameof(TorrentDownloadProtocol))
|
||||
{
|
||||
model = new TorrentInfo
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using Lidarr.Http;
|
||||
using Lidarr.Http.REST;
|
||||
|
@ -21,16 +22,10 @@ namespace Lidarr.Api.V1.Profiles.Delay
|
|||
SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1);
|
||||
SharedValidator.RuleFor(d => d.Tags).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1);
|
||||
SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator);
|
||||
SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0);
|
||||
SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0);
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((delayProfile, context) =>
|
||||
{
|
||||
if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent)
|
||||
{
|
||||
context.AddFailure("Either Usenet or Torrent should be enabled");
|
||||
}
|
||||
});
|
||||
SharedValidator.RuleFor(d => d.Items).Cascade(CascadeMode.Stop)
|
||||
.NotNull()
|
||||
.Must(items => items.All(x => x.Delay >= 0)).WithMessage("Protocols cannot have a negative delay")
|
||||
.Must(items => items.Any(x => x.Allowed)).WithMessage("At least one protocol must be enabled");
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
using NzbDrone.Core.Profiles.Delay;
|
||||
|
||||
namespace Lidarr.Api.V1.Profiles.Delay
|
||||
{
|
||||
public class DelayProfileProtocolItemResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
public int Delay { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileItemResourceMapper
|
||||
{
|
||||
public static DelayProfileProtocolItemResource ToResource(this DelayProfileProtocolItem model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DelayProfileProtocolItemResource
|
||||
{
|
||||
Name = model.Name,
|
||||
Protocol = model.Protocol,
|
||||
Allowed = model.Allowed,
|
||||
Delay = model.Delay
|
||||
};
|
||||
}
|
||||
|
||||
public static DelayProfileProtocolItem ToModel(this DelayProfileProtocolItemResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DelayProfileProtocolItem
|
||||
{
|
||||
Name = resource.Name,
|
||||
Protocol = resource.Protocol,
|
||||
Allowed = resource.Allowed,
|
||||
Delay = resource.Delay
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
|
||||
namespace Lidarr.Api.V1.Profiles.Delay
|
||||
{
|
||||
public class DelayProfileResource : RestResource
|
||||
{
|
||||
public bool EnableUsenet { get; set; }
|
||||
public bool EnableTorrent { get; set; }
|
||||
public DownloadProtocol PreferredProtocol { get; set; }
|
||||
public int UsenetDelay { get; set; }
|
||||
public int TorrentDelay { get; set; }
|
||||
public string Name { get; set; }
|
||||
public List<DelayProfileProtocolItemResource> Items { get; set; }
|
||||
public bool BypassIfHighestQuality { get; set; }
|
||||
public bool BypassIfAboveCustomFormatScore { get; set; }
|
||||
public int MinimumCustomFormatScore { get; set; }
|
||||
|
@ -32,12 +28,8 @@ namespace Lidarr.Api.V1.Profiles.Delay
|
|||
return new DelayProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
EnableUsenet = model.EnableUsenet,
|
||||
EnableTorrent = model.EnableTorrent,
|
||||
PreferredProtocol = model.PreferredProtocol,
|
||||
UsenetDelay = model.UsenetDelay,
|
||||
TorrentDelay = model.TorrentDelay,
|
||||
Name = model.Name,
|
||||
Items = model.Items.Select(x => x.ToResource()).ToList(),
|
||||
BypassIfHighestQuality = model.BypassIfHighestQuality,
|
||||
BypassIfAboveCustomFormatScore = model.BypassIfAboveCustomFormatScore,
|
||||
MinimumCustomFormatScore = model.MinimumCustomFormatScore,
|
||||
|
@ -56,12 +48,8 @@ namespace Lidarr.Api.V1.Profiles.Delay
|
|||
return new DelayProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
EnableUsenet = resource.EnableUsenet,
|
||||
EnableTorrent = resource.EnableTorrent,
|
||||
PreferredProtocol = resource.PreferredProtocol,
|
||||
UsenetDelay = resource.UsenetDelay,
|
||||
TorrentDelay = resource.TorrentDelay,
|
||||
Name = resource.Name,
|
||||
Items = resource.Items.Select(x => x.ToModel()).ToList(),
|
||||
BypassIfHighestQuality = resource.BypassIfHighestQuality,
|
||||
BypassIfAboveCustomFormatScore = resource.BypassIfAboveCustomFormatScore,
|
||||
MinimumCustomFormatScore = resource.MinimumCustomFormatScore,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
using Lidarr.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
|
||||
namespace Lidarr.Api.V1.Profiles.Delay
|
||||
{
|
||||
[V1ApiController("delayprofile/schema")]
|
||||
public class DelayProfileSchemaController : Controller
|
||||
{
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
|
||||
public DelayProfileSchemaController(IDelayProfileService delayProfileService)
|
||||
{
|
||||
_delayProfileService = delayProfileService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public DelayProfileResource GetSchema()
|
||||
{
|
||||
return _delayProfileService.GetDefaultProfile().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ namespace Lidarr.Api.V1
|
|||
Tags = resource.Tags
|
||||
};
|
||||
|
||||
var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract);
|
||||
var configContract = ReflectionExtensions.FindTypeByName(definition.ConfigContract);
|
||||
definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract, existingDefinition?.Settings);
|
||||
|
||||
return definition;
|
||||
|
|
|
@ -13,7 +13,6 @@ using NzbDrone.Core.Datastore.Events;
|
|||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
@ -130,7 +129,7 @@ namespace Lidarr.Api.V1.Queue
|
|||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false, [FromQuery] int[] artistIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] quality = null)
|
||||
public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false, [FromQuery] int[] artistIds = null, string protocol = null, [FromQuery] int[] quality = null)
|
||||
{
|
||||
var pagingResource = new PagingResource<QueueResource>(paging);
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending);
|
||||
|
@ -138,7 +137,7 @@ namespace Lidarr.Api.V1.Queue
|
|||
return pagingSpec.ApplyToPage((spec) => GetQueue(spec, artistIds?.ToHashSet(), protocol, quality?.ToHashSet(), includeUnknownArtistItems), (q) => MapToResource(q, includeArtist, includeAlbum));
|
||||
}
|
||||
|
||||
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> artistIds, DownloadProtocol? protocol, HashSet<int> quality, bool includeUnknownArtistItems)
|
||||
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> artistIds, string protocol, HashSet<int> quality, bool includeUnknownArtistItems)
|
||||
{
|
||||
var ascending = pagingSpec.SortDirection == SortDirection.Ascending;
|
||||
var orderByFunc = GetOrderByFunc(pagingSpec);
|
||||
|
@ -159,9 +158,9 @@ namespace Lidarr.Api.V1.Queue
|
|||
include &= q.Artist != null && artistIds.Contains(q.Artist.Id);
|
||||
}
|
||||
|
||||
if (include && protocol.HasValue)
|
||||
if (include && protocol.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
include &= q.Protocol == protocol.Value;
|
||||
include &= q.Protocol == protocol;
|
||||
}
|
||||
|
||||
if (include && hasQualityFilter)
|
||||
|
|
|
@ -7,7 +7,6 @@ using Lidarr.Api.V1.CustomFormats;
|
|||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace Lidarr.Api.V1.Queue
|
||||
|
@ -33,7 +32,7 @@ namespace Lidarr.Api.V1.Queue
|
|||
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string DownloadClient { get; set; }
|
||||
public bool DownloadClientHasPostImportCategory { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
|
|
24
src/Lidarr.Api.V1/System/Plugins/PluginController.cs
Normal file
24
src/Lidarr.Api.V1/System/Plugins/PluginController.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using Lidarr.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Plugins;
|
||||
|
||||
namespace Lidarr.Api.V1.System.Plugins
|
||||
{
|
||||
[V1ApiController("system/plugins")]
|
||||
public class PluginController : Controller
|
||||
{
|
||||
private readonly IPluginService _pluginService;
|
||||
|
||||
public PluginController(IPluginService pluginService)
|
||||
{
|
||||
_pluginService = pluginService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<PluginResource> GetInstalledPlugins()
|
||||
{
|
||||
return _pluginService.GetInstalledPlugins().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
38
src/Lidarr.Api.V1/System/Plugins/PluginResource.cs
Normal file
38
src/Lidarr.Api.V1/System/Plugins/PluginResource.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lidarr.Http.REST;
|
||||
using NzbDrone.Core.Plugins;
|
||||
|
||||
namespace Lidarr.Api.V1.System.Plugins
|
||||
{
|
||||
public class PluginResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Owner { get; set; }
|
||||
public string GithubUrl { get; set; }
|
||||
public string InstalledVersion { get; set; }
|
||||
public string AvailableVersion { get; set; }
|
||||
public bool UpdateAvailable { get; set; }
|
||||
}
|
||||
|
||||
public static class PluginResourceMapper
|
||||
{
|
||||
public static PluginResource ToResource(this IPlugin plugin)
|
||||
{
|
||||
return new PluginResource
|
||||
{
|
||||
Name = plugin.Name,
|
||||
Owner = plugin.Owner,
|
||||
GithubUrl = plugin.GithubUrl,
|
||||
InstalledVersion = plugin.InstalledVersion.ToString(),
|
||||
AvailableVersion = plugin.AvailableVersion.ToString(),
|
||||
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
||||
};
|
||||
}
|
||||
|
||||
public static List<PluginResource> ToResource(this IEnumerable<IPlugin> plugins)
|
||||
{
|
||||
return plugins.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Reflection;
|
||||
|
@ -12,7 +11,7 @@ namespace NzbDrone.Common.Test.ReflectionTests
|
|||
[Test]
|
||||
public void should_get_properties_from_models()
|
||||
{
|
||||
var models = Assembly.Load("Lidarr.Core").ImplementationsOf<ModelBase>();
|
||||
var models = Reflection.ReflectionExtensions.ImplementationsOf<ModelBase>();
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
|
@ -23,7 +22,7 @@ namespace NzbDrone.Common.Test.ReflectionTests
|
|||
[Test]
|
||||
public void should_be_able_to_get_implementations()
|
||||
{
|
||||
var models = Assembly.Load("Lidarr.Core").ImplementationsOf<ModelBase>();
|
||||
var models = Reflection.ReflectionExtensions.ImplementationsOf<ModelBase>();
|
||||
|
||||
models.Should().NotBeEmpty();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
|
@ -15,7 +16,6 @@ using NzbDrone.Core.Datastore;
|
|||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Host;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Common.Test
|
||||
|
@ -26,9 +26,10 @@ namespace NzbDrone.Common.Test
|
|||
[Test]
|
||||
public void event_handlers_should_be_unique()
|
||||
{
|
||||
var assemblies = AssemblyLoader.LoadBaseAssemblies();
|
||||
var container = new Container(rules => rules.WithNzbDroneRules())
|
||||
.AddNzbDroneLogger()
|
||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||
.AutoAddServices(assemblies)
|
||||
.AddDummyDatabase()
|
||||
.AddDummyLogDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
|
|
|
@ -9,17 +9,38 @@ using NzbDrone.Common.EnvironmentInfo;
|
|||
|
||||
namespace NzbDrone.Common.Composition
|
||||
{
|
||||
public class AssemblyLoader
|
||||
public static class AssemblyLoader
|
||||
{
|
||||
private static readonly string[] BaseAssemblies =
|
||||
{
|
||||
"Lidarr.Host",
|
||||
"Lidarr.Core",
|
||||
"Lidarr.SignalR",
|
||||
"Lidarr.Api.V1",
|
||||
"Lidarr.Http"
|
||||
};
|
||||
|
||||
private static readonly string[] UpdateAssemblies = { "Lidarr.Update" };
|
||||
|
||||
static AssemblyLoader()
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
|
||||
RegisterSQLiteResolver();
|
||||
}
|
||||
|
||||
public static IList<Assembly> Load(IList<string> assemblyNames)
|
||||
public static List<Assembly> LoadBaseAssemblies()
|
||||
{
|
||||
var toLoad = assemblyNames.ToList();
|
||||
return Load(BaseAssemblies);
|
||||
}
|
||||
|
||||
public static List<Assembly> LoadUpdateAssemblies()
|
||||
{
|
||||
return Load(UpdateAssemblies);
|
||||
}
|
||||
|
||||
private static List<Assembly> Load(IList<string> assemblies)
|
||||
{
|
||||
var toLoad = assemblies.ToList();
|
||||
toLoad.Add("Lidarr.Common");
|
||||
toLoad.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono");
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using DryIoc;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
|
@ -20,10 +21,8 @@ namespace NzbDrone.Common.Composition.Extensions
|
|||
return container;
|
||||
}
|
||||
|
||||
public static IContainer AutoAddServices(this IContainer container, List<string> assemblyNames)
|
||||
public static IContainer AutoAddServices(this IContainer container, List<Assembly> assemblies)
|
||||
{
|
||||
var assemblies = AssemblyLoader.Load(assemblyNames);
|
||||
|
||||
container.RegisterMany(assemblies,
|
||||
serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"),
|
||||
reuse: Reuse.Singleton);
|
||||
|
@ -37,5 +36,17 @@ namespace NzbDrone.Common.Composition.Extensions
|
|||
|
||||
return container;
|
||||
}
|
||||
|
||||
public static IContainer SetPluginStatus(this IContainer container, bool enabled)
|
||||
{
|
||||
var pluginStatus = new PluginStatus
|
||||
{
|
||||
Enabled = enabled
|
||||
};
|
||||
|
||||
container.RegisterInstance(pluginStatus);
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
41
src/NzbDrone.Common/Composition/PluginLoadContext.cs
Normal file
41
src/NzbDrone.Common/Composition/PluginLoadContext.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace NzbDrone.Common.Composition
|
||||
{
|
||||
public class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private AssemblyDependencyResolver _resolver;
|
||||
|
||||
public PluginLoadContext(string pluginPath)
|
||||
: base(isCollectible: true)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
if (assemblyPath != null)
|
||||
{
|
||||
using var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read);
|
||||
return LoadFromStream(fs);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
if (libraryPath != null)
|
||||
{
|
||||
return LoadUnmanagedDllFromPath(libraryPath);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
81
src/NzbDrone.Common/Composition/PluginLoader.cs
Normal file
81
src/NzbDrone.Common/Composition/PluginLoader.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Common.Composition
|
||||
{
|
||||
public static class PluginLoader
|
||||
{
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PluginLoader));
|
||||
|
||||
public static (List<Assembly>, List<WeakReference>) LoadPlugins(IEnumerable<string> pluginPaths)
|
||||
{
|
||||
var assemblies = new List<Assembly>();
|
||||
var pluginRefs = new List<WeakReference>();
|
||||
|
||||
foreach (var pluginPath in pluginPaths)
|
||||
{
|
||||
(var plugin, var pluginRef) = LoadPlugin(pluginPath);
|
||||
pluginRefs.Add(pluginRef);
|
||||
assemblies.Add(plugin);
|
||||
}
|
||||
|
||||
return (assemblies, pluginRefs);
|
||||
}
|
||||
|
||||
public static bool UnloadPlugins(List<WeakReference> pluginRefs)
|
||||
{
|
||||
RequestPluginUnload(pluginRefs);
|
||||
return AwaitPluginUnload(pluginRefs);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static (Assembly, WeakReference) LoadPlugin(string path)
|
||||
{
|
||||
var context = new PluginLoadContext(path);
|
||||
var weakRef = new WeakReference(context, trackResurrection: true);
|
||||
|
||||
// load from stream to avoid locking on windows
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
var assembly = context.LoadFromStream(fs);
|
||||
|
||||
return (assembly, weakRef);
|
||||
}
|
||||
|
||||
private static void RequestPluginUnload(List<WeakReference> pluginRefs)
|
||||
{
|
||||
foreach (var pluginRef in pluginRefs)
|
||||
{
|
||||
if (pluginRef?.Target != null)
|
||||
{
|
||||
((PluginLoadContext)pluginRef.Target).Unload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AwaitPluginUnload(List<WeakReference> pluginRefs)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var pluginRef in pluginRefs.Where(x => x != null))
|
||||
{
|
||||
while (pluginRef.IsAlive)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
if (i++ >= 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
7
src/NzbDrone.Common/Composition/PluginStatus.cs
Normal file
7
src/NzbDrone.Common/Composition/PluginStatus.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace NzbDrone.Common.Composition
|
||||
{
|
||||
public class PluginStatus
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ namespace NzbDrone.Common.Extensions
|
|||
private const string LOG_DB = "logs.db";
|
||||
private const string NLOG_CONFIG_FILE = "nlog.config";
|
||||
private const string UPDATE_CLIENT_EXE_NAME = "Lidarr.Update";
|
||||
private const string PLUGIN_FOLDER_NAME = "plugins";
|
||||
|
||||
private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "lidarr_update" + Path.DirectorySeparatorChar;
|
||||
private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Lidarr" + Path.DirectorySeparatorChar;
|
||||
|
@ -314,6 +315,26 @@ namespace NzbDrone.Common.Extensions
|
|||
return Path.Combine(GetAppDataPath(appFolderInfo), APP_CONFIG_FILE);
|
||||
}
|
||||
|
||||
public static string GetPluginPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), PLUGIN_FOLDER_NAME);
|
||||
}
|
||||
|
||||
public static List<string> GetPluginAssemblies(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
var pluginFolder = appFolderInfo.GetPluginPath();
|
||||
|
||||
if (!Directory.Exists(pluginFolder))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
return Directory.GetDirectories(pluginFolder)
|
||||
.SelectMany(owner => Directory.GetDirectories(owner)
|
||||
.SelectMany(folder => Directory.GetFiles(folder, "Lidarr.Plugin.*.dll").ToList()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string GetMediaCoverPath(this IAppFolderInfo appFolderInfo)
|
||||
{
|
||||
return Path.Combine(GetAppDataPath(appFolderInfo), "MediaCover");
|
||||
|
|
|
@ -7,17 +7,15 @@ namespace NzbDrone.Common.Reflection
|
|||
{
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
public static readonly Assembly CoreAssembly = Assembly.Load("Lidarr.Core");
|
||||
|
||||
public static List<PropertyInfo> GetSimpleProperties(this Type type)
|
||||
{
|
||||
var properties = type.GetProperties();
|
||||
return properties.Where(c => c.PropertyType.IsSimpleType()).ToList();
|
||||
}
|
||||
|
||||
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
|
||||
public static List<Type> ImplementationsOf<T>()
|
||||
{
|
||||
return assembly.GetExportedTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
|
||||
return GetAllTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
|
||||
}
|
||||
|
||||
public static bool IsSimpleType(this Type type)
|
||||
|
@ -71,6 +69,32 @@ namespace NzbDrone.Common.Reflection
|
|||
return assembly.GetExportedTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public static Type FindTypeByName(string name)
|
||||
{
|
||||
return GetAllTypes()
|
||||
.SingleOrDefault(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetAllTypes()
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
return assemblies
|
||||
.Where(x => ShouldUseAssembly(x))
|
||||
.SelectMany(x => x.GetExportedTypes());
|
||||
}
|
||||
|
||||
private static bool ShouldUseAssembly(Assembly assembly)
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = assembly.GetName();
|
||||
return name.Name == "Lidarr.Core" || name.Name.Contains("Lidarr.Plugin");
|
||||
}
|
||||
|
||||
public static bool HasAttribute<TAttribute>(this Type type)
|
||||
{
|
||||
return type.GetCustomAttributes(typeof(TAttribute), true).Any();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
|
@ -30,8 +31,10 @@ namespace NzbDrone.Core.Test.Blocklisting
|
|||
_event.Data.Add("publishedDate", DateTime.UtcNow.ToString("s") + "Z");
|
||||
_event.Data.Add("size", "1000");
|
||||
_event.Data.Add("indexer", "nzbs.org");
|
||||
_event.Data.Add("protocol", "1");
|
||||
_event.Data.Add("protocol", nameof(UsenetDownloadProtocol));
|
||||
_event.Data.Add("message", "Marked as failed");
|
||||
|
||||
Mocker.SetConstant<IBlocklistForProtocol>(Mocker.Resolve<UsenetBlocklist>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
|
||||
|
||||
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
|
||||
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||
.With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
|
||||
.With(t => t.InfoHash = downloadId)
|
||||
.Build();
|
||||
|
||||
|
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
|
||||
|
||||
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
|
||||
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||
.With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
|
||||
.With(t => t.InfoHash = null)
|
||||
.Build();
|
||||
|
||||
|
@ -174,7 +174,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
GivenHistoryItem(null, TITLE, _flac, EntityHistoryEventType.DownloadImported);
|
||||
|
||||
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
|
||||
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||
.With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
|
||||
.With(t => t.InfoHash = null)
|
||||
.Build();
|
||||
|
||||
|
@ -190,7 +190,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
|
||||
|
||||
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
|
||||
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||
.With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
|
||||
.With(t => t.InfoHash = downloadId)
|
||||
.Build();
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
_remoteAlbum = new RemoteAlbum
|
||||
{
|
||||
Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Usenet }
|
||||
Release = new ReleaseInfo() { DownloadProtocol = nameof(UsenetDownloadProtocol) }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||
GivenPreferredDownloadProtocol(nameof(UsenetDownloadProtocol));
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(s => s.Get(It.IsAny<Quality>()))
|
||||
|
@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
.Build();
|
||||
}
|
||||
|
||||
private RemoteAlbum GivenRemoteAlbum(List<Album> albums, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet, int indexerPriority = 25)
|
||||
private RemoteAlbum GivenRemoteAlbum(List<Album> albums, QualityModel quality, int age = 0, long size = 0, string downloadProtocol = "UsenetDownloadProtocol", int indexerPriority = 25)
|
||||
{
|
||||
var remoteAlbum = new RemoteAlbum();
|
||||
remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
|
||||
|
@ -78,14 +78,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
return remoteAlbum;
|
||||
}
|
||||
|
||||
private void GivenPreferredDownloadProtocol(DownloadProtocol downloadProtocol)
|
||||
private void GivenPreferredDownloadProtocol(string downloadProtocol)
|
||||
{
|
||||
var profile = new DelayProfile();
|
||||
profile.Items = profile.Items.OrderByDescending(x => x.Protocol == downloadProtocol).ToList();
|
||||
|
||||
Mocker.GetMock<IDelayProfileService>()
|
||||
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||
.Returns(new DelayProfile
|
||||
{
|
||||
PreferredProtocol = downloadProtocol
|
||||
});
|
||||
.Returns(profile);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -218,33 +218,33 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_put_usenet_above_torrent_when_usenet_is_preferred()
|
||||
{
|
||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||
GivenPreferredDownloadProtocol(nameof(UsenetDownloadProtocol));
|
||||
|
||||
var remoteAlbum1 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent);
|
||||
var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet);
|
||||
var remoteAlbum1 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(TorrentDownloadProtocol));
|
||||
var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(UsenetDownloadProtocol));
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteAlbum1));
|
||||
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||
|
||||
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
|
||||
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(nameof(UsenetDownloadProtocol));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_put_torrent_above_usenet_when_torrent_is_preferred()
|
||||
{
|
||||
GivenPreferredDownloadProtocol(DownloadProtocol.Torrent);
|
||||
GivenPreferredDownloadProtocol(nameof(TorrentDownloadProtocol));
|
||||
|
||||
var remoteAlbum1 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent);
|
||||
var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet);
|
||||
var remoteAlbum1 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(TorrentDownloadProtocol));
|
||||
var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(UsenetDownloadProtocol));
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteAlbum1));
|
||||
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||
|
||||
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -302,7 +302,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
var torrentInfo1 = new TorrentInfo();
|
||||
torrentInfo1.PublishDate = DateTime.Now;
|
||||
torrentInfo1.Size = 0;
|
||||
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
torrentInfo1.Seeders = 10;
|
||||
|
||||
var torrentInfo2 = torrentInfo1.JsonClone();
|
||||
|
@ -328,7 +328,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
var torrentInfo1 = new TorrentInfo();
|
||||
torrentInfo1.PublishDate = DateTime.Now;
|
||||
torrentInfo1.Size = 0;
|
||||
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
torrentInfo1.Seeders = 10;
|
||||
torrentInfo1.Peers = 10;
|
||||
|
||||
|
@ -355,7 +355,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
var torrentInfo1 = new TorrentInfo();
|
||||
torrentInfo1.PublishDate = DateTime.Now;
|
||||
torrentInfo1.Size = 0;
|
||||
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
torrentInfo1.Seeders = 0;
|
||||
torrentInfo1.Peers = 10;
|
||||
|
||||
|
@ -382,7 +382,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
var torrentInfo1 = new TorrentInfo();
|
||||
torrentInfo1.PublishDate = DateTime.Now;
|
||||
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
torrentInfo1.Seeders = 1000;
|
||||
torrentInfo1.Peers = 10;
|
||||
torrentInfo1.Size = 200.Megabytes();
|
||||
|
@ -431,7 +431,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
var torrentInfo1 = new TorrentInfo();
|
||||
torrentInfo1.PublishDate = DateTime.Now;
|
||||
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
torrentInfo1.Seeders = 100;
|
||||
torrentInfo1.Peers = 10;
|
||||
torrentInfo1.Size = 200.Megabytes();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
@ -25,13 +26,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
_remoteAlbum.Artist = new Artist();
|
||||
|
||||
_delayProfile = new DelayProfile();
|
||||
_delayProfile.Items.ForEach(x => x.Allowed = false);
|
||||
|
||||
Mocker.GetMock<IDelayProfileService>()
|
||||
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||
.Returns(_delayProfile);
|
||||
}
|
||||
|
||||
private void GivenProtocol(DownloadProtocol downloadProtocol)
|
||||
private void GivenProtocol(string downloadProtocol)
|
||||
{
|
||||
_remoteAlbum.Release.DownloadProtocol = downloadProtocol;
|
||||
}
|
||||
|
@ -39,8 +41,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_be_true_if_usenet_and_usenet_is_enabled()
|
||||
{
|
||||
GivenProtocol(DownloadProtocol.Usenet);
|
||||
_delayProfile.EnableUsenet = true;
|
||||
GivenProtocol(nameof(UsenetDownloadProtocol));
|
||||
_delayProfile.Items.Single(x => x.Protocol == nameof(UsenetDownloadProtocol)).Allowed = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true);
|
||||
}
|
||||
|
@ -48,8 +50,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_be_true_if_torrent_and_torrent_is_enabled()
|
||||
{
|
||||
GivenProtocol(DownloadProtocol.Torrent);
|
||||
_delayProfile.EnableTorrent = true;
|
||||
GivenProtocol(nameof(TorrentDownloadProtocol));
|
||||
_delayProfile.Items.Single(x => x.Protocol == nameof(TorrentDownloadProtocol)).Allowed = true;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true);
|
||||
}
|
||||
|
@ -57,8 +59,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_be_false_if_usenet_and_usenet_is_disabled()
|
||||
{
|
||||
GivenProtocol(DownloadProtocol.Usenet);
|
||||
_delayProfile.EnableUsenet = false;
|
||||
GivenProtocol(nameof(UsenetDownloadProtocol));
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
@ -66,8 +67,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_be_false_if_torrent_and_torrent_is_disabled()
|
||||
{
|
||||
GivenProtocol(DownloadProtocol.Torrent);
|
||||
_delayProfile.EnableTorrent = false;
|
||||
GivenProtocol(nameof(TorrentDownloadProtocol));
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
_remoteAlbum = new RemoteAlbum
|
||||
{
|
||||
Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Torrent }
|
||||
Release = new ReleaseInfo() { DownloadProtocol = nameof(TorrentDownloadProtocol) }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
_remoteAlbum = new RemoteAlbum
|
||||
{
|
||||
Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Usenet }
|
||||
Release = new ReleaseInfo() { DownloadProtocol = nameof(UsenetDownloadProtocol) }
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_true_when_release_is_not_usenet()
|
||||
{
|
||||
_remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
_remoteAlbum.Release.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
|
||||
WithRetention(10);
|
||||
WithAge(100);
|
||||
|
|
|
@ -34,9 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_profile = Builder<QualityProfile>.CreateNew()
|
||||
.Build();
|
||||
|
||||
_delayProfile = Builder<DelayProfile>.CreateNew()
|
||||
.With(d => d.PreferredProtocol = DownloadProtocol.Usenet)
|
||||
.Build();
|
||||
_delayProfile = new DelayProfile();
|
||||
|
||||
var artist = Builder<Artist>.CreateNew()
|
||||
.With(s => s.QualityProfile = _profile)
|
||||
|
@ -55,7 +53,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
|
||||
_remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
|
||||
_remoteAlbum.Release = new ReleaseInfo();
|
||||
_remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Usenet;
|
||||
_remoteAlbum.Release.DownloadProtocol = nameof(UsenetDownloadProtocol);
|
||||
|
||||
_remoteAlbum.Albums = Builder<Album>.CreateListOfSize(1).Build().ToList();
|
||||
|
||||
|
@ -104,7 +102,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192);
|
||||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, new AlbumSearchCriteria()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
@ -112,7 +110,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
[Test]
|
||||
public void should_be_true_when_profile_does_not_have_a_delay()
|
||||
{
|
||||
_delayProfile.UsenetDelay = 0;
|
||||
_delayProfile.Items[0].Delay = 0;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
@ -123,7 +121,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320);
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
@ -131,7 +129,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
[Test]
|
||||
public void should_be_true_when_quality_is_last_allowed_in_profile_and_bypass_enabled()
|
||||
{
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
_delayProfile.BypassIfHighestQuality = true;
|
||||
|
||||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
|
@ -146,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256);
|
||||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow.AddHours(-10);
|
||||
|
||||
_delayProfile.UsenetDelay = 60;
|
||||
_delayProfile.Items[0].Delay = 60;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
@ -157,7 +155,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192);
|
||||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
@ -175,7 +173,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
@ -193,7 +191,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
@ -206,7 +204,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
|
||||
GivenExistingFile(new QualityModel(Quality.MP3_256));
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
@ -217,7 +215,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteAlbum.CustomFormatScore = 100;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
|
@ -229,7 +227,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteAlbum.CustomFormatScore = 5;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
_delayProfile.BypassIfAboveCustomFormatScore = true;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
|
@ -242,7 +240,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteAlbum.CustomFormatScore = 100;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.Items[0].Delay = 720;
|
||||
_delayProfile.BypassIfAboveCustomFormatScore = true;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||
.Build();
|
||||
}
|
||||
|
||||
private RemoteAlbum GetRemoteAlbum(List<Album> albums, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
|
||||
private RemoteAlbum GetRemoteAlbum(List<Album> albums, QualityModel quality, string downloadProtocol = "UsenetDownloadProtocol")
|
||||
{
|
||||
var remoteAlbum = new RemoteAlbum();
|
||||
remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
|
||||
|
@ -253,19 +253,19 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
|||
public async Task should_not_add_to_failed_if_failed_for_a_different_protocol()
|
||||
{
|
||||
var albums = new List<Album> { GetAlbum(1) };
|
||||
var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Usenet);
|
||||
var remoteAlbum2 = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Torrent);
|
||||
var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), nameof(UsenetDownloadProtocol));
|
||||
var remoteAlbum2 = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), nameof(TorrentDownloadProtocol));
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteAlbum));
|
||||
decisions.Add(new DownloadDecision(remoteAlbum2));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null))
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(UsenetDownloadProtocol)), null))
|
||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent), null), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(UsenetDownloadProtocol)), null), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(TorrentDownloadProtocol)), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
_downloadClients.Add(mock.Object);
|
||||
|
||||
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet);
|
||||
mock.SetupGet(v => v.Protocol).Returns(nameof(UsenetDownloadProtocol));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
_downloadClients.Add(mock.Object);
|
||||
|
||||
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent);
|
||||
mock.SetupGet(v => v.Protocol).Returns(nameof(TorrentDownloadProtocol));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
@ -98,11 +98,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithUsenetClient();
|
||||
WithTorrentClient();
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client1 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
var client5 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(1);
|
||||
client2.Definition.Id.Should().Be(2);
|
||||
|
@ -119,11 +119,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentClient();
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(2);
|
||||
client2.Definition.Id.Should().Be(3);
|
||||
|
@ -139,10 +139,10 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentClient();
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(1);
|
||||
client2.Definition.Id.Should().Be(2);
|
||||
|
@ -161,10 +161,10 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentClient(0, clientTags);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
|
||||
client1.Definition.Id.Should().Be(2);
|
||||
client2.Definition.Id.Should().Be(4);
|
||||
|
@ -183,10 +183,10 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentClient(0, clientTags);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
|
||||
|
||||
client1.Definition.Id.Should().Be(1);
|
||||
client2.Definition.Id.Should().Be(3);
|
||||
|
@ -202,7 +202,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
WithTorrentClient(0, clientTags);
|
||||
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags));
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -215,10 +215,10 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
GivenBlockedClient(3);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(2);
|
||||
client2.Definition.Id.Should().Be(4);
|
||||
|
@ -238,11 +238,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
GivenBlockedClient(3);
|
||||
GivenBlockedClient(4);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(2);
|
||||
client2.Definition.Id.Should().Be(3);
|
||||
|
@ -258,11 +258,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentClient();
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(3);
|
||||
client2.Definition.Id.Should().Be(4);
|
||||
|
@ -280,11 +280,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
GivenBlockedClient(4);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
|
||||
|
||||
client1.Definition.Id.Should().Be(2);
|
||||
client2.Definition.Id.Should().Be(3);
|
||||
|
@ -301,11 +301,11 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentIndexer(3);
|
||||
|
||||
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
|
||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
|
||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
|
||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
|
||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
|
||||
var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
|
||||
var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
|
||||
var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
|
||||
var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
|
||||
var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
|
||||
|
||||
client1.Definition.Id.Should().Be(3);
|
||||
client2.Definition.Id.Should().Be(3);
|
||||
|
@ -323,7 +323,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
WithTorrentClient();
|
||||
WithTorrentIndexer(5);
|
||||
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 1));
|
||||
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ namespace NzbDrone.Core.Test.Download
|
|||
.Returns(_downloadClients);
|
||||
|
||||
Mocker.GetMock<IProvideDownloadClient>()
|
||||
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<HashSet<int>>()))
|
||||
.Returns<DownloadProtocol, int, bool, HashSet<int>>((v, i, f, t) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
|
||||
.Setup(v => v.GetDownloadClient(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<HashSet<int>>()))
|
||||
.Returns<string, int, bool, HashSet<int>>((v, i, f, t) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
|
||||
|
||||
var episodes = Builder<Album>.CreateListOfSize(2)
|
||||
.TheFirst(1).With(s => s.Id = 12)
|
||||
|
@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
.Build().ToList();
|
||||
|
||||
var releaseInfo = Builder<ReleaseInfo>.CreateNew()
|
||||
.With(v => v.DownloadProtocol = DownloadProtocol.Usenet)
|
||||
.With(v => v.DownloadProtocol = nameof(UsenetDownloadProtocol))
|
||||
.With(v => v.DownloadUrl = "http://test.site/download1.ext")
|
||||
.Build();
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
_downloadClients.Add(mock.Object);
|
||||
|
||||
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet);
|
||||
mock.SetupGet(v => v.Protocol).Returns(nameof(UsenetDownloadProtocol));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
|
||||
_downloadClients.Add(mock.Object);
|
||||
|
||||
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent);
|
||||
mock.SetupGet(v => v.Protocol).Returns(nameof(TorrentDownloadProtocol));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ namespace NzbDrone.Core.Test.Download
|
|||
var mockTorrent = WithTorrentClient();
|
||||
var mockUsenet = WithUsenetClient();
|
||||
|
||||
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
_parseResult.Release.DownloadProtocol = nameof(TorrentDownloadProtocol);
|
||||
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
Protocol = nameof(TorrentDownloadProtocol)
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
|
@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
Protocol = nameof(TorrentDownloadProtocol)
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
|
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
Protocol = nameof(TorrentDownloadProtocol)
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
|
@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
Protocol = nameof(TorrentDownloadProtocol)
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
|
@ -266,7 +266,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
|||
var client = new DownloadClientDefinition()
|
||||
{
|
||||
Id = 1,
|
||||
Protocol = DownloadProtocol.Torrent
|
||||
Protocol = nameof(TorrentDownloadProtocol)
|
||||
};
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
|
|
|
@ -10,6 +10,7 @@ using NzbDrone.Core.Configuration;
|
|||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.HealthCheck.Checks;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
|
@ -41,7 +42,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
|||
{
|
||||
_downloadItem = new DownloadClientItem
|
||||
{
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo { Name = "Test" },
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo
|
||||
{
|
||||
Protocol = nameof(UsenetDownloadProtocol),
|
||||
Id = 1,
|
||||
Name = "Test"
|
||||
},
|
||||
DownloadId = "TestId",
|
||||
OutputPath = new OsPath(_downloadItemPath)
|
||||
};
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.HistoryTests
|
|||
{
|
||||
DownloadClientInfo = new DownloadClientItemClientInfo
|
||||
{
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Protocol = nameof(UsenetDownloadProtocol),
|
||||
Id = 1,
|
||||
Name = "sab"
|
||||
},
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
|||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("Mankind.Divided.2019.S01E01.1080p.WEB-DL");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
torrentInfo.DownloadUrl.Should().Be("https://filelist.io/download.php?id=1234&passkey=somepass");
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=1234");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleTests
|
|||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Shania Twain - Shania Twain (1993) [FLAC 24bit Lossless] [WEB]");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
releaseInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
releaseInfo.DownloadUrl.Should()
|
||||
.Be("http://someurl.ch/torrents.php?action=download&id=1541452&authkey=redacted&torrent_pass=redacted");
|
||||
releaseInfo.InfoUrl.Should().Be("http://someurl.ch/torrents.php?id=106951&torrentid=1541452");
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests
|
|||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Lady Gaga Born This Way 2CD FLAC 2011 WRE");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
|
||||
releaseInfo.DownloadProtocol.Should().Be(nameof(UsenetDownloadProtocol));
|
||||
releaseInfo.DownloadUrl.Should().Be("https://indexer.codeshy.com/api?t=g&guid=123456&apikey=123456789");
|
||||
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2013/06/02 08:58:54"));
|
||||
|
|
|
@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
|
|||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("24 S03E12 720p WEBRip h264-DRAWER");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
torrentInfo.DownloadUrl.Should().Be("http://iptorrents.com/download.php/1234/24.S03E12.720p.WEBRip.h264-DRAWER.torrent?torrent_pass=abcd");
|
||||
torrentInfo.InfoUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
|
|||
|
||||
ValidateResult(reports, hasSize, hasInfoUrl);
|
||||
|
||||
reports.Should().OnlyContain(c => c.DownloadProtocol == DownloadProtocol.Torrent);
|
||||
reports.Should().OnlyContain(c => c.DownloadProtocol == nameof(TorrentDownloadProtocol));
|
||||
|
||||
if (hasMagnet)
|
||||
{
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Brainstorm-Scary Creatures-CD-FLAC-2016-NBFLAC");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
|
||||
releaseInfo.DownloadProtocol.Should().Be(nameof(UsenetDownloadProtocol));
|
||||
releaseInfo.DownloadUrl.Should().Be("http://api.nzbgeek.info/api?t=get&id=38884827e1e56b9336278a449e0a38ec&apikey=xxx");
|
||||
releaseInfo.InfoUrl.Should().Be("https://nzbgeek.info/geekseek.php?guid=38884827e1e56b9336278a449e0a38ec");
|
||||
releaseInfo.CommentUrl.Should().Be("https://nzbgeek.info/geekseek.php?guid=38884827e1e56b9336278a449e0a38ec");
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
|||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("[TSRaws] Futsuu no Joshikousei ga [Locodol] Yattemita. #07 (TBS).ts");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
torrentInfo.DownloadUrl.Should().Be("https://www.nyaa.se/?page=download&tid=587750");
|
||||
torrentInfo.InfoUrl.Should().Be("https://www.nyaa.se/?page=view&tid=587750");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
|
@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
|||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
torrentInfo.DownloadUrl.Should().Be("https://nyaa.si/download/1424896.torrent");
|
||||
torrentInfo.InfoUrl.Should().Be("https://nyaa.si/view/1424896");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
|
|||
var releaseInfo = releases.First();
|
||||
|
||||
releaseInfo.Title.Should().Be("Shania Twain - Shania Twain (1993) [Album] [FLAC 24bit Lossless / WEB]");
|
||||
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
releaseInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
|
||||
releaseInfo.DownloadUrl.Should().Be("https://redacted.sh/ajax.php?action=download&id=1541452");
|
||||
releaseInfo.InfoUrl.Should().Be("https://redacted.sh/torrents.php?id=106951&torrentid=1541452");
|
||||
releaseInfo.CommentUrl.Should().Be(null);
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||
{
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
DownloadProtocol = DownloadProtocol.Torrent,
|
||||
DownloadProtocol = nameof(TorrentDownloadProtocol),
|
||||
IndexerId = 0
|
||||
}
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||
{
|
||||
Release = new ReleaseInfo()
|
||||
{
|
||||
DownloadProtocol = DownloadProtocol.Torrent,
|
||||
DownloadProtocol = nameof(TorrentDownloadProtocol),
|
||||
IndexerId = 1
|
||||
},
|
||||
ParsedAlbumInfo = new ParsedAlbumInfo
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||
{
|
||||
public override string Name => "Test Indexer";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
public override string Protocol => nameof(UsenetDownloadProtocol);
|
||||
|
||||
public int _supportedPageSize;
|
||||
public override int PageSize => _supportedPageSize;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue