New: Plugin support

This commit is contained in:
ta264 2022-07-19 21:08:53 +01:00
commit 311322651a
167 changed files with 3178 additions and 773 deletions

View file

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

View file

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

View file

@ -28,6 +28,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 UpdatesConnector from 'System/Updates/UpdatesConnector'; import UpdatesConnector from 'System/Updates/UpdatesConnector';
@ -231,6 +232,11 @@ function AppRoutes(props) {
component={UpdatesConnector} component={UpdatesConnector}
/> />
<Route
path="/system/plugins"
component={PluginsConnector}
/>
<Route <Route
path="/system/events" path="/system/events"
component={LogsTableConnector} component={LogsTableConnector}

View file

@ -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';
@ -21,3 +22,4 @@ export const RESET_API_KEY = 'ResetApiKey';
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';

View file

@ -35,6 +35,13 @@ class FilterBuilderModalContent extends Component {
}; };
} }
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
this.props.dispatchFetchIndexers();
}
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
id, id,
@ -218,9 +225,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
}; };

View file

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

View file

@ -9,7 +9,7 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector'; import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValueConnector';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';

View file

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

View file

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

View file

@ -160,6 +160,10 @@ const links = [
title: 'Updates', title: 'Updates',
to: '/system/updates' to: '/system/updates'
}, },
{
title: 'Plugins',
to: '/system/plugins'
},
{ {
title: 'Events', title: 'Events',
to: '/system/events' to: '/system/events'

View file

@ -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,
@ -333,6 +334,7 @@ class SignalRConnector extends Component {
const { const {
dispatchFetchCommands, dispatchFetchCommands,
dispatchFetchArtist, dispatchFetchArtist,
dispatchClearMessages,
dispatchSetAppValue dispatchSetAppValue
} = this.props; } = this.props;
@ -346,7 +348,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();
}; };
@ -375,6 +379,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,

View file

@ -5,9 +5,9 @@ import { kinds } from 'Helpers/Props';
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 }) {
return ( return (
<div className={styles.tags}> <div className={className}>
{ {
tags.map((t) => { tags.map((t) => {
const tag = _.find(tagList, { id: t }); const tag = _.find(tagList, { id: t });
@ -31,8 +31,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;

View file

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

View file

@ -153,7 +153,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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(preferredProtocol);
if (!enableUsenet) {
preferred = 'Only Torrent';
} else if (!enableTorrent) {
preferred = 'Only Usenet';
}
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,

View file

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function getDelay(item) {
if (!item.allowed) {
return '-';
}
if (!item.delay) {
return 'No Delay';
}
if (item.delay === 1) {
return '1 Minute';
}
// TODO: use better units of time than just minutes
return `${item.delay} Minutes`;
}
function DelayProfileItem(props) {
const {
name,
allowed
} = props;
return (
<Label
kind={allowed ? kinds.INFO : kinds.DANGER}
>
{name}: {getDelay(props)}
</Label>
);
}
DelayProfileItem.propTypes = {
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired
};
export default DelayProfileItem;

View file

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

View file

@ -82,10 +82,10 @@ class DelayProfiles extends Component {
> >
<div> <div>
<div className={styles.delayProfilesHeader}> <div className={styles.delayProfilesHeader}>
<div className={styles.column}>Protocol</div> <div className={styles.name}>Name</div>
<div className={styles.column}>Usenet Delay</div> <div className={styles.fillcolumn}>Protocols</div>
<div className={styles.column}>Torrent Delay</div> <div className={styles.fillcolumn}>Tags</div>
<div className={styles.tags}>Tags</div> <div className={styles.actions} />
</div> </div>
<div className={styles.delayProfiles}> <div className={styles.delayProfiles}>

View file

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

View file

@ -0,0 +1,113 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import NumberInput from 'Components/Form/NumberInput';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import styles from './DownloadProtocolItem.css';
class DownloadProtocolItem extends Component {
//
// Listeners
onChange = ({ name, value }) => {
const {
protocol,
onDownloadProtocolItemFieldChange
} = this.props;
onDownloadProtocolItemFieldChange(protocol, name, value);
};
//
// Render
render() {
const {
isPreview,
name,
allowed,
delay,
isDragging,
isOverCurrent,
connectDragSource
} = this.props;
return (
<div
className={classNames(
styles.qualityProfileItem,
isDragging && styles.isDragging,
isPreview && styles.isPreview,
isOverCurrent && styles.isOverCurrent
)}
>
<label
className={styles.qualityNameContainer}
>
<CheckInput
className={styles.checkInput}
containerClassName={styles.checkInputContainer}
name={'allowed'}
value={allowed}
onChange={this.onChange}
/>
<div className={classNames(
styles.qualityName,
!allowed && styles.notAllowed
)}
>
{name}
</div>
</label>
<NumberInput
containerClassName={styles.delayContainer}
className={styles.delayInput}
name={'delay'}
value={delay}
min={0}
max={9999999}
onChange={this.onChange}
/>
{
connectDragSource(
<div className={styles.dragHandle}>
<Icon
className={styles.dragIcon}
title="Create group"
name={icons.REORDER}
/>
</div>
)
}
</div>
);
}
}
DownloadProtocolItem.propTypes = {
isPreview: PropTypes.bool,
protocol: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
delay: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
isOverCurrent: PropTypes.bool.isRequired,
connectDragSource: PropTypes.func,
onDownloadProtocolItemFieldChange: PropTypes.func
};
DownloadProtocolItem.defaultProps = {
isPreview: false,
isOverCurrent: false,
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};
export default DownloadProtocolItem;

View file

@ -0,0 +1,4 @@
.dragPreview {
width: 480px;
opacity: 0.75;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,150 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormLabel from 'Components/Form/FormLabel';
import Measure from 'Components/Measure';
import { sizes } from 'Helpers/Props';
import DownloadProtocolItemDragPreview from './DownloadProtocolItemDragPreview';
import DownloadProtocolItemDragSource from './DownloadProtocolItemDragSource';
import styles from './DownloadProtocolItems.css';
class DownloadProtocolItems extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
height: 0
};
}
//
// Listeners
onMeasure = ({ height }) => {
this.setState({ height });
};
//
// Render
render() {
const {
dropIndex,
dropPosition,
items,
errors,
warnings,
...otherProps
} = this.props;
const {
height
} = this.state;
const isDragging = dropIndex !== null;
const isDraggingUp = isDragging && dropPosition === 'above';
const isDraggingDown = isDragging && dropPosition === 'below';
return (
<FormGroup size={sizes.SMALL}>
<FormLabel size={sizes.SMALL}>
Download Protocols
</FormLabel>
<div>
<FormInputHelpText
text="Protocols higher in the list are more preferred. Only checked protocols are allowed"
/>
{
errors.map((error, index) => {
return (
<FormInputHelpText
key={index}
text={error.message}
isError={true}
isCheckInput={false}
/>
);
})
}
{
warnings.map((warning, index) => {
return (
<FormInputHelpText
key={index}
text={warning.message}
isWarning={true}
isCheckInput={false}
/>
);
})
}
<Measure
whitelist={['height']}
includeMargin={false}
onMeasure={this.onMeasure}
>
<div
className={styles.qualities}
style={{ minHeight: `${height}px` }}
>
<div className={styles.headerContainer}>
<div className={styles.headerTitle}>
Protocol
</div>
<div className={styles.headerDelay}>
Delay (minutes)
</div>
</div>
{
items.map(({ protocol, name, allowed, delay }, index) => {
return (
<DownloadProtocolItemDragSource
key={protocol}
protocol={protocol}
name={name}
allowed={allowed}
delay={delay}
index={index}
isDragging={isDragging}
isDraggingUp={isDraggingUp}
isDraggingDown={isDraggingDown}
{...otherProps}
/>
);
})
}
<DownloadProtocolItemDragPreview />
</div>
</Measure>
</div>
</FormGroup>
);
}
}
DownloadProtocolItems.propTypes = {
dragIndex: PropTypes.number,
dropIndex: PropTypes.number,
dropPosition: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object)
};
DownloadProtocolItems.defaultProps = {
errors: [],
warnings: []
};
export default DownloadProtocolItems;

View file

@ -12,23 +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 { 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';
function EditDelayProfileModalContent(props) { function EditDelayProfileModalContent(props) {
const { const {
id, id,
isFetching, isFetching,
isPopulated,
error, error,
isSaving, isSaving,
saveError, saveError,
item, item,
protocol,
protocolOptions,
onInputChange, onInputChange,
onProtocolChange,
onSavePress, onSavePress,
onModalClose, onModalClose,
onDeleteDelayProfilePress, onDeleteDelayProfilePress,
@ -36,10 +35,8 @@ function EditDelayProfileModalContent(props) {
} = props; } = props;
const { const {
enableUsenet, name,
enableTorrent, items,
usenetDelay,
torrentDelay,
tags tags
} = item; } = item;
@ -58,72 +55,43 @@ function EditDelayProfileModalContent(props) {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div> <div>
{translate('UnableToAddANewQualityProfilePleaseTryAgain')} {translate('UnableToAddANewDelayProfilePleaseTryAgain')}
</div> </div>
} }
{ {
!isFetching && !error && !isFetching && isPopulated && !error &&
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup> <FormGroup size={sizes.SMALL}>
<FormLabel> <FormLabel size={sizes.SMALL}>
{translate('Protocol')} {translate('Name')}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.TEXT}
name="protocol" name="name"
value={protocol} {...name}
values={protocolOptions}
helpText={translate('ProtocolHelpText')}
onChange={onProtocolChange}
/>
</FormGroup>
{
enableUsenet.value &&
<FormGroup>
<FormLabel>
{translate('UsenetDelay')}
</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="usenetDelay"
unit="minutes"
{...usenetDelay}
helpText={translate('UsenetDelayHelpText')}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
}
{ <div className={styles.formGroupWrapper}>
enableTorrent.value && <DownloadProtocolItems
<FormGroup> items={items.value}
<FormLabel> errors={items.errors}
{translate('TorrentDelay')} warnings={items.warnings}
</FormLabel> {...otherProps}
<FormInputGroup
type={inputTypes.NUMBER}
name="torrentDelay"
unit="minutes"
{...torrentDelay}
helpText={translate('TorrentDelayHelpText')}
onChange={onInputChange}
/> />
</FormGroup> </div>
}
{ {
id === 1 ? id === 1 ?
<Alert> <Alert>
This is the default profile. It applies to all artist that don't have an explicit profile. 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> <FormLabel size={sizes.SMALL}>
{translate('Tags')} {translate('Tags')}
</FormLabel> </FormLabel>
@ -170,10 +138,8 @@ function EditDelayProfileModalContent(props) {
} }
const delayProfileShape = { const delayProfileShape = {
enableUsenet: PropTypes.shape(boolSettingShape).isRequired, name: PropTypes.shape(stringSettingShape).isRequired,
enableTorrent: PropTypes.shape(boolSettingShape).isRequired, items: PropTypes.object.isRequired,
usenetDelay: PropTypes.shape(numberSettingShape).isRequired,
torrentDelay: PropTypes.shape(numberSettingShape).isRequired,
order: PropTypes.shape(numberSettingShape), order: PropTypes.shape(numberSettingShape),
tags: PropTypes.shape(tagSettingShape).isRequired tags: PropTypes.shape(tagSettingShape).isRequired
}; };
@ -181,14 +147,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,
protocolOptions: PropTypes.arrayOf(PropTypes.object).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

View file

@ -3,82 +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,
tags: []
};
const protocolOptions = [
{ key: 'preferUsenet', value: 'Prefer Usenet' },
{ key: 'preferTorrent', value: 'Prefer Torrent' },
{ key: 'onlyUsenet', value: 'Only Usenet' },
{ key: 'onlyTorrent', value: 'Only Torrent' }
];
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,
protocolOptions,
...delayProfile ...delayProfile
}; };
} }
@ -86,6 +19,7 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchDelayProfileSchema,
setDelayProfileValue, setDelayProfileValue,
saveDelayProfile saveDelayProfile
}; };
@ -95,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]
});
});
} }
} }
@ -119,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
});
}; };
// //
@ -156,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}
/> />
); );
} }
@ -167,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

