New: Plugin support

pull/1829/head
ta264 2 years ago committed by Bogdan
parent cfb517a90f
commit 9531bec8c2

@ -46,7 +46,7 @@ class BlocklistDetailsModal extends Component {
<DescriptionListItem <DescriptionListItem
title={translate('Protocol')} title={translate('Protocol')}
data={protocol} data={protocol.replace('DownloadProtocol', '')}
/> />
{ {

@ -4,7 +4,8 @@ import Label from 'Components/Label';
import styles from './ProtocolLabel.css'; import styles from './ProtocolLabel.css';
function ProtocolLabel({ protocol }) { function ProtocolLabel({ protocol }) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol; const strippedName = protocol.replace('DownloadProtocol', '').toLowerCase();
const protocolName = strippedName === 'usenet' ? 'nzb' : strippedName;
return ( return (
<Label className={styles[protocol]}> <Label className={styles[protocol]}>

@ -27,6 +27,7 @@ import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import BackupsConnector from 'System/Backup/BackupsConnector'; import BackupsConnector from 'System/Backup/BackupsConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector'; import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs'; import Logs from 'System/Logs/Logs';
import PluginsConnector from 'System/Plugins/PluginsConnector';
import Status from 'System/Status/Status'; import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks'; import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates'; import Updates from 'System/Updates/Updates';
@ -251,6 +252,11 @@ function AppRoutes(props) {
component={Updates} component={Updates}
/> />
<Route
path="/system/plugins"
component={PluginsConnector}
/>
<Route <Route
path="/system/events" path="/system/events"
component={LogsTableConnector} component={LogsTableConnector}

@ -8,6 +8,7 @@ export const DELETE_LOG_FILES = 'DeleteLogFiles';
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles'; export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan'; export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan';
export const ALBUM_SEARCH = 'AlbumSearch'; export const ALBUM_SEARCH = 'AlbumSearch';
export const INSTALL_PLUGIN = 'InstallPlugin';
export const INTERACTIVE_IMPORT = 'ManualImport'; export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch'; export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch';
export const MOVE_ARTIST = 'MoveArtist'; export const MOVE_ARTIST = 'MoveArtist';
@ -22,3 +23,4 @@ export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync'; export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'AlbumSearch'; export const SEASON_SEARCH = 'AlbumSearch';
export const ARTIST_SEARCH = 'ArtistSearch'; 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) { componentDidUpdate(prevProps) {
const { const {
id, id,
@ -222,9 +229,12 @@ FilterBuilderModalContent.propTypes = {
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
isPopulated: PropTypes.bool.isRequired,
dispatchDeleteCustomFilter: PropTypes.func.isRequired, dispatchDeleteCustomFilter: PropTypes.func.isRequired,
onSaveCustomFilterPress: PropTypes.func.isRequired, onSaveCustomFilterPress: PropTypes.func.isRequired,
dispatchSetFilter: PropTypes.func.isRequired, dispatchSetFilter: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired,
onCancelPress: PropTypes.func.isRequired, onCancelPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteCustomFilter, saveCustomFilter } from 'Store/Actions/customFilterActions'; import { deleteCustomFilter, saveCustomFilter } from 'Store/Actions/customFilterActions';
import { fetchDownloadClients, fetchIndexers } from 'Store/Actions/settingsActions';
import FilterBuilderModalContent from './FilterBuilderModalContent'; import FilterBuilderModalContent from './FilterBuilderModalContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -9,7 +10,11 @@ function createMapStateToProps() {
(state, { id }) => id, (state, { id }) => id,
(state) => state.customFilters.isSaving, (state) => state.customFilters.isSaving,
(state) => state.customFilters.saveError, (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) { if (id) {
const customFilter = customFilters.find((c) => c.id === id); const customFilter = customFilters.find((c) => c.id === id);
@ -19,7 +24,8 @@ function createMapStateToProps() {
filters: customFilter.filters, filters: customFilter.filters,
customFilters, customFilters,
isSaving, isSaving,
saveError saveError,
isPopulated
}; };
} }
@ -28,7 +34,8 @@ function createMapStateToProps() {
filters: [], filters: [],
customFilters, customFilters,
isSaving, isSaving,
saveError saveError,
isPopulated
}; };
} }
); );
@ -36,7 +43,9 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
onSaveCustomFilterPress: saveCustomFilter, onSaveCustomFilterPress: saveCustomFilter,
dispatchDeleteCustomFilter: deleteCustomFilter dispatchDeleteCustomFilter: deleteCustomFilter,
dispatchFetchDownloadClients: fetchDownloadClients,
dispatchFetchIndexers: fetchIndexers
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent); export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);

@ -13,7 +13,7 @@ import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuild
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue'; import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue';
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue'; import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValueConnector';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; 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'), title: () => translate('Updates'),
to: '/system/updates' to: '/system/updates'
}, },
{
title: () => translate('Plugins'),
to: '/system/plugins'
},
{ {
title: () => translate('Events'), title: () => translate('Events'),
to: '/system/events' to: '/system/events'

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Component } from 'react'; import { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; 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 { fetchArtist } from 'Store/Actions/artistActions';
import { removeItem, update, updateItem } from 'Store/Actions/baseActions'; import { removeItem, update, updateItem } from 'Store/Actions/baseActions';
import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions'; import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions';
@ -40,6 +40,7 @@ const mapDispatchToProps = {
dispatchFetchCommands: fetchCommands, dispatchFetchCommands: fetchCommands,
dispatchUpdateCommand: updateCommand, dispatchUpdateCommand: updateCommand,
dispatchFinishCommand: finishCommand, dispatchFinishCommand: finishCommand,
dispatchClearMessages: clearMessages,
dispatchSetAppValue: setAppValue, dispatchSetAppValue: setAppValue,
dispatchSetVersion: setVersion, dispatchSetVersion: setVersion,
dispatchUpdate: update, dispatchUpdate: update,
@ -340,6 +341,7 @@ class SignalRConnector extends Component {
const { const {
dispatchFetchCommands, dispatchFetchCommands,
dispatchFetchArtist, dispatchFetchArtist,
dispatchClearMessages,
dispatchSetAppValue dispatchSetAppValue
} = this.props; } = this.props;
@ -353,7 +355,9 @@ class SignalRConnector extends Component {
// Repopulate the page (if a repopulator is set) to ensure things // Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting. // are in sync after reconnecting.
dispatchFetchArtist(); dispatchFetchArtist();
dispatchClearMessages();
dispatchFetchCommands(); dispatchFetchCommands();
repopulatePage(); repopulatePage();
}; };
@ -382,6 +386,7 @@ SignalRConnector.propTypes = {
dispatchFetchCommands: PropTypes.func.isRequired, dispatchFetchCommands: PropTypes.func.isRequired,
dispatchUpdateCommand: PropTypes.func.isRequired, dispatchUpdateCommand: PropTypes.func.isRequired,
dispatchFinishCommand: PropTypes.func.isRequired, dispatchFinishCommand: PropTypes.func.isRequired,
dispatchClearMessages: PropTypes.func.isRequired,
dispatchSetAppValue: PropTypes.func.isRequired, dispatchSetAppValue: PropTypes.func.isRequired,
dispatchSetVersion: PropTypes.func.isRequired, dispatchSetVersion: PropTypes.func.isRequired,
dispatchUpdate: PropTypes.func.isRequired, dispatchUpdate: PropTypes.func.isRequired,

@ -5,14 +5,14 @@ import sortByProp from 'Utilities/Array/sortByProp';
import Label from './Label'; import Label from './Label';
import styles from './TagList.css'; import styles from './TagList.css';
function TagList({ tags, tagList }) { function TagList({ className, tags, tagList }) {
const sortedTags = tags const sortedTags = tags
.map((tagId) => tagList.find((tag) => tag.id === tagId)) .map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((tag) => !!tag) .filter((tag) => !!tag)
.sort(sortByProp('label')); .sort(sortByProp('label'));
return ( return (
<div className={styles.tags}> <div className={className}>
{ {
sortedTags.map((tag) => { sortedTags.map((tag) => {
return ( return (
@ -30,8 +30,13 @@ function TagList({ tags, tagList }) {
} }
TagList.propTypes = { TagList.propTypes = {
className: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired tagList: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
TagList.defaultProps = {
className: styles.tags
};
export default TagList; export default TagList;

@ -1,3 +1,4 @@
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem'; export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
export const DELAY_PROFILE = 'delayProfile'; export const DELAY_PROFILE = 'delayProfile';
export const DOWNLOAD_PROTOCOL_ITEM = 'downloadProtocolItem';
export const TABLE_COLUMN = 'tableColumn'; export const TABLE_COLUMN = 'tableColumn';

@ -171,7 +171,7 @@ class InteractiveSearchRow extends Component {
<TableRowCell className={styles.peers}> <TableRowCell className={styles.peers}>
{ {
protocol === 'torrent' && protocol === 'TorrentDownloadProtocol' &&
<Peers <Peers
seeders={seeders} seeders={seeders}
leechers={leechers} leechers={leechers}

@ -13,6 +13,19 @@ import translate from 'Utilities/String/translate';
import AddDownloadClientItem from './AddDownloadClientItem'; import AddDownloadClientItem from './AddDownloadClientItem';
import styles from './AddDownloadClientModalContent.css'; 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 { class AddDownloadClientModalContent extends Component {
// //
@ -25,6 +38,7 @@ class AddDownloadClientModalContent extends Component {
schemaError, schemaError,
usenetDownloadClients, usenetDownloadClients,
torrentDownloadClients, torrentDownloadClients,
otherDownloadClients,
onDownloadClientSelect, onDownloadClientSelect,
onModalClose onModalClose
} = this.props; } = this.props;
@ -64,16 +78,7 @@ class AddDownloadClientModalContent extends Component {
<FieldSet legend={translate('Usenet')}> <FieldSet legend={translate('Usenet')}>
<div className={styles.downloadClients}> <div className={styles.downloadClients}>
{ {
usenetDownloadClients.map((downloadClient) => { mapDownloadClients(usenetDownloadClients, onDownloadClientSelect)
return (
<AddDownloadClientItem
key={downloadClient.implementation}
implementation={downloadClient.implementation}
{...downloadClient}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})
} }
</div> </div>
</FieldSet> </FieldSet>
@ -81,19 +86,22 @@ class AddDownloadClientModalContent extends Component {
<FieldSet legend={translate('Torrents')}> <FieldSet legend={translate('Torrents')}>
<div className={styles.downloadClients}> <div className={styles.downloadClients}>
{ {
torrentDownloadClients.map((downloadClient) => { mapDownloadClients(torrentDownloadClients, onDownloadClientSelect)
return (
<AddDownloadClientItem
key={downloadClient.implementation}
implementation={downloadClient.implementation}
{...downloadClient}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})
} }
</div> </div>
</FieldSet> </FieldSet>
{
otherDownloadClients.length ?
<FieldSet legend="Other">
<div className={styles.downloadClients}>
{
mapDownloadClients(otherDownloadClients, onDownloadClientSelect)
}
</div>
</FieldSet> :
null
}
</div> </div>
} }
</ModalBody> </ModalBody>
@ -115,6 +123,7 @@ AddDownloadClientModalContent.propTypes = {
schemaError: PropTypes.object, schemaError: PropTypes.object,
usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
otherDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
onDownloadClientSelect: PropTypes.func.isRequired, onDownloadClientSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -17,15 +17,18 @@ function createMapStateToProps() {
schema schema
} = downloadClients; } = downloadClients;
const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' }); const usenetDownloadClients = _.filter(schema, { protocol: 'UsenetDownloadProtocol' });
const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' }); const torrentDownloadClients = _.filter(schema, { protocol: 'TorrentDownloadProtocol' });
const otherDownloadClients = _.filter(schema, (x) => x.protocol !== 'UsenetDownloadProtocol' &&
x.protocol !== 'TorrentDownloadProtocol');
return { return {
isSchemaFetching, isSchemaFetching,
isSchemaPopulated, isSchemaPopulated,
schemaError, schemaError,
usenetDownloadClients, usenetDownloadClients,
torrentDownloadClients torrentDownloadClients,
otherDownloadClients
}; };
} }
); );

@ -13,6 +13,19 @@ import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem'; import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css'; 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 { class AddIndexerModalContent extends Component {
// //
@ -25,6 +38,7 @@ class AddIndexerModalContent extends Component {
schemaError, schemaError,
usenetIndexers, usenetIndexers,
torrentIndexers, torrentIndexers,
otherIndexers,
onIndexerSelect, onIndexerSelect,
onModalClose onModalClose
} = this.props; } = this.props;
@ -64,16 +78,7 @@ class AddIndexerModalContent extends Component {
<FieldSet legend={translate('Usenet')}> <FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}> <div className={styles.indexers}>
{ {
usenetIndexers.map((indexer) => { mapIndexers(usenetIndexers, onIndexerSelect)
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
} }
</div> </div>
</FieldSet> </FieldSet>
@ -81,19 +86,22 @@ class AddIndexerModalContent extends Component {
<FieldSet legend={translate('Torrents')}> <FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}> <div className={styles.indexers}>
{ {
torrentIndexers.map((indexer) => { mapIndexers(torrentIndexers, onIndexerSelect)
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
} }
</div> </div>
</FieldSet> </FieldSet>
{
otherIndexers.length ?
<FieldSet legend="Other">
<div className={styles.indexers}>
{
mapIndexers(otherIndexers, onIndexerSelect)
}
</div>
</FieldSet> :
null
}
</div> </div>
} }
</ModalBody> </ModalBody>
@ -115,6 +123,7 @@ AddIndexerModalContent.propTypes = {
schemaError: PropTypes.object, schemaError: PropTypes.object,
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
otherIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired, onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -17,15 +17,18 @@ function createMapStateToProps() {
schema schema
} = indexers; } = indexers;
const usenetIndexers = _.filter(schema, { protocol: 'usenet' }); const usenetIndexers = _.filter(schema, { protocol: 'UsenetDownloadProtocol' });
const torrentIndexers = _.filter(schema, { protocol: 'torrent' }); const torrentIndexers = _.filter(schema, { protocol: 'TorrentDownloadProtocol' });
const otherIndexers = _.filter(schema, (x) => x.protocol !== 'UsenetDownloadProtocol' &&
x.protocol !== 'TorrentDownloadProtocol');
return { return {
isSchemaFetching, isSchemaFetching,
isSchemaPopulated, isSchemaPopulated,
schemaError, schemaError,
usenetIndexers, usenetIndexers,
torrentIndexers torrentIndexers,
otherIndexers
}; };
} }
); );

@ -7,10 +7,14 @@
line-height: 30px; line-height: 30px;
} }
.column { .name {
flex: 0 0 200px; flex: 0 0 200px;
} }
.fillcolumn {
flex: 1 0 0;
}
.actions { .actions {
display: flex; display: flex;
} }

@ -2,12 +2,13 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'column': string;
'delayProfile': string; 'delayProfile': string;
'dragHandle': string; 'dragHandle': string;
'dragIcon': string; 'dragIcon': string;
'editButton': string; 'editButton': string;
'fillcolumn': string;
'isDragging': string; 'isDragging': string;
'name': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

@ -6,28 +6,11 @@ import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList'; import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import DelayProfileItem from './DelayProfileItem';
import EditDelayProfileModalConnector from './EditDelayProfileModalConnector'; import EditDelayProfileModalConnector from './EditDelayProfileModalConnector';
import styles from './DelayProfile.css'; 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 { class DelayProfile extends Component {
// //
@ -74,25 +57,14 @@ class DelayProfile extends Component {
render() { render() {
const { const {
id, id,
enableUsenet, name,
enableTorrent, items,
preferredProtocol,
usenetDelay,
torrentDelay,
tags, tags,
tagList, tagList,
isDragging, isDragging,
connectDragSource connectDragSource
} = this.props; } = this.props;
let preferred = titleCase(translate('PreferProtocol', { preferredProtocol }));
if (!enableUsenet) {
preferred = translate('OnlyTorrent');
} else if (!enableTorrent) {
preferred = translate('OnlyUsenet');
}
return ( return (
<div <div
className={classNames( className={classNames(
@ -100,11 +72,26 @@ class DelayProfile extends Component {
isDragging && styles.isDragging isDragging && styles.isDragging
)} )}
> >
<div className={styles.column}>{preferred}</div>
<div className={styles.column}>{getDelay(enableUsenet, usenetDelay)}</div> <div className={styles.name}>{name}</div>
<div className={styles.column}>{getDelay(enableTorrent, torrentDelay)}</div>
<div className={styles.fillcolumn}>
{
items.map((x) => {
return (
<DelayProfileItem
key={x.protocol}
name={x.name}
allowed={x.allowed}
delay={x.delay}
/>
);
})
}
</div>
<TagList <TagList
className={styles.fillcolumn}
tags={tags} tags={tags}
tagList={tagList} tagList={tagList}
/> />
@ -153,11 +140,8 @@ class DelayProfile extends Component {
DelayProfile.propTypes = { DelayProfile.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
enableUsenet: PropTypes.bool.isRequired, name: PropTypes.string.isRequired,
enableTorrent: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
preferredProtocol: PropTypes.string.isRequired,
usenetDelay: PropTypes.number.isRequired,
torrentDelay: PropTypes.number.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
isDragging: PropTypes.bool.isRequired, isDragging: PropTypes.bool.isRequired,

@ -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; font-weight: bold;
} }
.column { .name {
flex: 0 0 200px; flex: 0 0 200px;
} }
.tags { .fillcolumn {
flex: 1 0 auto; flex: 1 0 auto;
} }
.actions {
flex: 0 0 80px;
}
.addDelayProfile { .addDelayProfile {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

@ -1,13 +1,14 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string;
'addButton': string; 'addButton': string;
'addDelayProfile': string; 'addDelayProfile': string;
'column': string;
'delayProfiles': string; 'delayProfiles': string;
'delayProfilesHeader': string; 'delayProfilesHeader': string;
'fillcolumn': string;
'horizontalScroll': string; 'horizontalScroll': string;
'tags': string; 'name': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

@ -82,18 +82,10 @@ class DelayProfiles extends Component {
> >
<div> <div>
<div className={styles.delayProfilesHeader}> <div className={styles.delayProfilesHeader}>
<div className={styles.column}> <div className={styles.name}>{translate('Name')}</div>
{translate('PreferredProtocol')} <div className={styles.fillcolumn}>{translate('Protocols')}</div>
</div> <div className={styles.fillcolumn}>{translate('Tags')}</div>
<div className={styles.column}> <div className={styles.actions} />
{translate('UsenetDelay')}
</div>
<div className={styles.column}>
{translate('TorrentDelay')}
</div>
<div className={styles.tags}>
{translate('Tags')}
</div>
</div> </div>
<div className={styles.delayProfiles}> <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;
}
}

@ -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;

@ -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;
}

@ -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;
}

@ -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;
}

@ -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;

@ -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 ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape'; import { boolSettingShape, numberSettingShape, stringSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import DownloadProtocolItems from './DownloadProtocolItems';
import styles from './EditDelayProfileModalContent.css'; 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) { function EditDelayProfileModalContent(props) {
const { const {
id, id,
isFetching, isFetching,
isPopulated,
error, error,
isSaving, isSaving,
saveError, saveError,
item, item,
protocol,
onInputChange, onInputChange,
onProtocolChange,
onSavePress, onSavePress,
onModalClose, onModalClose,
onDeleteDelayProfilePress, onDeleteDelayProfilePress,
@ -62,10 +35,8 @@ function EditDelayProfileModalContent(props) {
} = props; } = props;
const { const {
enableUsenet, name,
enableTorrent, items,
usenetDelay,
torrentDelay,
bypassIfHighestQuality, bypassIfHighestQuality,
bypassIfAboveCustomFormatScore, bypassIfAboveCustomFormatScore,
minimumCustomFormatScore, minimumCustomFormatScore,
@ -88,60 +59,35 @@ function EditDelayProfileModalContent(props) {
{ {
!isFetching && !!error ? !isFetching && !!error ?
<div> <div>
{translate('UnableToAddANewQualityProfilePleaseTryAgain')} {translate('UnableToAddANewDelayProfilePleaseTryAgain')}
</div> : </div> :
null null
} }
{ {
!isFetching && !error ? !isFetching && isPopulated && !error ?
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup> <FormGroup size={sizes.SMALL}>
<FormLabel>{translate('PreferredProtocol')}</FormLabel> <FormLabel size={sizes.SMALL}>
{translate('Name')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.TEXT}
name="protocol" name="name"
value={protocol} {...name}
values={protocolOptions} onChange={onInputChange}
helpText={translate('ProtocolHelpText')}
onChange={onProtocolChange}
/> />
</FormGroup> </FormGroup>
{ <div className={styles.formGroupWrapper}>
enableUsenet.value ? <DownloadProtocolItems
<FormGroup> items={items.value}
<FormLabel>{translate('UsenetDelay')}</FormLabel> errors={items.errors}
warnings={items.warnings}
<FormInputGroup {...otherProps}
type={inputTypes.NUMBER} />
name="usenetDelay" </div>
unit="minutes"
{...usenetDelay}
helpText={translate('UsenetDelayHelpText')}
onChange={onInputChange}
/>
</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
}
<FormGroup> <FormGroup>
<FormLabel>{translate('BypassIfHighestQuality')}</FormLabel> <FormLabel>{translate('BypassIfHighestQuality')}</FormLabel>
@ -186,11 +132,13 @@ function EditDelayProfileModalContent(props) {
{ {
id === 1 ? id === 1 ?
<Alert> <Alert>
{translate('DefaultDelayProfileHelpText')} This is the default profile. It applies to all artists that don't have an explicit profile.
</Alert> : </Alert> :
<FormGroup> <FormGroup size={sizes.SMALL}>
<FormLabel>{translate('Tags')}</FormLabel> <FormLabel size={sizes.SMALL}>
{translate('Tags')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
@ -237,13 +185,11 @@ function EditDelayProfileModalContent(props) {
} }
const delayProfileShape = { 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, bypassIfHighestQuality: PropTypes.shape(boolSettingShape).isRequired,
bypassIfAboveCustomFormatScore: PropTypes.shape(boolSettingShape).isRequired, bypassIfAboveCustomFormatScore: PropTypes.shape(boolSettingShape).isRequired,
minimumCustomFormatScore: PropTypes.shape(numberSettingShape).isRequired, minimumCustomFormatScore: PropTypes.shape(numberSettingShape).isRequired,
name: PropTypes.shape(stringSettingShape).isRequired,
items: PropTypes.object.isRequired,
order: PropTypes.shape(numberSettingShape), order: PropTypes.shape(numberSettingShape),
tags: PropTypes.shape(tagSettingShape).isRequired tags: PropTypes.shape(tagSettingShape).isRequired
}; };
@ -251,13 +197,12 @@ const delayProfileShape = {
EditDelayProfileModalContent.propTypes = { EditDelayProfileModalContent.propTypes = {
id: PropTypes.number, id: PropTypes.number,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.shape(delayProfileShape).isRequired, item: PropTypes.shape(delayProfileShape).isRequired,
protocol: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onProtocolChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onDeleteDelayProfilePress: PropTypes.func onDeleteDelayProfilePress: PropTypes.func

@ -3,77 +3,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions'; import { fetchDelayProfileSchema, saveDelayProfile, setDelayProfileValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDelayProfileModalContent from './EditDelayProfileModalContent'; 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() { function createMapStateToProps() {
return createSelector( return createSelector(
createDelayProfileSelector(), createProviderSettingsSelector('delayProfiles'),
(delayProfile) => { (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 { return {
protocol,
...delayProfile ...delayProfile
}; };
} }
@ -81,6 +19,7 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchDelayProfileSchema,
setDelayProfileValue, setDelayProfileValue,
saveDelayProfile saveDelayProfile
}; };
@ -90,14 +29,19 @@ class EditDelayProfileModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
dragIndex: null,
dropIndex: null,
dropPosition: null
};
}
componentDidMount() { componentDidMount() {
if (!this.props.id) { if (!this.props.id && !this.props.isPopulated) {
Object.keys(newDelayProfile).forEach((name) => { this.props.fetchDelayProfileSchema();
this.props.setDelayProfileValue({
name,
value: newDelayProfile[name]
});
});
} }
} }
@ -114,35 +58,77 @@ class EditDelayProfileModalContentConnector extends Component {
this.props.setDelayProfileValue({ name, value }); this.props.setDelayProfileValue({ name, value });
}; };
onProtocolChange = ({ value }) => { onSavePress = () => {
switch (value) { this.props.saveDelayProfile({ id: this.props.id });
case 'preferUsenet': };
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); onDownloadProtocolItemFieldChange = (protocol, name, value) => {
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); const delayProfile = _.cloneDeep(this.props.item);
break; const items = delayProfile.items.value;
case 'preferTorrent': const item = _.find(delayProfile.items.value, (i) => i.protocol === protocol);
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true });
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); item[name] = value;
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' });
break; this.props.setDelayProfileValue({
case 'onlyUsenet': name: 'items',
this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); value: items
this.props.setDelayProfileValue({ name: 'enableTorrent', value: false }); });
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); };
break;
case 'onlyTorrent': onDownloadProtocolItemDragMove = ({ dragIndex, dropIndex, dropPosition }) => {
this.props.setDelayProfileValue({ name: 'enableUsenet', value: false }); if (
this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); (dropPosition === 'below' && dropIndex + 1 === dragIndex) ||
this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' }); (dropPosition === 'above' && dropIndex - 1 === dragIndex)
break; ) {
default: if (
throw Error(`Unknown protocol option: ${value}`); 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 = () => { onDownloadProtocolItemDragEnd = (didDrop) => {
this.props.saveDelayProfile({ id: this.props.id }); 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() { render() {
return ( return (
<EditDelayProfileModalContent <EditDelayProfileModalContent
{...this.state}
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onInputChange={this.onInputChange} 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 = { EditDelayProfileModalContentConnector.propTypes = {
id: PropTypes.number, id: PropTypes.number,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
fetchDelayProfileSchema: PropTypes.func.isRequired,
setDelayProfileValue: PropTypes.func.isRequired, setDelayProfileValue: PropTypes.func.isRequired,
saveDelayProfile: PropTypes.func.isRequired, saveDelayProfile: PropTypes.func.isRequired,
onModalClose: 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;
}

@ -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 PropTypes from 'prop-types';
import React from 'react'; 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) { function TagDetailsDelayProfile(props) {
const { const {
preferredProtocol, name: profileName,
enableUsenet, items
enableTorrent,
usenetDelay,
torrentDelay
} = props; } = props;
return ( return (
<div> <div
<div> className={styles.delayProfile}
Protocol: {titleCase(preferredProtocol)} >
<div
className={styles.name}
>
{profileName}
</div> </div>
<div> <div className={styles.tags}>
{ {
enableUsenet ? items.map((item) => {
`Usenet Delay: ${usenetDelay}` : const {
'Usenet disabled' protocol,
} name,
</div> allowed
} = item;
<div> return (
{ <DelayProfileItem
enableTorrent ? key={protocol}
`Torrent Delay: ${torrentDelay}` : name={name}
'Torrents disabled' allowed={allowed}
/>
);
})
} }
</div> </div>
</div> </div>
@ -37,11 +43,8 @@ function TagDetailsDelayProfile(props) {
} }
TagDetailsDelayProfile.propTypes = { TagDetailsDelayProfile.propTypes = {
preferredProtocol: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enableUsenet: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired
enableTorrent: PropTypes.bool.isRequired,
usenetDelay: PropTypes.number.isRequired,
torrentDelay: PropTypes.number.isRequired
}; };
export default TagDetailsDelayProfile; export default TagDetailsDelayProfile;

@ -13,7 +13,7 @@ function TagDetailsModal(props) {
return ( return (
<Modal <Modal
size={sizes.SMALL} size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >

@ -65,21 +65,15 @@ function TagDetailsModalContent(props) {
delayProfiles.map((item) => { delayProfiles.map((item) => {
const { const {
id, id,
preferredProtocol, name,
enableUsenet, items
enableTorrent,
usenetDelay,
torrentDelay
} = item; } = item;
return ( return (
<TagDetailsDelayProfile <TagDetailsDelayProfile
key={id} key={id}
preferredProtocol={preferredProtocol} name={name}
enableUsenet={enableUsenet} items={items}
enableTorrent={enableTorrent}
usenetDelay={usenetDelay}
torrentDelay={torrentDelay}
/> />
); );
}) })

@ -52,6 +52,10 @@ export default {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
error: null, error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: {},
items: [], items: [],
isSaving: false, isSaving: false,
saveError: null, saveError: null,

@ -55,6 +55,7 @@ export const defaultState = {
export const SHOW_MESSAGE = 'app/showMessage'; export const SHOW_MESSAGE = 'app/showMessage';
export const HIDE_MESSAGE = 'app/hideMessage'; export const HIDE_MESSAGE = 'app/hideMessage';
export const CLEAR_MESSAGES = 'app/clearMessages';
export const SAVE_DIMENSIONS = 'app/saveDimensions'; export const SAVE_DIMENSIONS = 'app/saveDimensions';
export const SET_VERSION = 'app/setVersion'; export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue'; 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 setAppValue = createAction(SET_APP_VALUE);
export const showMessage = createAction(SHOW_MESSAGE); export const showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE); export const hideMessage = createAction(HIDE_MESSAGE);
export const clearMessages = createAction(CLEAR_MESSAGES);
export const pingServer = createThunk(PING_SERVER); export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS); export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
@ -192,6 +194,14 @@ export const reducers = createHandleActions({
return updateSectionState(state, messagesSection, newState); 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 }) { [SET_APP_VALUE]: function(state, { payload }) {
const newState = Object.assign(getSectionState(state, section), payload); const newState = Object.assign(getSectionState(state, section), payload);

@ -70,6 +70,13 @@ export const defaultState = {
items: [] items: []
}, },
plugins: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
logs: { logs: {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
@ -199,6 +206,8 @@ export const DELETE_BACKUP = 'system/backups/deleteBackup';
export const FETCH_UPDATES = 'system/updates/fetchUpdates'; export const FETCH_UPDATES = 'system/updates/fetchUpdates';
export const FETCH_INSTALLED_PLUGINS = 'system/plugins/fetchInstalledPlugins';
export const FETCH_LOGS = 'system/logs/fetchLogs'; export const FETCH_LOGS = 'system/logs/fetchLogs';
export const GOTO_FIRST_LOGS_PAGE = 'system/logs/gotoLogsFirstPage'; export const GOTO_FIRST_LOGS_PAGE = 'system/logs/gotoLogsFirstPage';
export const GOTO_PREVIOUS_LOGS_PAGE = 'system/logs/gotoLogsPreviousPage'; 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 fetchUpdates = createThunk(FETCH_UPDATES);
export const fetchInstalledPlugins = createThunk(FETCH_INSTALLED_PLUGINS);
export const fetchLogs = createThunk(FETCH_LOGS); export const fetchLogs = createThunk(FETCH_LOGS);
export const gotoLogsFirstPage = createThunk(GOTO_FIRST_LOGS_PAGE); export const gotoLogsFirstPage = createThunk(GOTO_FIRST_LOGS_PAGE);
export const gotoLogsPreviousPage = createThunk(GOTO_PREVIOUS_LOGS_PAGE); export const gotoLogsPreviousPage = createThunk(GOTO_PREVIOUS_LOGS_PAGE);
@ -326,6 +337,7 @@ export const actionHandlers = handleThunks({
[DELETE_BACKUP]: createRemoveItemHandler(backupsSection, '/system/backup'), [DELETE_BACKUP]: createRemoveItemHandler(backupsSection, '/system/backup'),
[FETCH_UPDATES]: createFetchHandler('system.updates', '/update'), [FETCH_UPDATES]: createFetchHandler('system.updates', '/update'),
[FETCH_INSTALLED_PLUGINS]: createFetchHandler('system.plugins', '/system/plugins'),
[FETCH_LOG_FILES]: createFetchHandler('system.logFiles', '/log/file'), [FETCH_LOG_FILES]: createFetchHandler('system.logFiles', '/log/file'),
[FETCH_UPDATE_LOG_FILES]: createFetchHandler('system.updateLogFiles', '/log/file/update'), [FETCH_UPDATE_LOG_FILES]: createFetchHandler('system.updateLogFiles', '/log/file/update'),

@ -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;
}

@ -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;

@ -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;

@ -0,0 +1,6 @@
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 5px;
margin-left: auto;
}

@ -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;

@ -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.Api.V1.CustomFormats;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.Blocklist namespace Lidarr.Api.V1.Blocklist
@ -17,7 +16,7 @@ namespace Lidarr.Api.V1.Blocklist
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; } public List<CustomFormatResource> CustomFormats { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public DownloadProtocol Protocol { get; set; } public string Protocol { get; set; }
public string Indexer { get; set; } public string Indexer { get; set; }
public string Message { get; set; } public string Message { get; set; }

@ -66,8 +66,9 @@ namespace Lidarr.Api.V1.Commands
? CommandPriority.High ? CommandPriority.High
: CommandPriority.Normal; : 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.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true; command.SendUpdatesToClient = true;
command.ClientUserAgent = Request.Headers["UserAgent"]; command.ClientUserAgent = Request.Headers["UserAgent"];

@ -1,12 +1,11 @@
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
namespace Lidarr.Api.V1.DownloadClient namespace Lidarr.Api.V1.DownloadClient
{ {
public class DownloadClientResource : ProviderResource<DownloadClientResource> public class DownloadClientResource : ProviderResource<DownloadClientResource>
{ {
public bool Enable { get; set; } public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; } public string Protocol { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public bool RemoveCompletedDownloads { get; set; } public bool RemoveCompletedDownloads { get; set; }
public bool RemoveFailedDownloads { get; set; } public bool RemoveFailedDownloads { get; set; }

@ -9,7 +9,7 @@ namespace Lidarr.Api.V1.Indexers
public bool EnableInteractiveSearch { get; set; } public bool EnableInteractiveSearch { get; set; }
public bool SupportsRss { get; set; } public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; } public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; } public string Protocol { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public int DownloadClientId { get; set; } public int DownloadClientId { get; set; }
} }

@ -48,7 +48,7 @@ namespace Lidarr.Api.V1.Indexers
public string InfoHash { get; set; } public string InfoHash { get; set; }
public int? Seeders { get; set; } public int? Seeders { get; set; }
public int? Leechers { get; set; } public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; } public string Protocol { get; set; }
public int IndexerFlags { get; set; } public int IndexerFlags { get; set; }
// Sent when queuing an unknown release // Sent when queuing an unknown release
@ -125,7 +125,7 @@ namespace Lidarr.Api.V1.Indexers
{ {
ReleaseInfo model; ReleaseInfo model;
if (resource.Protocol == DownloadProtocol.Torrent) if (resource.Protocol == nameof(TorrentDownloadProtocol))
{ {
model = new TorrentInfo model = new TorrentInfo
{ {

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using FluentValidation; using FluentValidation;
using Lidarr.Http; using Lidarr.Http;
using Lidarr.Http.REST; using Lidarr.Http.REST;
@ -21,16 +22,8 @@ namespace Lidarr.Api.V1.Profiles.Delay
SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); 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).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1);
SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator);
SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0); SharedValidator.RuleFor(d => d.Items).Must(items => items.All(x => x.Delay >= 0)).WithMessage("Protocols cannot have a negative delay");
SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0); SharedValidator.RuleFor(d => d.Items).Must(items => items.Any(x => x.Allowed)).WithMessage("At least one protocol must be enabled");
SharedValidator.RuleFor(d => d).Custom((delayProfile, context) =>
{
if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent)
{
context.AddFailure("Either Usenet or Torrent should be enabled");
}
});
} }
[RestPostById] [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.Collections.Generic;
using System.Linq; using System.Linq;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Profiles.Delay;
namespace Lidarr.Api.V1.Profiles.Delay namespace Lidarr.Api.V1.Profiles.Delay
{ {
public class DelayProfileResource : RestResource public class DelayProfileResource : RestResource
{ {
public bool EnableUsenet { get; set; } public string Name { get; set; }
public bool EnableTorrent { get; set; } public List<DelayProfileProtocolItemResource> Items { get; set; }
public DownloadProtocol PreferredProtocol { get; set; }
public int UsenetDelay { get; set; }
public int TorrentDelay { get; set; }
public bool BypassIfHighestQuality { get; set; } public bool BypassIfHighestQuality { get; set; }
public bool BypassIfAboveCustomFormatScore { get; set; } public bool BypassIfAboveCustomFormatScore { get; set; }
public int MinimumCustomFormatScore { get; set; } public int MinimumCustomFormatScore { get; set; }
@ -32,12 +28,8 @@ namespace Lidarr.Api.V1.Profiles.Delay
return new DelayProfileResource return new DelayProfileResource
{ {
Id = model.Id, Id = model.Id,
Name = model.Name,
EnableUsenet = model.EnableUsenet, Items = model.Items.Select(x => x.ToResource()).ToList(),
EnableTorrent = model.EnableTorrent,
PreferredProtocol = model.PreferredProtocol,
UsenetDelay = model.UsenetDelay,
TorrentDelay = model.TorrentDelay,
BypassIfHighestQuality = model.BypassIfHighestQuality, BypassIfHighestQuality = model.BypassIfHighestQuality,
BypassIfAboveCustomFormatScore = model.BypassIfAboveCustomFormatScore, BypassIfAboveCustomFormatScore = model.BypassIfAboveCustomFormatScore,
MinimumCustomFormatScore = model.MinimumCustomFormatScore, MinimumCustomFormatScore = model.MinimumCustomFormatScore,
@ -56,12 +48,8 @@ namespace Lidarr.Api.V1.Profiles.Delay
return new DelayProfile return new DelayProfile
{ {
Id = resource.Id, Id = resource.Id,
Name = resource.Name,
EnableUsenet = resource.EnableUsenet, Items = resource.Items.Select(x => x.ToModel()).ToList(),
EnableTorrent = resource.EnableTorrent,
PreferredProtocol = resource.PreferredProtocol,
UsenetDelay = resource.UsenetDelay,
TorrentDelay = resource.TorrentDelay,
BypassIfHighestQuality = resource.BypassIfHighestQuality, BypassIfHighestQuality = resource.BypassIfHighestQuality,
BypassIfAboveCustomFormatScore = resource.BypassIfAboveCustomFormatScore, BypassIfAboveCustomFormatScore = resource.BypassIfAboveCustomFormatScore,
MinimumCustomFormatScore = resource.MinimumCustomFormatScore, MinimumCustomFormatScore = resource.MinimumCustomFormatScore,

@ -0,0 +1,22 @@
using Lidarr.Http;
using Lidarr.Http.REST;
using NzbDrone.Core.Profiles.Delay;
namespace Lidarr.Api.V1.Profiles.Delay
{
[V1ApiController("delayprofile/schema")]
public class DelayProfileSchemaController : RestController<DelayProfileResource>
{
private readonly IDelayProfileService _profileService;
public DelayProfileSchemaController(IDelayProfileService profileService)
{
_profileService = profileService;
}
public override DelayProfileResource GetResourceById(int id)
{
return _profileService.GetDefaultProfile().ToResource();
}
}
}

@ -63,7 +63,7 @@ namespace Lidarr.Api.V1
Tags = resource.Tags 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); definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract, existingDefinition?.Settings);
return definition; return definition;

@ -13,7 +13,6 @@ using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -130,7 +129,7 @@ namespace Lidarr.Api.V1.Queue
[HttpGet] [HttpGet]
[Produces("application/json")] [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 pagingResource = new PagingResource<QueueResource>(paging);
var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending); 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)); 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 ascending = pagingSpec.SortDirection == SortDirection.Ascending;
var orderByFunc = GetOrderByFunc(pagingSpec); var orderByFunc = GetOrderByFunc(pagingSpec);
@ -159,9 +158,9 @@ namespace Lidarr.Api.V1.Queue
include &= q.Artist != null && artistIds.Contains(q.Artist.Id); 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) if (include && hasQualityFilter)

@ -7,7 +7,6 @@ using Lidarr.Api.V1.CustomFormats;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.Queue namespace Lidarr.Api.V1.Queue
@ -33,7 +32,7 @@ namespace Lidarr.Api.V1.Queue
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public string ErrorMessage { get; set; } public string ErrorMessage { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public DownloadProtocol Protocol { get; set; } public string Protocol { get; set; }
public string DownloadClient { get; set; } public string DownloadClient { get; set; }
public bool DownloadClientHasPostImportCategory { get; set; } public bool DownloadClientHasPostImportCategory { get; set; }
public string Indexer { get; set; } public string Indexer { get; set; }

@ -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();
}
}
}

@ -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 FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Reflection; using NzbDrone.Common.Reflection;
@ -12,7 +11,7 @@ namespace NzbDrone.Common.Test.ReflectionTests
[Test] [Test]
public void should_get_properties_from_models() 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) foreach (var model in models)
{ {
@ -23,7 +22,7 @@ namespace NzbDrone.Common.Test.ReflectionTests
[Test] [Test]
public void should_be_able_to_get_implementations() 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(); models.Should().NotBeEmpty();
} }

@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
@ -15,7 +16,6 @@ using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test namespace NzbDrone.Common.Test
@ -26,9 +26,10 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void event_handlers_should_be_unique() public void event_handlers_should_be_unique()
{ {
var assemblies = AssemblyLoader.LoadBaseAssemblies();
var container = new Container(rules => rules.WithNzbDroneRules()) var container = new Container(rules => rules.WithNzbDroneRules())
.AddNzbDroneLogger() .AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES) .AutoAddServices(assemblies)
.AddDummyDatabase() .AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second")); .AddStartupContext(new StartupContext("first", "second"));

@ -9,17 +9,38 @@ using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition 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() static AssemblyLoader()
{ {
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler); AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
RegisterSQLiteResolver(); RegisterSQLiteResolver();
} }
public static IList<Assembly> Load(IList<string> assemblyNames) public static List<Assembly> LoadBaseAssemblies()
{
return Load(BaseAssemblies);
}
public static List<Assembly> LoadUpdateAssemblies()
{
return Load(UpdateAssemblies);
}
private static List<Assembly> Load(IList<string> assemblies)
{ {
var toLoad = assemblyNames.ToList(); var toLoad = assemblies.ToList();
toLoad.Add("Lidarr.Common"); toLoad.Add("Lidarr.Common");
toLoad.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono"); toLoad.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono");

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using DryIoc; using DryIoc;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -20,10 +21,8 @@ namespace NzbDrone.Common.Composition.Extensions
return container; 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, container.RegisterMany(assemblies,
serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"), serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"),
reuse: Reuse.Singleton); reuse: Reuse.Singleton);
@ -37,5 +36,17 @@ namespace NzbDrone.Common.Composition.Extensions
return container; return container;
} }
public static IContainer SetPluginStatus(this IContainer container, bool enabled)
{
var pluginStatus = new PluginStatus
{
Enabled = enabled
};
container.RegisterInstance(pluginStatus);
return container;
}
} }
} }

@ -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;
}
}
}

@ -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;
}
}
}

@ -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 LOG_DB = "logs.db";
private const string NLOG_CONFIG_FILE = "nlog.config"; private const string NLOG_CONFIG_FILE = "nlog.config";
private const string UPDATE_CLIENT_EXE_NAME = "Lidarr.Update"; 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_SANDBOX_FOLDER_NAME = "lidarr_update" + Path.DirectorySeparatorChar;
private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Lidarr" + Path.DirectorySeparatorChar; private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Lidarr" + Path.DirectorySeparatorChar;
@ -310,6 +311,26 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetAppDataPath(appFolderInfo), APP_CONFIG_FILE); 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) public static string GetMediaCoverPath(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(GetAppDataPath(appFolderInfo), "MediaCover"); return Path.Combine(GetAppDataPath(appFolderInfo), "MediaCover");

@ -7,17 +7,15 @@ namespace NzbDrone.Common.Reflection
{ {
public static class ReflectionExtensions public static class ReflectionExtensions
{ {
public static readonly Assembly CoreAssembly = Assembly.Load("Lidarr.Core");
public static List<PropertyInfo> GetSimpleProperties(this Type type) public static List<PropertyInfo> GetSimpleProperties(this Type type)
{ {
var properties = type.GetProperties(); var properties = type.GetProperties();
return properties.Where(c => c.PropertyType.IsSimpleType()).ToList(); 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) 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)); 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) public static bool HasAttribute<TAttribute>(this Type type)
{ {
return type.GetCustomAttributes(typeof(TAttribute), true).Any(); return type.GetCustomAttributes(typeof(TAttribute), true).Any();

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework; 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("publishedDate", DateTime.UtcNow.ToString("s") + "Z");
_event.Data.Add("size", "1000"); _event.Data.Add("size", "1000");
_event.Data.Add("indexer", "nzbs.org"); _event.Data.Add("indexer", "nzbs.org");
_event.Data.Add("protocol", "1"); _event.Data.Add("protocol", nameof(UsenetDownloadProtocol));
_event.Data.Add("message", "Marked as failed"); _event.Data.Add("message", "Marked as failed");
Mocker.SetConstant<IBlocklistForProtocol>(Mocker.Resolve<UsenetBlocklist>());
} }
[Test] [Test]

@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported); GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew() _remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent) .With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
.With(t => t.InfoHash = downloadId) .With(t => t.InfoHash = downloadId)
.Build(); .Build();
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported); GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew() _remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent) .With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
.With(t => t.InfoHash = null) .With(t => t.InfoHash = null)
.Build(); .Build();
@ -174,7 +174,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenHistoryItem(null, TITLE, _flac, EntityHistoryEventType.DownloadImported); GivenHistoryItem(null, TITLE, _flac, EntityHistoryEventType.DownloadImported);
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew() _remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent) .With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
.With(t => t.InfoHash = null) .With(t => t.InfoHash = null)
.Build(); .Build();
@ -190,7 +190,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported); GivenHistoryItem(downloadId, TITLE, _flac, EntityHistoryEventType.DownloadImported);
_remoteAlbum.Release = Builder<TorrentInfo>.CreateNew() _remoteAlbum.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent) .With(t => t.DownloadProtocol = nameof(TorrentDownloadProtocol))
.With(t => t.InfoHash = downloadId) .With(t => t.InfoHash = downloadId)
.Build(); .Build();

@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{ {
_remoteAlbum = new RemoteAlbum _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] [SetUp]
public void Setup() public void Setup()
{ {
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); GivenPreferredDownloadProtocol(nameof(UsenetDownloadProtocol));
Mocker.GetMock<IQualityDefinitionService>() Mocker.GetMock<IQualityDefinitionService>()
.Setup(s => s.Get(It.IsAny<Quality>())) .Setup(s => s.Get(It.IsAny<Quality>()))
@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Build(); .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(); var remoteAlbum = new RemoteAlbum();
remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
@ -78,14 +78,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
return remoteAlbum; 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>() Mocker.GetMock<IDelayProfileService>()
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
.Returns(new DelayProfile .Returns(profile);
{
PreferredProtocol = downloadProtocol
});
} }
[Test] [Test]
@ -218,33 +218,33 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_put_usenet_above_torrent_when_usenet_is_preferred() 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 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: DownloadProtocol.Usenet); var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(UsenetDownloadProtocol));
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteAlbum1)); decisions.Add(new DownloadDecision(remoteAlbum1));
decisions.Add(new DownloadDecision(remoteAlbum2)); decisions.Add(new DownloadDecision(remoteAlbum2));
var qualifiedReports = Subject.PrioritizeDecisions(decisions); var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(nameof(UsenetDownloadProtocol));
} }
[Test] [Test]
public void should_put_torrent_above_usenet_when_torrent_is_preferred() 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 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: DownloadProtocol.Usenet); var remoteAlbum2 = GivenRemoteAlbum(new List<Album> { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: nameof(UsenetDownloadProtocol));
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteAlbum1)); decisions.Add(new DownloadDecision(remoteAlbum1));
decisions.Add(new DownloadDecision(remoteAlbum2)); decisions.Add(new DownloadDecision(remoteAlbum2));
var qualifiedReports = Subject.PrioritizeDecisions(decisions); var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); qualifiedReports.First().RemoteAlbum.Release.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
} }
[Test] [Test]
@ -302,7 +302,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var torrentInfo1 = new TorrentInfo(); var torrentInfo1 = new TorrentInfo();
torrentInfo1.PublishDate = DateTime.Now; torrentInfo1.PublishDate = DateTime.Now;
torrentInfo1.Size = 0; torrentInfo1.Size = 0;
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
torrentInfo1.Seeders = 10; torrentInfo1.Seeders = 10;
var torrentInfo2 = torrentInfo1.JsonClone(); var torrentInfo2 = torrentInfo1.JsonClone();
@ -328,7 +328,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var torrentInfo1 = new TorrentInfo(); var torrentInfo1 = new TorrentInfo();
torrentInfo1.PublishDate = DateTime.Now; torrentInfo1.PublishDate = DateTime.Now;
torrentInfo1.Size = 0; torrentInfo1.Size = 0;
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
torrentInfo1.Seeders = 10; torrentInfo1.Seeders = 10;
torrentInfo1.Peers = 10; torrentInfo1.Peers = 10;
@ -355,7 +355,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var torrentInfo1 = new TorrentInfo(); var torrentInfo1 = new TorrentInfo();
torrentInfo1.PublishDate = DateTime.Now; torrentInfo1.PublishDate = DateTime.Now;
torrentInfo1.Size = 0; torrentInfo1.Size = 0;
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
torrentInfo1.Seeders = 0; torrentInfo1.Seeders = 0;
torrentInfo1.Peers = 10; torrentInfo1.Peers = 10;
@ -382,7 +382,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var torrentInfo1 = new TorrentInfo(); var torrentInfo1 = new TorrentInfo();
torrentInfo1.PublishDate = DateTime.Now; torrentInfo1.PublishDate = DateTime.Now;
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
torrentInfo1.Seeders = 1000; torrentInfo1.Seeders = 1000;
torrentInfo1.Peers = 10; torrentInfo1.Peers = 10;
torrentInfo1.Size = 200.Megabytes(); torrentInfo1.Size = 200.Megabytes();
@ -431,7 +431,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var torrentInfo1 = new TorrentInfo(); var torrentInfo1 = new TorrentInfo();
torrentInfo1.PublishDate = DateTime.Now; torrentInfo1.PublishDate = DateTime.Now;
torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; torrentInfo1.DownloadProtocol = nameof(TorrentDownloadProtocol);
torrentInfo1.Seeders = 100; torrentInfo1.Seeders = 100;
torrentInfo1.Peers = 10; torrentInfo1.Peers = 10;
torrentInfo1.Size = 200.Megabytes(); torrentInfo1.Size = 200.Megabytes();

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
@ -25,13 +26,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteAlbum.Artist = new Artist(); _remoteAlbum.Artist = new Artist();
_delayProfile = new DelayProfile(); _delayProfile = new DelayProfile();
_delayProfile.Items.ForEach(x => x.Allowed = false);
Mocker.GetMock<IDelayProfileService>() Mocker.GetMock<IDelayProfileService>()
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
.Returns(_delayProfile); .Returns(_delayProfile);
} }
private void GivenProtocol(DownloadProtocol downloadProtocol) private void GivenProtocol(string downloadProtocol)
{ {
_remoteAlbum.Release.DownloadProtocol = downloadProtocol; _remoteAlbum.Release.DownloadProtocol = downloadProtocol;
} }
@ -39,8 +41,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_be_true_if_usenet_and_usenet_is_enabled() public void should_be_true_if_usenet_and_usenet_is_enabled()
{ {
GivenProtocol(DownloadProtocol.Usenet); GivenProtocol(nameof(UsenetDownloadProtocol));
_delayProfile.EnableUsenet = true; _delayProfile.Items.Single(x => x.Protocol == nameof(UsenetDownloadProtocol)).Allowed = true;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true);
} }
@ -48,8 +50,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_be_true_if_torrent_and_torrent_is_enabled() public void should_be_true_if_torrent_and_torrent_is_enabled()
{ {
GivenProtocol(DownloadProtocol.Torrent); GivenProtocol(nameof(TorrentDownloadProtocol));
_delayProfile.EnableTorrent = true; _delayProfile.Items.Single(x => x.Protocol == nameof(TorrentDownloadProtocol)).Allowed = true;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(true);
} }
@ -57,8 +59,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_be_false_if_usenet_and_usenet_is_disabled() public void should_be_false_if_usenet_and_usenet_is_disabled()
{ {
GivenProtocol(DownloadProtocol.Usenet); GivenProtocol(nameof(UsenetDownloadProtocol));
_delayProfile.EnableUsenet = false;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false);
} }
@ -66,8 +67,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_be_false_if_torrent_and_torrent_is_disabled() public void should_be_false_if_torrent_and_torrent_is_disabled()
{ {
GivenProtocol(DownloadProtocol.Torrent); GivenProtocol(nameof(TorrentDownloadProtocol));
_delayProfile.EnableTorrent = false;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(false);
} }

@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{ {
_remoteAlbum = new RemoteAlbum _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 _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] [Test]
public void should_return_true_when_release_is_not_usenet() public void should_return_true_when_release_is_not_usenet()
{ {
_remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Torrent; _remoteAlbum.Release.DownloadProtocol = nameof(TorrentDownloadProtocol);
WithRetention(10); WithRetention(10);
WithAge(100); WithAge(100);

@ -34,9 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_profile = Builder<QualityProfile>.CreateNew() _profile = Builder<QualityProfile>.CreateNew()
.Build(); .Build();
_delayProfile = Builder<DelayProfile>.CreateNew() _delayProfile = new DelayProfile();
.With(d => d.PreferredProtocol = DownloadProtocol.Usenet)
.Build();
var artist = Builder<Artist>.CreateNew() var artist = Builder<Artist>.CreateNew()
.With(s => s.QualityProfile = _profile) .With(s => s.QualityProfile = _profile)
@ -55,7 +53,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); _remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
_remoteAlbum.Release = new ReleaseInfo(); _remoteAlbum.Release = new ReleaseInfo();
_remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Usenet; _remoteAlbum.Release.DownloadProtocol = nameof(UsenetDownloadProtocol);
_remoteAlbum.Albums = Builder<Album>.CreateListOfSize(1).Build().ToList(); _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.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192);
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, new AlbumSearchCriteria()).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteAlbum, new AlbumSearchCriteria()).Accepted.Should().BeFalse();
} }
@ -112,7 +110,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
[Test] [Test]
public void should_be_true_when_profile_does_not_have_a_delay() 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(); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
} }
@ -123,7 +121,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320);
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
} }
@ -131,7 +129,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
[Test] [Test]
public void should_be_true_when_quality_is_last_allowed_in_profile_and_bypass_enabled() 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; _delayProfile.BypassIfHighestQuality = true;
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
@ -146,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256); _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256);
_remoteAlbum.Release.PublishDate = DateTime.UtcNow.AddHours(-10); _remoteAlbum.Release.PublishDate = DateTime.UtcNow.AddHours(-10);
_delayProfile.UsenetDelay = 60; _delayProfile.Items[0].Delay = 60;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); 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.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_192);
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); 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>())) .Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
.Returns(true); .Returns(true);
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); 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>())) .Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
.Returns(true); .Returns(true);
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
} }
@ -206,7 +204,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
GivenExistingFile(new QualityModel(Quality.MP3_256)); GivenExistingFile(new QualityModel(Quality.MP3_256));
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
} }
@ -217,7 +215,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_remoteAlbum.CustomFormatScore = 100; _remoteAlbum.CustomFormatScore = 100;
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
_delayProfile.MinimumCustomFormatScore = 50; _delayProfile.MinimumCustomFormatScore = 50;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
@ -229,7 +227,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_remoteAlbum.CustomFormatScore = 5; _remoteAlbum.CustomFormatScore = 5;
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
_delayProfile.BypassIfAboveCustomFormatScore = true; _delayProfile.BypassIfAboveCustomFormatScore = true;
_delayProfile.MinimumCustomFormatScore = 50; _delayProfile.MinimumCustomFormatScore = 50;
@ -242,7 +240,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteAlbum.Release.PublishDate = DateTime.UtcNow; _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_remoteAlbum.CustomFormatScore = 100; _remoteAlbum.CustomFormatScore = 100;
_delayProfile.UsenetDelay = 720; _delayProfile.Items[0].Delay = 720;
_delayProfile.BypassIfAboveCustomFormatScore = true; _delayProfile.BypassIfAboveCustomFormatScore = true;
_delayProfile.MinimumCustomFormatScore = 50; _delayProfile.MinimumCustomFormatScore = 50;