View file

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

View file

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

View file

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

View file

@ -62,21 +62,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}
/> />
); );
}) })

View file

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

View file

@ -49,6 +49,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';
@ -65,6 +66,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);
// //
@ -173,6 +175,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);

View file

@ -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'),

View file

@ -0,0 +1,11 @@
.version {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}

View file

@ -0,0 +1,79 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import styles from './PluginRow.css';
class PluginRow extends Component {
//
// Listeners
onInstallPluginPress = () => {
this.props.onInstallPluginPress(this.props.githubUrl);
};
onUninstallPluginPress = () => {
this.props.onUninstallPluginPress(this.props.githubUrl);
};
//
// Render
render() {
const {
name,
owner,
installedVersion,
availableVersion,
updateAvailable,
isInstallingPlugin,
isUninstallingPlugin
} = this.props;
return (
<TableRow>
<TableRowCell>{name}</TableRowCell>
<TableRowCell>{owner}</TableRowCell>
<TableRowCell className={styles.version}>{installedVersion}</TableRowCell>
<TableRowCell className={styles.version}>{availableVersion}</TableRowCell>
<TableRowCell
className={styles.actions}
>
{
updateAvailable &&
<SpinnerIconButton
name={icons.UPDATE}
kind={kinds.DEFAULT}
isSpinning={isInstallingPlugin}
onPress={this.onInstallPluginPress}
/>
}
<SpinnerIconButton
name={icons.DELETE}
kind={kinds.DEFAULT}
isSpinning={isUninstallingPlugin}
onPress={this.onUninstallPluginPress}
/>
</TableRowCell>
</TableRow>
);
}
}
PluginRow.propTypes = {
githubUrl: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
owner: PropTypes.string.isRequired,
installedVersion: PropTypes.string.isRequired,
availableVersion: PropTypes.string.isRequired,
updateAvailable: PropTypes.bool.isRequired,
isInstallingPlugin: PropTypes.bool.isRequired,
onInstallPluginPress: PropTypes.func.isRequired,
isUninstallingPlugin: PropTypes.bool.isRequired,
onUninstallPluginPress: PropTypes.func.isRequired
};
export default PluginRow;