@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
.Build(); .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(); var remoteAlbum = new RemoteAlbum();
remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); 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() public async Task should_not_add_to_failed_if_failed_for_a_different_protocol()
{ {
var albums = new List<Album> { GetAlbum(1) }; var albums = new List<Album> { GetAlbum(1) };
var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Usenet); var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), nameof(UsenetDownloadProtocol));
var remoteAlbum2 = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), DownloadProtocol.Torrent); var remoteAlbum2 = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320), nameof(TorrentDownloadProtocol));
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteAlbum)); decisions.Add(new DownloadDecision(remoteAlbum));
decisions.Add(new DownloadDecision(remoteAlbum2)); 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")); .Throws(new DownloadClientUnavailableException("Download client failed"));
await Subject.ProcessDecisions(decisions); 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 == nameof(UsenetDownloadProtocol)), 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(TorrentDownloadProtocol)), null), Times.Once());
} }
[Test] [Test]

@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Download
_downloadClients.Add(mock.Object); _downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet); mock.SetupGet(v => v.Protocol).Returns(nameof(UsenetDownloadProtocol));
return mock; return mock;
} }
@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.Download
_downloadClients.Add(mock.Object); _downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent); mock.SetupGet(v => v.Protocol).Returns(nameof(TorrentDownloadProtocol));
return mock; return mock;
} }
@ -98,11 +98,11 @@ namespace NzbDrone.Core.Test.Download
WithUsenetClient(); WithUsenetClient();
WithTorrentClient(); WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client1 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client2 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client3 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client4 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
var client5 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client5 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
client1.Definition.Id.Should().Be(1); client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2); client2.Definition.Id.Should().Be(2);
@ -119,11 +119,11 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentClient(); WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(2); client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3); client2.Definition.Id.Should().Be(3);
@ -139,10 +139,10 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentClient(); WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet); var client1 = Subject.GetDownloadClient(nameof(UsenetDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(1); client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2); client2.Definition.Id.Should().Be(2);
@ -161,10 +161,10 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentClient(0, clientTags); WithTorrentClient(0, clientTags);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
client1.Definition.Id.Should().Be(2); client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(4); client2.Definition.Id.Should().Be(4);
@ -183,10 +183,10 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentClient(0, clientTags); WithTorrentClient(0, clientTags);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags);
client1.Definition.Id.Should().Be(1); client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(3); client2.Definition.Id.Should().Be(3);
@ -202,7 +202,7 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(0, clientTags); WithTorrentClient(0, clientTags);
Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags)); Assert.Throws<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 0, false, seriesTags));
} }
[Test] [Test]
@ -215,10 +215,10 @@ namespace NzbDrone.Core.Test.Download
GivenBlockedClient(3); GivenBlockedClient(3);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(2); client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(4); client2.Definition.Id.Should().Be(4);
@ -238,11 +238,11 @@ namespace NzbDrone.Core.Test.Download
GivenBlockedClient(3); GivenBlockedClient(3);
GivenBlockedClient(4); GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(2); client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3); client2.Definition.Id.Should().Be(3);
@ -258,11 +258,11 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentClient(); WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(3); client1.Definition.Id.Should().Be(3);
client2.Definition.Id.Should().Be(4); client2.Definition.Id.Should().Be(4);
@ -280,11 +280,11 @@ namespace NzbDrone.Core.Test.Download
GivenBlockedClient(4); GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent); var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol));
client1.Definition.Id.Should().Be(2); client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3); client2.Definition.Id.Should().Be(3);
@ -301,11 +301,11 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentIndexer(3); WithTorrentIndexer(3);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1); var client1 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1); var client2 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1); var client3 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1); var client4 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1); var client5 = Subject.GetDownloadClient(nameof(TorrentDownloadProtocol), 1);
client1.Definition.Id.Should().Be(3); client1.Definition.Id.Should().Be(3);
client2.Definition.Id.Should().Be(3); client2.Definition.Id.Should().Be(3);
@ -323,7 +323,7 @@ namespace NzbDrone.Core.Test.Download
WithTorrentClient(); WithTorrentClient();
WithTorrentIndexer(5); 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); .Returns(_downloadClients);
Mocker.GetMock<IProvideDownloadClient>() Mocker.GetMock<IProvideDownloadClient>()
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<HashSet<int>>())) .Setup(v => v.GetDownloadClient(It.IsAny<string>(), 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)); .Returns<string, int, bool, HashSet<int>>((v, i, f, t) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
var episodes = Builder<Album>.CreateListOfSize(2) var episodes = Builder<Album>.CreateListOfSize(2)
.TheFirst(1).With(s => s.Id = 12) .TheFirst(1).With(s => s.Id = 12)
@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.Download
.Build().ToList(); .Build().ToList();
var releaseInfo = Builder<ReleaseInfo>.CreateNew() 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") .With(v => v.DownloadUrl = "http://test.site/download1.ext")
.Build(); .Build();
@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.Download
_downloadClients.Add(mock.Object); _downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet); mock.SetupGet(v => v.Protocol).Returns(nameof(UsenetDownloadProtocol));
return mock; return mock;
} }
@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.Download
_downloadClients.Add(mock.Object); _downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent); mock.SetupGet(v => v.Protocol).Returns(nameof(TorrentDownloadProtocol));
return mock; return mock;
} }
@ -247,7 +247,7 @@ namespace NzbDrone.Core.Test.Download
var mockTorrent = WithTorrentClient(); var mockTorrent = WithTorrentClient();
var mockUsenet = WithUsenetClient(); var mockUsenet = WithUsenetClient();
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent; _parseResult.Release.DownloadProtocol = nameof(TorrentDownloadProtocol);
await Subject.DownloadReport(_parseResult, null); await Subject.DownloadReport(_parseResult, null);