View file

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

View file

@ -0,0 +1,168 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { inputTypes, kinds } from 'Helpers/Props';
import PluginRow from './PluginRow';
const columns = [
{
name: 'name',
label: 'Name',
isVisible: true
},
{
name: 'owner',
label: 'Owner',
isVisible: true
},
{
name: 'installedVersion',
label: 'Installed Version',
isVisible: true
},
{
name: 'availableVersion',
label: 'Available Version',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class Plugins extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
repoUrl: ''
};
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({
[name]: value
});
};
onInstallPluginPress = () => {
this.props.onInstallPluginPress(this.state.repoUrl);
};
//
// Render
render() {
const {
isPopulated,
error,
items,
isInstallingPlugin,
onInstallPluginPress,
isUninstallingPlugin,
onUninstallPluginPress
} = this.props;
const {
repoUrl
} = this.state;
const noPlugins = isPopulated && !error && !items.length;
return (
<PageContent title="Plugins">
<PageContentBody>
<Form>
<FieldSet legend="Install New Plugin">
<FormGroup>
<FormLabel>GitHub URL</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="repoUrl"
helpText="URL to GitHub repository containing plugin"
helpLink="https://wiki.servarr.com/Lidarr_FAQ#How_do_I_install_plugins"
value={repoUrl}
onChange={this.onInputChange}
/>
</FormGroup>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isInstallingPlugin}
onPress={this.onInstallPluginPress}
>
Install
</SpinnerButton>
</FieldSet>
</Form>
<FieldSet legend="Installed Plugins">
{
!isPopulated && !error &&
<LoadingIndicator />
}
{
isPopulated && noPlugins &&
<div>No plugins are installed</div>
}
{
isPopulated && !noPlugins &&
<Table
columns={columns}
>
<TableBody>
{
items.map((plugin) => {
return (
<PluginRow
key={plugin.githubUrl}
{...plugin}
isInstallingPlugin={isInstallingPlugin}
isUninstallingPlugin={isUninstallingPlugin}
onInstallPluginPress={onInstallPluginPress}
onUninstallPluginPress={onUninstallPluginPress}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
</PageContentBody>
</PageContent>
);
}
}
Plugins.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.array.isRequired,
isInstallingPlugin: PropTypes.bool.isRequired,
onInstallPluginPress: PropTypes.func.isRequired,
isUninstallingPlugin: PropTypes.bool.isRequired,
onUninstallPluginPress: PropTypes.func.isRequired
};
export default Plugins;

View file

@ -0,0 +1,95 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchInstalledPlugins } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Plugins from './Plugins';
function createMapStateToProps() {
return createSelector(
(state) => state.system.plugins,
createCommandExecutingSelector(commandNames.INSTALL_PLUGIN),
createCommandExecutingSelector(commandNames.UNINSTALL_PLUGIN),
(
plugins,
isInstallingPlugin,
isUninstallingPlugin
) => {
return {
...plugins,
isInstallingPlugin,
isUninstallingPlugin
};
}
);
}
const mapDispatchToProps = {
dispatchFetchInstalledPlugins: fetchInstalledPlugins,
dispatchExecuteCommand: executeCommand
};
class PluginsConnector extends Component {
//
// Lifecycle
componentDidMount() {
registerPagePopulator(this.repopulate);
this.repopulate();
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.dispatchFetchInstalledPlugins();
};
//
// Listeners
onInstallPluginPress = (url) => {
this.props.dispatchExecuteCommand({
name: commandNames.INSTALL_PLUGIN,
githubUrl: url
});
};
onUninstallPluginPress = (url) => {
this.props.dispatchExecuteCommand({
name: commandNames.UNINSTALL_PLUGIN,
githubUrl: url
});
};
//
// Render
render() {
return (
<Plugins
onInstallPluginPress={this.onInstallPluginPress}
onUninstallPluginPress={this.onUninstallPluginPress}
{...this.props}
/>
);
}
}
PluginsConnector.propTypes = {
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Lidarr.Api.V1.Artist; using Lidarr.Api.V1.Artist;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace Lidarr.Api.V1.Blocklist namespace Lidarr.Api.V1.Blocklist
@ -14,7 +13,7 @@ namespace Lidarr.Api.V1.Blocklist
public string SourceTitle { get; set; } public string SourceTitle { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { 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; }

View file

@ -1,12 +1,12 @@
using NzbDrone.Core.Download; using System;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Download;
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; }

View file

@ -1,3 +1,4 @@
using System;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
namespace Lidarr.Api.V1.Indexers namespace Lidarr.Api.V1.Indexers
@ -9,7 +10,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; }
} }

View file

@ -46,7 +46,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; }
// Sent when queuing an unknown release // Sent when queuing an unknown release
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
@ -113,7 +113,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
{ {

View file

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

View file

@ -0,0 +1,48 @@
using System;
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
};
}
}
}

View file

@ -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 int Order { get; set; } public int Order { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
} }
@ -29,12 +25,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,
Order = model.Order, Order = model.Order,
Tags = new HashSet<int>(model.Tags) Tags = new HashSet<int>(model.Tags)
}; };
@ -50,12 +42,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,
Order = resource.Order, Order = resource.Order,
Tags = new HashSet<int>(resource.Tags) Tags = new HashSet<int>(resource.Tags)
}; };

View file

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

View file

@ -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); definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract);
return definition; return definition;

View file

@ -29,7 +29,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 string Indexer { get; set; } public string Indexer { get; set; }
public string OutputPath { get; set; } public string OutputPath { get; set; }

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Plugins;
namespace Lidarr.Api.V1.System.Plugins
{
[V1ApiController("system/plugins")]
public class PluginController : Controller
{
private readonly IPluginService _pluginService;
public PluginController(IPluginService pluginService)
{
_pluginService = pluginService;
}
[HttpGet]
public List<PluginResource> GetInstalledPlugins()
{
return _pluginService.GetInstalledPlugins().ToResource();
}
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http.REST;
using NzbDrone.Core.Plugins;
namespace Lidarr.Api.V1.System.Plugins
{
public class PluginResource : RestResource
{
public string Name { get; set; }
public string Owner { get; set; }
public string GithubUrl { get; set; }
public string InstalledVersion { get; set; }
public string AvailableVersion { get; set; }
public bool UpdateAvailable { get; set; }
}
public static class PluginResourceMapper
{
public static PluginResource ToResource(this IPlugin plugin)
{
return new PluginResource
{
Name = plugin.Name,
Owner = plugin.Owner,
GithubUrl = plugin.GithubUrl,
InstalledVersion = plugin.InstalledVersion.ToString(),
AvailableVersion = plugin.AvailableVersion.ToString(),
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
};
}
public static List<PluginResource> ToResource(this IEnumerable<IPlugin> plugins)
{
return plugins.Select(ToResource).ToList();
}
}
}

View file

@ -12,7 +12,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 +23,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();
} }

View file

@ -6,13 +6,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
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;
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
@ -23,9 +23,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"));

View file

@ -3,23 +3,43 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using NzbDrone.Common.EnvironmentInfo; 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 IEnumerable<Assembly> Load(IEnumerable<string> assemblies) public static List<Assembly> LoadBaseAssemblies()
{
return Load(BaseAssemblies);
}
public static List<Assembly> LoadUpdateAssemblies()
{
return Load(UpdateAssemblies);
}
private static List<Assembly> Load(IEnumerable<string> assemblies)
{ {
var toLoad = assemblies.ToList(); var toLoad = assemblies.ToList();
toLoad.Add("Lidarr.Common"); toLoad.Add("Lidarr.Common");
@ -28,7 +48,7 @@ namespace NzbDrone.Common.Composition
var startupPath = AppDomain.CurrentDomain.BaseDirectory; var startupPath = AppDomain.CurrentDomain.BaseDirectory;
return toLoad.Select(x => return toLoad.Select(x =>
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll"))); AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll"))).ToList();
} }
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args) private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)

View file

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

View file

@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace NzbDrone.Common.Composition
{
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
: base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string 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)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}

View file

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Composition
{
public static class PluginLoader
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PluginLoader));
public static (List<Assembly>, List<WeakReference>) LoadPlugins(IEnumerable<string> pluginPaths)
{
var assemblies = new List<Assembly>();
var pluginRefs = new List<WeakReference>();
foreach (var pluginPath in pluginPaths)
{
(var plugin, var pluginRef) = LoadPlugin(pluginPath);
pluginRefs.Add(pluginRef);
assemblies.Add(plugin);
}
return (assemblies, pluginRefs);
}
public static bool UnloadPlugins(List<WeakReference> pluginRefs)
{
RequestPluginUnload(pluginRefs);
return AwaitPluginUnload(pluginRefs);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static (Assembly, WeakReference) LoadPlugin(string path)
{
var context = new PluginLoadContext(path);
var weakRef = new WeakReference(context, trackResurrection: true);
// load from stream to avoid locking on windows
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
var assembly = context.LoadFromStream(fs);
return (assembly, weakRef);
}
private static void RequestPluginUnload(List<WeakReference> pluginRefs)
{
foreach (var pluginRef in pluginRefs)
{
if (pluginRef?.Target != null)
{
((PluginLoadContext)pluginRef.Target).Unload();
}
}
}
private static bool AwaitPluginUnload(List<WeakReference> pluginRefs)
{
var i = 0;
foreach (var pluginRef in pluginRefs.Where(x => x != null))
{
while (pluginRef.IsAlive)
{
GC.Collect();
GC.WaitForPendingFinalizers();
if (i++ >= 10)
{
return false;
}
}
}
return true;
}
}
}

View file

@ -0,0 +1,7 @@
namespace NzbDrone.Common.Composition
{
public class PluginStatus
{
public bool Enabled { get; set; }
}
}