@ -57,7 +57,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
var client = new DownloadClientDefinition() var client = new DownloadClientDefinition()
{ {
Id = 1, Id = 1,
Protocol = DownloadProtocol.Torrent Protocol = nameof(TorrentDownloadProtocol)
}; };
var item = new DownloadClientItem() var item = new DownloadClientItem()
@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
var client = new DownloadClientDefinition() var client = new DownloadClientDefinition()
{ {
Id = 1, Id = 1,
Protocol = DownloadProtocol.Torrent Protocol = nameof(TorrentDownloadProtocol)
}; };
var item = new DownloadClientItem() var item = new DownloadClientItem()
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
var client = new DownloadClientDefinition() var client = new DownloadClientDefinition()
{ {
Id = 1, Id = 1,
Protocol = DownloadProtocol.Torrent Protocol = nameof(TorrentDownloadProtocol)
}; };
var item = new DownloadClientItem() var item = new DownloadClientItem()
@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
var client = new DownloadClientDefinition() var client = new DownloadClientDefinition()
{ {
Id = 1, Id = 1,
Protocol = DownloadProtocol.Torrent Protocol = nameof(TorrentDownloadProtocol)
}; };
var item = new DownloadClientItem() var item = new DownloadClientItem()
@ -266,7 +266,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
var client = new DownloadClientDefinition() var client = new DownloadClientDefinition()
{ {
Id = 1, Id = 1,
Protocol = DownloadProtocol.Torrent Protocol = nameof(TorrentDownloadProtocol)
}; };
var item = new DownloadClientItem() var item = new DownloadClientItem()