View file

@ -24,6 +24,7 @@ namespace NzbDrone.Common.Extensions
private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "lidarr_appdata_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "lidarr_appdata_backup" + Path.DirectorySeparatorChar;
private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Lidarr.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Lidarr.Update" + Path.DirectorySeparatorChar;
private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar;
private static readonly string PLUGIN_FOLDER_NAME = "plugins";
private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(?<!:)\\$", RegexOptions.Compiled); private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(?<!:)\\$", RegexOptions.Compiled);
@ -301,6 +302,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");

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); GivenPreferredDownloadProtocol(nameof(UsenetDownloadProtocol));
} }
private Album GivenAlbum(int id) private Album GivenAlbum(int id)
@ -34,7 +34,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();
@ -60,14 +60,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]
@ -162,33 +162,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]
@ -246,7 +246,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();
@ -272,7 +272,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;
@ -299,7 +299,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;
@ -326,7 +326,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();
@ -375,7 +375,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();

View file

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

View file

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

View file

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

View file

@ -33,9 +33,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)
@ -54,7 +52,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();
@ -103,7 +101,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();
} }
@ -111,7 +109,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();
} }
@ -130,7 +128,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();
} }
@ -141,7 +139,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();
} }
@ -159,7 +157,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();
} }
@ -177,7 +175,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();
} }
@ -190,7 +188,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();
} }

View file

@ -37,7 +37,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();
@ -242,19 +242,19 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
public void should_not_add_to_failed_if_failed_for_a_different_protocol() public void 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))) Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(UsenetDownloadProtocol))))
.Throws(new DownloadClientUnavailableException("Download client failed")); .Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once()); Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(UsenetDownloadProtocol))), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once()); Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == nameof(TorrentDownloadProtocol))), Times.Once());
} }
[Test] [Test]

View file

@ -46,7 +46,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;
} }
@ -63,7 +63,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;
} }
@ -96,11 +96,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);
@ -117,11 +117,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);
@ -137,10 +137,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);
@ -158,11 +158,11 @@ 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));
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(4); client2.Definition.Id.Should().Be(4);
@ -182,11 +182,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);
@ -202,11 +202,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);
@ -224,11 +224,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);
@ -245,11 +245,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);
@ -267,7 +267,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));
} }
} }
} }

View file

@ -31,8 +31,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>())) .Setup(v => v.GetDownloadClient(It.IsAny<string>(), It.IsAny<int>()))
.Returns<DownloadProtocol, int>((v, i) => _downloadClients.FirstOrDefault(d => d.Protocol == v)); .Returns<string, int>((v, i) => _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)
@ -41,7 +41,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();
@ -59,7 +59,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;
} }
@ -71,7 +71,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;
} }
@ -246,7 +246,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);
Subject.DownloadReport(_parseResult); Subject.DownloadReport(_parseResult);

View file

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

View file

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

View file

@ -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"
}, },

View file

@ -41,7 +41,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();

View file

@ -53,7 +53,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&usetoken=0"); .Be("http://someurl.ch/torrents.php?action=download&id=1541452&authkey=redacted&torrent_pass=redacted&usetoken=0");
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");

View file

@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests
var releaseInfo = releases.First() as ReleaseInfo; var releaseInfo = releases.First() as ReleaseInfo;
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.BasicAuthString.Should().Be("dXNlcjpwYXNz"); releaseInfo.BasicAuthString.Should().Be("dXNlcjpwYXNz");
releaseInfo.Indexer.Should().Be(Subject.Definition.Name); releaseInfo.Indexer.Should().Be(Subject.Definition.Name);

View file

@ -99,7 +99,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();

View file

@ -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)
{ {

View file

@ -53,7 +53,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");

View file

@ -41,7 +41,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();

View file

@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests
var releaseInfo = releases.First(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV"); releaseInfo.Title.Should().Be("Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); releaseInfo.DownloadProtocol.Should().Be(nameof(UsenetDownloadProtocol));
releaseInfo.DownloadUrl.Should().Be("http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone"); releaseInfo.DownloadUrl.Should().Be("http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone");
releaseInfo.InfoUrl.Should().Be("http://omgwtfnzbs.org/details.php?id=OAl4g"); releaseInfo.InfoUrl.Should().Be("http://omgwtfnzbs.org/details.php?id=OAl4g");
releaseInfo.CommentUrl.Should().BeNullOrEmpty(); releaseInfo.CommentUrl.Should().BeNullOrEmpty();

View file

@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT"); torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce"); torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=Lidarr"); torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=Lidarr");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);

View file

@ -52,7 +52,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("https://redacted.ch/torrents.php?action=download&id=1541452&authkey=lidarr&torrent_pass=redacted&usetoken=0"); .Be("https://redacted.ch/torrents.php?action=download&id=1541452&authkey=lidarr&torrent_pass=redacted&usetoken=0");
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");

View file

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

View file

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

View file

@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = (TorrentInfo)releases.First(); var torrentInfo = (TorrentInfo)releases.First();
torrentInfo.Title.Should().Be("Conan.2015.02.05.Jeff.Bridges.720p.HDTV.X264-CROOKS"); torrentInfo.Title.Should().Be("Conan.2015.02.05.Jeff.Bridges.720p.HDTV.X264-CROOKS");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("https://immortalseed.me/download.php?type=rss&secret_key=12345678910&id=374534"); torrentInfo.DownloadUrl.Should().Be("https://immortalseed.me/download.php?type=rss&secret_key=12345678910&id=374534");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("S4C I Grombil Cyfandir Pell American Interior [PDTV - MVGROUP]"); torrentInfo.Title.Should().Be("S4C I Grombil Cyfandir Pell American Interior [PDTV - MVGROUP]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://re.zoink.it/20a4ed4eFC"); torrentInfo.DownloadUrl.Should().Be("http://re.zoink.it/20a4ed4eFC");
torrentInfo.InfoUrl.Should().Be("http://eztv.it/ep/58439/s4c-i-grombil-cyfandir-pell-american-interior-pdtv-x264-mvgroup/"); torrentInfo.InfoUrl.Should().Be("http://eztv.it/ep/58439/s4c-i-grombil-cyfandir-pell-american-interior-pdtv-x264-mvgroup/");
torrentInfo.CommentUrl.Should().Be("http://eztv.it/forum/discuss/58439/"); torrentInfo.CommentUrl.Should().Be("http://eztv.it/forum/discuss/58439/");
@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("The Voice 8x25"); torrentInfo.Title.Should().Be("The Voice 8x25");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:96CD620BEDA3EFD7C4D7746EF94549D03A2EB13B&dn=The+Voice+S08E25+WEBRip+x264+WNN&tr=udp://tracker.coppersurfer.tk:6969/announce&tr=udp://tracker.leechers-paradise.org:6969&tr=udp://open.demonii.com:1337"); torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:96CD620BEDA3EFD7C4D7746EF94549D03A2EB13B&dn=The+Voice+S08E25+WEBRip+x264+WNN&tr=udp://tracker.coppersurfer.tk:6969/announce&tr=udp://tracker.leechers-paradise.org:6969&tr=udp://open.demonii.com:1337");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[Doki] PriPara 50 (848x480 h264 AAC) [6F0B49FD] mkv"); torrentInfo.Title.Should().Be("[Doki] PriPara 50 (848x480 h264 AAC) [6F0B49FD] mkv");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://tracker.anime-index.org/download.php?id=82d8ad84403e01a7786130905ca169a3429e657f&f=%5BDoki%5D+PriPara+-+50+%28848x480+h264+AAC%29+%5B6F0B49FD%5D.mkv.torrent"); torrentInfo.DownloadUrl.Should().Be("http://tracker.anime-index.org/download.php?id=82d8ad84403e01a7786130905ca169a3429e657f&f=%5BDoki%5D+PriPara+-+50+%28848x480+h264+AAC%29+%5B6F0B49FD%5D.mkv.torrent");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("One.Piece.E334.D ED.720p.HDTV.x264-W4F-={SPARROW}=-"); torrentInfo.Title.Should().Be("One.Piece.E334.D ED.720p.HDTV.x264-W4F-={SPARROW}=-");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://ac.me/download/4722030/One.Piece.E334.D+ED.720p.HDTV.x264-W4F-%3D%7BSPARROW%7D%3D-.torrent"); torrentInfo.DownloadUrl.Should().Be("http://ac.me/download/4722030/One.Piece.E334.D+ED.720p.HDTV.x264-W4F-%3D%7BSPARROW%7D%3D-.torrent");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("The Expanse 2x04 (720p-HDTV-x264-SVA)[VTV]"); torrentInfo.Title.Should().Be("The Expanse 2x04 (720p-HDTV-x264-SVA)[VTV]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://itorrents.org/torrent/51C578C9823DD58F6EEA287C368ED935843D63AB.torrent?title=The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]"); torrentInfo.DownloadUrl.Should().Be("http://itorrents.org/torrent/51C578C9823DD58F6EEA287C368ED935843D63AB.torrent?title=The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().Be("http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html"); torrentInfo.CommentUrl.Should().Be("http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html");
@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[FFF] Ore Monogatari!! - Vol.01 [BD][720p-AAC]"); torrentInfo.Title.Should().Be("[FFF] Ore Monogatari!! - Vol.01 [BD][720p-AAC]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://storage.animetosho.org/torrents/85a570f25067f69b3c83b901ce6c00c491345288/%5BFFF%5D%20Ore%20Monogatari%21%21%20-%20Vol.01%20%5BBD%5D%5B720p-AAC%5D.torrent"); torrentInfo.DownloadUrl.Should().Be("http://storage.animetosho.org/torrents/85a570f25067f69b3c83b901ce6c00c491345288/%5BFFF%5D%20Ore%20Monogatari%21%21%20-%20Vol.01%20%5BBD%5D%5B720p-AAC%5D.torrent");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().Be("https://animetosho.org/view/fff-ore-monogatari-vol-01-bd-720p-aac.1009077"); torrentInfo.CommentUrl.Should().Be("https://animetosho.org/view/fff-ore-monogatari-vol-01-bd-720p-aac.1009077");
@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.Last() as TorrentInfo; var torrentInfo = releases.Last() as TorrentInfo;
torrentInfo.Title.Should().Be("DAYS - 05 (1280x720 HEVC2 AAC).mkv"); torrentInfo.Title.Should().Be("DAYS - 05 (1280x720 HEVC2 AAC).mkv");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://storage.animetosho.org/torrents/4b58360143d59a55cbd922397a3eaa378165f3ff/DAYS%20-%2005%20%281280x720%20HEVC2%20AAC%29.torrent"); torrentInfo.DownloadUrl.Should().Be("http://storage.animetosho.org/torrents/4b58360143d59a55cbd922397a3eaa378165f3ff/DAYS%20-%2005%20%281280x720%20HEVC2%20AAC%29.torrent");
} }
@ -255,7 +255,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.Last() as TorrentInfo; var torrentInfo = releases.Last() as TorrentInfo;
torrentInfo.Title.Should().Be("TvHD 465860 465831 WWE.RAW.2016.11.28.720p.HDTV.x264-KYR"); torrentInfo.Title.Should().Be("TvHD 465860 465831 WWE.RAW.2016.11.28.720p.HDTV.x264-KYR");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("https://alpharatio.cc/torrents.php?action=download&authkey=private_auth_key&torrent_pass=private_torrent_pass&id=465831"); torrentInfo.DownloadUrl.Should().Be("https://alpharatio.cc/torrents.php?action=download&authkey=private_auth_key&torrent_pass=private_torrent_pass&id=465831");
} }
@ -273,7 +273,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[TVShow --> TVShow Bluray 720p] Fargo S01 Complete Season 1 720p BRRip DD5.1 x264-PSYPHER [SEEDERS (3)/LEECHERS (0)]"); torrentInfo.Title.Should().Be("[TVShow --> TVShow Bluray 720p] Fargo S01 Complete Season 1 720p BRRip DD5.1 x264-PSYPHER [SEEDERS (3)/LEECHERS (0)]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://ew.pw/download.php?id=dea071a7a62a0d662538d46402fb112f30b8c9fa&f=Fargo%20S01%20Complete%20Season%201%20720p%20BRRip%20DD5.1%20x264-PSYPHER.torrent&auth=secret"); torrentInfo.DownloadUrl.Should().Be("http://ew.pw/download.php?id=dea071a7a62a0d662538d46402fb112f30b8c9fa&f=Fargo%20S01%20Complete%20Season%201%20720p%20BRRip%20DD5.1%20x264-PSYPHER.torrent&auth=secret");
torrentInfo.InfoUrl.Should().BeNullOrEmpty(); torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.CommentUrl.Should().BeNullOrEmpty();