@ -10,6 +10,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
@ -41,7 +42,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{ {
_downloadItem = new DownloadClientItem _downloadItem = new DownloadClientItem
{ {
DownloadClientInfo = new DownloadClientItemClientInfo { Name = "Test" }, DownloadClientInfo = new DownloadClientItemClientInfo
{
Protocol = nameof(UsenetDownloadProtocol),
Id = 1,
Name = "Test"
},
DownloadId = "TestId", DownloadId = "TestId",
OutputPath = new OsPath(_downloadItemPath) OutputPath = new OsPath(_downloadItemPath)
}; };

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.HistoryTests
{ {
DownloadClientInfo = new DownloadClientItemClientInfo DownloadClientInfo = new DownloadClientItemClientInfo
{ {
Protocol = DownloadProtocol.Usenet, Protocol = nameof(UsenetDownloadProtocol),
Id = 1, Id = 1,
Name = "sab" Name = "sab"
}, },

@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Mankind.Divided.2019.S01E01.1080p.WEB-DL"); 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.DownloadUrl.Should().Be("https://filelist.io/download.php?id=1234&passkey=somepass");
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=1234"); torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=1234");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleTests
var releaseInfo = releases.First(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Shania Twain - Shania Twain (1993) [FLAC 24bit Lossless] [WEB]"); 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() releaseInfo.DownloadUrl.Should()
.Be("http://someurl.ch/torrents.php?action=download&id=1541452&authkey=redacted&torrent_pass=redacted"); .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"); 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(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Lady Gaga Born This Way 2CD FLAC 2011 WRE"); 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.DownloadUrl.Should().Be("https://indexer.codeshy.com/api?t=g&guid=123456&apikey=123456789");
releaseInfo.Indexer.Should().Be(Subject.Definition.Name); releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2013/06/02 08:58:54")); 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; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("24 S03E12 720p WEBRip h264-DRAWER"); 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.DownloadUrl.Should().Be("http://iptorrents.com/download.php/1234/24.S03E12.720p.WEBRip.h264-DRAWER.torrent?torrent_pass=abcd");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();

@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
ValidateResult(reports, hasSize, hasInfoUrl); ValidateResult(reports, hasSize, hasInfoUrl);
reports.Should().OnlyContain(c => c.DownloadProtocol == DownloadProtocol.Torrent); reports.Should().OnlyContain(c => c.DownloadProtocol == nameof(TorrentDownloadProtocol));
if (hasMagnet) if (hasMagnet)
{ {

@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
var releaseInfo = releases.First(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Brainstorm-Scary Creatures-CD-FLAC-2016-NBFLAC"); 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.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.InfoUrl.Should().Be("https://nzbgeek.info/geekseek.php?guid=38884827e1e56b9336278a449e0a38ec");
releaseInfo.CommentUrl.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; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[TSRaws] Futsuu no Joshikousei ga [Locodol] Yattemita. #07 (TBS).ts"); 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.DownloadUrl.Should().Be("https://www.nyaa.se/?page=download&tid=587750");
torrentInfo.InfoUrl.Should().Be("https://www.nyaa.se/?page=view&tid=587750"); torrentInfo.InfoUrl.Should().Be("https://www.nyaa.se/?page=view&tid=587750");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv"); 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.DownloadUrl.Should().Be("https://nyaa.si/download/1424896.torrent");
torrentInfo.InfoUrl.Should().Be("https://nyaa.si/view/1424896"); torrentInfo.InfoUrl.Should().Be("https://nyaa.si/view/1424896");
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();

@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
var releaseInfo = releases.First(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Shania Twain - Shania Twain (1993) [Album] [FLAC 24bit Lossless / WEB]"); 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.ch/ajax.php?action=download&id=1541452"); releaseInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=1541452");
releaseInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=106951&torrentid=1541452"); releaseInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=106951&torrentid=1541452");
releaseInfo.CommentUrl.Should().Be(null); releaseInfo.CommentUrl.Should().Be(null);

@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.IndexerTests
{ {
Release = new ReleaseInfo Release = new ReleaseInfo
{ {
DownloadProtocol = DownloadProtocol.Torrent, DownloadProtocol = nameof(TorrentDownloadProtocol),
IndexerId = 0 IndexerId = 0
} }
}); });
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.IndexerTests
{ {
Release = new ReleaseInfo() Release = new ReleaseInfo()
{ {
DownloadProtocol = DownloadProtocol.Torrent, DownloadProtocol = nameof(TorrentDownloadProtocol),
IndexerId = 1 IndexerId = 1
}, },
ParsedAlbumInfo = new ParsedAlbumInfo ParsedAlbumInfo = new ParsedAlbumInfo

@ -10,7 +10,7 @@ namespace NzbDrone.Core.Test.IndexerTests
{ {
public override string Name => "Test Indexer"; public override string Name => "Test Indexer";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override string Protocol => nameof(UsenetDownloadProtocol);
public int _supportedPageSize; public int _supportedPageSize;
public override int PageSize => _supportedPageSize; public override int PageSize => _supportedPageSize;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save