View file

@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentleechTests
var torrentInfo = releases.First() as TorrentInfo; var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Classic Car Rescue S02E04 720p HDTV x264-C4TV"); torrentInfo.Title.Should().Be("Classic Car Rescue S02E04 720p HDTV x264-C4TV");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
torrentInfo.DownloadUrl.Should().Be("http://www.torrentleech.org/rss/download/513575/1234/Classic.Car.Rescue.S02E04.720p.HDTV.x264-C4TV.torrent"); torrentInfo.DownloadUrl.Should().Be("http://www.torrentleech.org/rss/download/513575/1234/Classic.Car.Rescue.S02E04.720p.HDTV.x264-C4TV.torrent");
torrentInfo.InfoUrl.Should().Be("http://www.torrentleech.org/torrent/513575"); torrentInfo.InfoUrl.Should().Be("http://www.torrentleech.org/torrent/513575");
torrentInfo.CommentUrl.Should().Be("http://www.torrentleech.org/torrent/513575#comments"); torrentInfo.CommentUrl.Should().Be("http://www.torrentleech.org/torrent/513575#comments");

View file

@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var releaseInfo = releases.First() as TorrentInfo; var releaseInfo = releases.First() as TorrentInfo;
releaseInfo.Title.Should().Be("Better Call Saul S01E05 Alpine Shepherd 1080p NF WEBRip DD5.1 x264"); releaseInfo.Title.Should().Be("Better Call Saul S01E05 Alpine Shepherd 1080p NF WEBRip DD5.1 x264");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); releaseInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
releaseInfo.DownloadUrl.Should().Be("https://hdaccess.net/download.php?torrent=11515&passkey=123456"); releaseInfo.DownloadUrl.Should().Be("https://hdaccess.net/download.php?torrent=11515&passkey=123456");
releaseInfo.InfoUrl.Should().Be("https://hdaccess.net/details.php?id=11515&hit=1"); releaseInfo.InfoUrl.Should().Be("https://hdaccess.net/details.php?id=11515&hit=1");
releaseInfo.CommentUrl.Should().Be("https://hdaccess.net/details.php?id=11515&hit=1#comments"); releaseInfo.CommentUrl.Should().Be("https://hdaccess.net/details.php?id=11515&hit=1#comments");
@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var releaseInfo = releases.First() as TorrentInfo; var releaseInfo = releases.First() as TorrentInfo;
releaseInfo.Title.Should().Be("Series Title S05E02 HDTV x264-Xclusive [eztv]"); releaseInfo.Title.Should().Be("Series Title S05E02 HDTV x264-Xclusive [eztv]");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); releaseInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
releaseInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969"); releaseInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969");
releaseInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969"); releaseInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:9fb267cff5ae5603f07a347676ec3bf3e35f75e1&dn=Game+of+Thrones+S05E02+HDTV+x264-Xclusive+%5Beztv%5D&tr=udp:%2F%2Fopen.demonii.com:1337&tr=udp:%2F%2Ftracker.coppersurfer.tk:6969&tr=udp:%2F%2Ftracker.leechers-paradise.org:6969&tr=udp:%2F%2Fexodus.desync.com:6969");
releaseInfo.InfoUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D"); releaseInfo.InfoUrl.Should().Be("https://thepiratebay.se/torrent/11811366/Series_Title_S05E02_HDTV_x264-Xclusive_%5Beztv%5D");

View file

@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.WafflesTests
var releaseInfo = releases.First(); var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Coldplay - Kaleidoscope EP (FLAC HD) [2017-Web-FLAC-Lossless]"); releaseInfo.Title.Should().Be("Coldplay - Kaleidoscope EP (FLAC HD) [2017-Web-FLAC-Lossless]");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); releaseInfo.DownloadProtocol.Should().Be(nameof(TorrentDownloadProtocol));
releaseInfo.DownloadUrl.Should().Be("https://waffles.ch/download.php/xxx/1166992/" + releaseInfo.DownloadUrl.Should().Be("https://waffles.ch/download.php/xxx/1166992/" +
"Coldplay%20-%20Kaleidoscope%20EP%20%28FLAC%20HD%29%20%5B2017-Web-FLAC-Lossless%5D.torrent?passkey=123456789&uid=xxx&rss=1"); "Coldplay%20-%20Kaleidoscope%20EP%20%28FLAC%20HD%29%20%5B2017-Web-FLAC-Lossless%5D.torrent?passkey=123456789&uid=xxx&rss=1");
releaseInfo.InfoUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1"); releaseInfo.InfoUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1");

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -17,7 +16,7 @@ namespace NzbDrone.Core.Blocklisting
public DateTime Date { get; set; } public DateTime Date { get; set; }
public DateTime? PublishedDate { get; set; } public DateTime? PublishedDate { get; set; }
public long? Size { get; set; } public long? Size { 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; }
public string TorrentInfoHash { get; set; } public string TorrentInfoHash { get; set; }

View file

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music.Events; using NzbDrone.Core.Music.Events;
@ -21,46 +19,32 @@ namespace NzbDrone.Core.Blocklisting
} }
public class BlocklistService : IBlocklistService, public class BlocklistService : IBlocklistService,
IExecute<ClearBlocklistCommand>, IExecute<ClearBlocklistCommand>,
IHandle<DownloadFailedEvent>, IHandle<DownloadFailedEvent>,
IHandleAsync<ArtistsDeletedEvent> IHandleAsync<ArtistsDeletedEvent>
{ {
private readonly IBlocklistRepository _blocklistRepository; private readonly IBlocklistRepository _blocklistRepository;
private readonly List<IBlocklistForProtocol> _protocolBlocklists;
public BlocklistService(IBlocklistRepository blocklistRepository) public BlocklistService(IBlocklistRepository blocklistRepository,
IEnumerable<IBlocklistForProtocol> protocolBlocklists)
{ {
_blocklistRepository = blocklistRepository; _blocklistRepository = blocklistRepository;
_protocolBlocklists = protocolBlocklists.ToList();
} }
public bool Blocklisted(int artistId, ReleaseInfo release) public bool Blocklisted(int artistId, ReleaseInfo release)
{ {
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(artistId, release.Title); var protocolBlocklist = _protocolBlocklists.FirstOrDefault(x => x.Protocol == release.DownloadProtocol);
if (release.DownloadProtocol == DownloadProtocol.Torrent) if (protocolBlocklist != null)
{ {
var torrentInfo = release as TorrentInfo; return protocolBlocklist.IsBlocklisted(artistId, release);
}
if (torrentInfo == null)
{
return false; return false;
} }
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
{
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
.Any(b => SameTorrent(b, torrentInfo));
}
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(artistId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
}
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
.Any(b => SameNzb(b, release));
}
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec) public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
{ {
return _blocklistRepository.GetPaged(pagingSpec); return _blocklistRepository.GetPaged(pagingSpec);
@ -76,66 +60,6 @@ namespace NzbDrone.Core.Blocklisting
_blocklistRepository.DeleteMany(ids); _blocklistRepository.DeleteMany(ids);
} }
private bool SameNzb(Blocklist item, ReleaseInfo release)
{
if (item.PublishedDate == release.PublishDate)
{
return true;
}
if (!HasSameIndexer(item, release.Indexer) &&
HasSamePublishedDate(item, release.PublishDate) &&
HasSameSize(item, release.Size))
{
return true;
}
return false;
}
private bool SameTorrent(Blocklist item, TorrentInfo release)
{
if (release.InfoHash.IsNotNullOrWhiteSpace())
{
return release.InfoHash.Equals(item.TorrentInfoHash);
}
return item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase);
}
private bool HasSameIndexer(Blocklist item, string indexer)
{
if (item.Indexer.IsNullOrWhiteSpace())
{
return true;
}
return item.Indexer.Equals(indexer, StringComparison.InvariantCultureIgnoreCase);
}
private bool HasSamePublishedDate(Blocklist item, DateTime publishedDate)
{
if (!item.PublishedDate.HasValue)
{
return true;
}
return item.PublishedDate.Value.AddMinutes(-2) <= publishedDate &&
item.PublishedDate.Value.AddMinutes(2) >= publishedDate;
}
private bool HasSameSize(Blocklist item, long size)
{
if (!item.Size.HasValue)
{
return true;
}
var difference = Math.Abs(item.Size.Value - size);
return difference <= 2.Megabytes();
}
public void Execute(ClearBlocklistCommand message) public void Execute(ClearBlocklistCommand message)
{ {
_blocklistRepository.Purge(); _blocklistRepository.Purge();
@ -143,23 +67,15 @@ namespace NzbDrone.Core.Blocklisting
public void Handle(DownloadFailedEvent message) public void Handle(DownloadFailedEvent message)
{ {
var blocklist = new Blocklist var protocolBlocklist = _protocolBlocklists.FirstOrDefault(x => x.Protocol == message.Data.GetValueOrDefault("protocol"));
if (protocolBlocklist != null)
{ {
ArtistId = message.ArtistId, var blocklist = protocolBlocklist.GetBlocklist(message);
AlbumIds = message.AlbumIds,
SourceTitle = message.SourceTitle,
Quality = message.Quality,
Date = DateTime.UtcNow,
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")),
Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
Indexer = message.Data.GetValueOrDefault("indexer"),
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
Message = message.Message,
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash")
};
_blocklistRepository.Insert(blocklist); _blocklistRepository.Insert(blocklist);
} }
}
public void HandleAsync(ArtistsDeletedEvent message) public void HandleAsync(ArtistsDeletedEvent message)
{ {

View file

@ -0,0 +1,12 @@
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Blocklisting
{
public interface IBlocklistForProtocol
{
string Protocol { get; }
bool IsBlocklisted(int artistId, ReleaseInfo release);
Blocklist GetBlocklist(DownloadFailedEvent message);
}
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Blocklisting
{
public class TorrentBlocklist : IBlocklistForProtocol
{
private readonly IBlocklistRepository _blocklistRepository;
public TorrentBlocklist(IBlocklistRepository blocklistRepository)
{
_blocklistRepository = blocklistRepository;
}
public string Protocol => nameof(TorrentDownloadProtocol);
public bool IsBlocklisted(int artistId, ReleaseInfo release)
{
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(artistId, release.Title)
.Where(b => b.Protocol == Protocol);
var torrentInfo = release as TorrentInfo;
if (torrentInfo == null)
{
return false;
}
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
{
return blocklistedByTitle.Where(b => b.Protocol == nameof(TorrentDownloadProtocol))
.Any(b => SameTorrent(b, torrentInfo));
}
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(artistId, torrentInfo.InfoHash);
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
}
public Blocklist GetBlocklist(DownloadFailedEvent message)
{
return new Blocklist
{
ArtistId = message.ArtistId,
AlbumIds = message.AlbumIds,
SourceTitle = message.SourceTitle,
Quality = message.Quality,
Date = DateTime.UtcNow,
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")),
Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
Indexer = message.Data.GetValueOrDefault("indexer"),
Protocol = message.Data.GetValueOrDefault("protocol"),
Message = message.Message,
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash")
};
}
private bool SameTorrent(Blocklist item, TorrentInfo release)
{
if (release.InfoHash.IsNotNullOrWhiteSpace())
{
return release.InfoHash.Equals(item.TorrentInfoHash);
}
return item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase);
}
}
}

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