Fixed: Backend Updates from Sonarr

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Co-Authored-By: taloth <taloth@users.noreply.github.com>
This commit is contained in:
Qstick 2019-06-30 21:50:01 -04:00
parent d178dce0d3
commit 91ab518dfb
131 changed files with 2422 additions and 988 deletions

View File

@ -9,15 +9,15 @@ function getIconName(eventType) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
case 'seriesFolderImported':
case 'movieFolderImported':
return icons.DRIVE;
case 'downloadFolderImported':
return icons.DOWNLOADED;
case 'downloadFailed':
return icons.DOWNLOADING;
case 'episodeFileDeleted':
case 'movieFileDeleted':
return icons.DELETE;
case 'episodeFileRenamed':
case 'movieFileRenamed':
return icons.ORGANIZE;
default:
return icons.UNKNOWN;
@ -36,17 +36,17 @@ function getIconKind(eventType) {
function getTooltip(eventType, data) {
switch (eventType) {
case 'grabbed':
return `Episode grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
case 'seriesFolderImported':
return 'Episode imported from series folder';
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
case 'movieFolderImported':
return 'Movie imported from movie folder';
case 'downloadFolderImported':
return 'Episode downloaded successfully and picked up from download client';
return 'Movie downloaded successfully and picked up from download client';
case 'downloadFailed':
return 'Episode download failed';
case 'episodeFileDeleted':
return 'Episode file deleted';
case 'episodeFileRenamed':
return 'Episode file renamed';
return 'Movie download failed';
case 'movieFileDeleted':
return 'Movie file deleted';
case 'movieFileRenamed':
return 'Movie file renamed';
default:
return 'Unknown event';
}

View File

@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
import DeviceInput from './DeviceInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.devices,
(state) => state.providerOptions,
(value, devices) => {
return {
@ -37,8 +37,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
dispatchFetchDevices: fetchDevices,
dispatchClearDevices: clearDevices
dispatchFetchOptions: fetchOptions,
dispatchClearOptions: clearOptions
};
class DeviceInputConnector extends Component {
@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
}
componentWillUnmount = () => {
// this.props.dispatchClearDevices();
this.props.dispatchClearOptions();
}
//
@ -61,10 +61,14 @@ class DeviceInputConnector extends Component {
const {
provider,
providerData,
dispatchFetchDevices
dispatchFetchOptions
} = this.props;
dispatchFetchDevices({ provider, providerData });
dispatchFetchOptions({
action: 'getDevices',
provider,
providerData
});
}
//
@ -92,8 +96,8 @@ DeviceInputConnector.propTypes = {
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDevices: PropTypes.func.isRequired,
dispatchClearDevices: PropTypes.func.isRequired
dispatchFetchOptions: PropTypes.func.isRequired,
dispatchClearOptions: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);

View File

@ -6,7 +6,7 @@ import classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import isMobileUtil from 'Utilities/isMobile';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, scrollDirections } from 'Helpers/Props';
import { icons, sizes, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Portal from 'Components/Portal';
import Link from 'Components/Link/Link';
@ -14,8 +14,8 @@ import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import HintedSelectInputOption from './HintedSelectInputOption';
import styles from './EnhancedSelectInput.css';
function isArrowKey(keyCode) {
@ -150,9 +150,11 @@ class EnhancedSelectInput extends Component {
}
onBlur = () => {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
}
onKeyDown = (event) => {
@ -385,6 +387,7 @@ class EnhancedSelectInput extends Component {
isMobile &&
<Modal
className={styles.optionsModal}
size={sizes.EXTRA_SMALL}
isOpen={isOpen}
onModalClose={this.onOptionsModalClose}
>
@ -439,8 +442,8 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
selectedValueOptions: {},
selectedValueComponent: EnhancedSelectInputSelectedValue,
optionComponent: EnhancedSelectInputOption
selectedValueComponent: HintedSelectInputSelectedValue,
optionComponent: HintedSelectInputOption
};
export default EnhancedSelectInput;

View File

@ -7,13 +7,17 @@
cursor: default;
&:hover {
background-color: #f9f9f9;
background-color: #f8f8f8;
}
}
.isSelected {
background-color: #e2e2e2;
&:hover {
background-color: #e2e2e2;
}
&.isMobile {
background-color: inherit;

View File

@ -1,5 +1,6 @@
.inputGroupContainer {
flex: 1 1 auto;
min-width: 0;
}
.inputGroup {
@ -11,6 +12,7 @@
.inputContainer {
position: relative;
flex: 1 1 auto;
min-width: 0;
}
.inputUnit {

View File

@ -14,7 +14,7 @@ import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
import TagInputConnector from './TagInputConnector';
import TextTagInputConnector from './TextTagInputConnector';
import TextInput from './TextInput';
@ -60,7 +60,7 @@ function getComponent(type) {
return RootFolderSelectInputConnector;
case inputTypes.SELECT:
return SelectInput;
return EnhancedSelectInput;
case inputTypes.TAG:
return TagInputConnector;

View File

@ -0,0 +1,23 @@
.optionText {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 0;
min-width: 0;
&.isMobile {
display: block;
.hintText {
margin-left: 0;
}
}
}
.hintText {
@add-mixin truncate;
margin-left: 15px;
color: $darkGray;
font-size: $smallFontSize;
}

View File

@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import styles from './HintedSelectInputOption.css';
function HintedSelectInputOption(props) {
const {
value,
hint,
isMobile,
...otherProps
} = props;
return (
<EnhancedSelectInputOption
isMobile={isMobile}
{...otherProps}
>
<div className={classNames(
styles.optionText,
isMobile && styles.isMobile
)}
>
<div>{value}</div>
{
hint != null &&
<div className={styles.hintText}>
{hint}
</div>
}
</div>
</EnhancedSelectInputOption>
);
}
HintedSelectInputOption.propTypes = {
value: PropTypes.string.isRequired,
hint: PropTypes.node,
isMobile: PropTypes.bool.isRequired
};
export default HintedSelectInputOption;

View File

@ -0,0 +1,24 @@
.selectedValue {
composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css';
display: flex;
align-items: center;
justify-content: space-between;
overflow: hidden;
}
.valueText {
@add-mixin truncate;
flex: 0 0 auto;
}
.hintText {
@add-mixin truncate;
flex: 1 10 0;
margin-left: 15px;
color: $gray;
text-align: right;
font-size: $smallFontSize;
}

View File

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import React from 'react';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) {
const {
value,
hint,
includeHint,
...otherProps
} = props;
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.valueText}>
{value}
</div>
{
hint != null && includeHint &&
<div className={styles.hintText}>
{hint}
</div>
}
</EnhancedSelectInputSelectedValue>
);
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
hint: PropTypes.string,
includeHint: PropTypes.bool.isRequired
};
HintedSelectInputSelectedValue.defaultProps = {
includeHint: true
};
export default HintedSelectInputSelectedValue;

View File

@ -20,7 +20,7 @@ function getType(type) {
return inputTypes.NUMBER;
case 'path':
return inputTypes.PATH;
case 'filepath':
case 'filePath':
return inputTypes.PATH;
case 'select':
return inputTypes.SELECT;
@ -60,6 +60,7 @@ function ProviderFieldFormGroup(props) {
value,
type,
advanced,
hidden,
pending,
errors,
warnings,
@ -68,6 +69,13 @@ function ProviderFieldFormGroup(props) {
...otherProps
} = props;
if (
hidden === 'hidden' ||
(hidden === 'hiddenIfNotSet' && !value)
) {
return null;
}
return (
<FormGroup
advancedSettings={advancedSettings}
@ -86,7 +94,7 @@ function ProviderFieldFormGroup(props) {
errors={errors}
warnings={warnings}
pending={pending}
includeFiles={type === 'filepath' ? true : undefined}
includeFiles={type === 'filePath' ? true : undefined}
onChange={onChange}
{...otherProps}
/>
@ -108,6 +116,7 @@ ProviderFieldFormGroup.propTypes = {
value: PropTypes.any,
type: PropTypes.string.isRequired,
advanced: PropTypes.bool.isRequired,
hidden: PropTypes.string,
pending: PropTypes.bool.isRequired,
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@ -1,7 +1,6 @@
.input {
composes: input from '~./AutoSuggestInput.css';
position: relative;
padding: 0;
min-height: 35px;
height: auto;

View File

@ -1,5 +1,4 @@
.inputContainer {
position: absolute;
top: -1px;
right: -1px;
bottom: -1px;

View File

@ -128,6 +128,8 @@ class TextInput extends Component {
hasWarning,
hasButton,
step,
min,
max,
onBlur
} = this.props;
@ -148,6 +150,8 @@ class TextInput extends Component {
name={name}
value={value}
step={step}
min={min}
max={max}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={onBlur}
@ -171,6 +175,8 @@ TextInput.propTypes = {
hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
step: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,

View File

@ -47,7 +47,7 @@ class Link extends Component {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else if (to.startsWith(window.Radarr.urlBase)) {
} else if (to.startsWith(`${window.Radarr.urlBase}/`)) {
el = RouterLink;
linkProps.to = to;
linkProps.target = target;

View File

@ -154,8 +154,33 @@ class MovieSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
const fuse = new Fuse(this.props.movies, fuseOptions);
const suggestions = fuse.search(value);
const { movies } = this.props;
let suggestions = [];
if (value.length === 1) {
suggestions = movies.reduce((acc, s) => {
if (s.firstCharacter === value.toLowerCase()) {
acc.push({
item: s,
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
}
return acc;
}, []);
} else {
const fuse = new Fuse(movies, fuseOptions);
suggestions = fuse.search(value);
}
this.setState({ suggestions });
}

View File

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { push } from 'connected-react-router';
import { createSelector } from 'reselect';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import MovieSearchInput from './MovieSearchInput';
@ -26,9 +27,16 @@ function createCleanMovieSelector() {
sortTitle,
images,
alternateTitles,
tags: tags.map((id) => {
return allTags.find((tag) => tag.id === id);
})
firstCharacter: title.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id);
if (matchingTag) {
acc.push(matchingTag);
}
return acc;
}, [])
};
});
}
@ -36,7 +44,7 @@ function createCleanMovieSelector() {
}
function createMapStateToProps() {
return createSelector(
return createDeepEqualSelector(
createCleanMovieSelector(),
(movies) => {
return {

View File

@ -84,7 +84,7 @@ class SignalRConnector extends Component {
constructor(props, context) {
super(props, context);
this.signalRconnectionOptions = { transport: ['webSockets', 'longPolling'] };
this.signalRconnectionOptions = { transport: ['webSockets', 'serverSentEvents', 'longPolling'] };
this.signalRconnection = null;
this.retryInterval = 1;
this.retryTimeoutId = null;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -52,15 +52,15 @@ class DeleteMovieModalContent extends Component {
} = this.props;
const {
episodeFileCount,
movieFileCount,
sizeOnDisk
} = statistics;
const deleteFiles = this.state.deleteFiles;
let deleteFilesLabel = `Delete ${episodeFileCount} Movie Files`;
let deleteFilesLabel = `Delete ${movieFileCount} Movie Files`;
let deleteFilesHelpText = 'Delete the movie files and movie folder';
if (episodeFileCount === 0) {
if (movieFileCount === 0) {
deleteFilesLabel = 'Delete Movie Folder';
deleteFilesHelpText = 'Delete the movie folder and it\'s contents';
}
@ -102,8 +102,8 @@ class DeleteMovieModalContent extends Component {
<div>The movie folder <strong>{path}</strong> and all it's content will be deleted.</div>
{
!!episodeFileCount &&
<div>{episodeFileCount} movie files totaling {formatBytes(sizeOnDisk)}</div>
!!movieFileCount &&
<div>{movieFileCount} movie files totaling {formatBytes(sizeOnDisk)}</div>
}
</div>
}
@ -137,7 +137,7 @@ DeleteMovieModalContent.propTypes = {
DeleteMovieModalContent.defaultProps = {
statistics: {
episodeFileCount: 0
movieFileCount: 0
}
};

View File

@ -584,7 +584,7 @@ MovieDetails.propTypes = {
};
MovieDetails.defaultProps = {
tag: [],
tags: [],
isSaving: false,
sizeOnDisk: 0
};

View File

@ -254,10 +254,7 @@ MovieIndexPoster.propTypes = {
MovieIndexPoster.defaultProps = {
statistics: {
seasonCount: 0,
episodeCount: 0,
episodeFileCount: 0,
totalEpisodeCount: 0
movieFileCount: 0
}
};

View File

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieImage from './MovieImage';
const bannerPlaceholder = 'data:image/png;base64,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';
function MovieBanner(props) {
return (
<MovieImage
{...props}
coverType="banner"
placeholder={bannerPlaceholder}
/>
);
}
MovieBanner.propTypes = {
size: PropTypes.number.isRequired
};
MovieBanner.defaultProps = {
size: 70
};
export default MovieBanner;

View File

@ -0,0 +1,199 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LazyLoad from 'react-lazyload';
function findImage(images, coverType) {
return images.find((image) => image.coverType === coverType);
}
function getUrl(image, coverType, size) {
if (image) {
// Remove protocol
let url = image.url.replace(/^https?:/, '');
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return url;
}
}
class MovieImage extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const pixelRatio = Math.ceil(window.devicePixelRatio);
const {
images,
coverType,
size
} = props;
const image = findImage(images, coverType);
this.state = {
pixelRatio,
image,
url: getUrl(image, coverType, pixelRatio * size),
isLoaded: false,
hasError: false
};
}
componentDidMount() {
if (!this.state.url && this.props.onError) {
this.props.onError();
}
}
componentDidUpdate() {
const {
images,
coverType,
placeholder,
size,
onError
} = this.props;
const {
image,
pixelRatio
} = this.state;
const nextImage = findImage(images, coverType);
if (nextImage && (!image || nextImage.url !== image.url)) {
this.setState({
image: nextImage,
url: getUrl(nextImage, coverType, pixelRatio * size),
hasError: false
// Don't reset isLoaded, as we want to immediately try to
// show the new image, whether an image was shown previously
// or the placeholder was shown.
});
} else if (!nextImage && image) {
this.setState({
image: nextImage,
url: placeholder,
hasError: false
});
if (onError) {
onError();
}
}
}
//
// Listeners
onError = () => {
this.setState({
hasError: true
});
if (this.props.onError) {
this.props.onError();
}
}
onLoad = () => {
this.setState({
isLoaded: true,
hasError: false
});
if (this.props.onLoad) {
this.props.onLoad();
}
}
//
// Render
render() {
const {
className,
style,
placeholder,
size,
lazy,
overflow
} = this.props;
const {
url,
hasError,
isLoaded
} = this.state;
if (hasError || !url) {
return (
<img
className={className}
style={style}
src={placeholder}
/>
);
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img
className={className}
style={style}
src={placeholder}
/>
}
>
<img
className={className}
style={style}
src={url}
onError={this.onError}
onLoad={this.onLoad}
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? url : placeholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
}
MovieImage.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
coverType: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired,
onError: PropTypes.func,
onLoad: PropTypes.func
};
MovieImage.defaultProps = {
size: 250,
lazy: true,
overflow: false
};
export default MovieImage;

View File

@ -1,195 +1,25 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LazyLoad from 'react-lazyload';
import React from 'react';
import MovieImage from './MovieImage';
const posterPlaceholder = 'data:image/png;base64,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';
function findPoster(images) {
return _.find(images, { coverType: 'poster' });
}
function getPosterUrl(poster, size) {
if (poster) {
// Remove protocol
let url = poster.url.replace(/^https?:/, '');
url = url.replace('poster.jpg', `poster-${size}.jpg`);
return url;
}
}
class MoviePoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const pixelRatio = Math.ceil(window.devicePixelRatio);
const {
images,
size
} = props;
const poster = findPoster(images);
this.state = {
pixelRatio,
poster,
posterUrl: getPosterUrl(poster, pixelRatio * size),
isLoaded: false,
hasError: false
};
}
componentDidMount() {
if (!this.state.posterUrl && this.props.onError) {
this.props.onError();
}
}
componentDidUpdate(prevProps, prevState) {
const {
images,
size,
onError
} = this.props;
const {
poster,
pixelRatio
} = this.state;
const nextPoster = findPoster(images);
if (nextPoster && (!poster || nextPoster.url !== poster.url)) {
this.setState({
poster: nextPoster,
posterUrl: getPosterUrl(nextPoster, pixelRatio * size),
hasError: false
// Don't reset isLoaded, as we want to immediately try to
// show the new image, whether an image was shown previously
// or the placeholder was shown.
});
} else if (!nextPoster && poster) {
this.setState({
poster: nextPoster,
posterUrl: posterPlaceholder,
hasError: false
});
if (onError) {
onError();
}
}
}
//
// Listeners
onError = () => {
this.setState({
hasError: true
});
if (this.props.onError) {
this.props.onError();
}
}
onLoad = () => {
this.setState({
isLoaded: true,
hasError: false
});
if (this.props.onLoad) {
this.props.onLoad();
}
}
//
// Render
render() {
const {
className,
style,
size,
lazy,
overflow
} = this.props;
const {
posterUrl,
hasError,
isLoaded
} = this.state;
if (hasError || !posterUrl) {
return (
<img
className={className}
style={style}
src={posterPlaceholder}
/>
);
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img
className={className}
style={style}
src={posterPlaceholder}
/>
}
>
<img
className={className}
style={style}
src={posterUrl}
onError={this.onError}
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? posterUrl : posterPlaceholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
function MoviePoster(props) {
return (
<MovieImage
{...props}
coverType="poster"
placeholder={posterPlaceholder}
/>
);
}
MoviePoster.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired,
onError: PropTypes.func,
onLoad: PropTypes.func
size: PropTypes.number.isRequired
};
MoviePoster.defaultProps = {
size: 250,
lazy: true,
overflow: false
size: 250
};
export default MoviePoster;

View File

@ -54,7 +54,8 @@ class DownloadClient extends Component {
const {
id,
name,
enable
enable,
priority
} = this.props;
return (
@ -80,6 +81,16 @@ class DownloadClient extends Component {
Disabled
</Label>
}
{
priority > 1 &&
<Label
kind={kinds.DISABLED}
outline={true}
>
Priority: {priority}
</Label>
}
</div>
<EditDownloadClientModalConnector
@ -107,6 +118,7 @@ DownloadClient.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired,
priority: PropTypes.number.isRequired,
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
};

View File

@ -44,6 +44,7 @@ class EditDownloadClientModalContent extends Component {
implementationName,
name,
enable,
priority,
fields,
message
} = item;
@ -67,9 +68,7 @@ class EditDownloadClientModalContent extends Component {
{
!isFetching && !error &&
<Form
{...otherProps}
>
<Form {...otherProps}>
{
!!message &&
<Alert
@ -117,6 +116,23 @@ class EditDownloadClientModalContent extends Component {
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Client Priority</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText="Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority."
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>

View File

@ -23,6 +23,7 @@ function EditRemotePathMappingModalContent(props) {
isSaving,
saveError,
item,
downloadClientHosts,
onInputChange,
onSavePress,
onModalClose,
@ -55,17 +56,16 @@ function EditRemotePathMappingModalContent(props) {
{
!isFetching && !error &&
<Form
{...otherProps}
>
<Form {...otherProps}>
<FormGroup>
<FormLabel>Host</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
type={inputTypes.SELECT}
name="host"
helpText="The same host you specified for the remote Download Client"
{...host}
values={downloadClientHosts}
onChange={onInputChange}
/>
</FormGroup>
@ -140,6 +140,7 @@ EditRemotePathMappingModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.shape(remotePathMappingShape).isRequired,
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,

View File

@ -13,11 +13,39 @@ const newRemotePathMapping = {
localPath: ''
};
const selectDownloadClientHosts = createSelector(
(state) => state.settings.downloadClients.items,
(downloadClients) => {
const hosts = downloadClients.reduce((acc, downloadClient) => {
const name = downloadClient.name;
const host = downloadClient.fields.find((field) => {
return field.name === 'host';
});
if (host) {
const group = acc[host.value] = acc[host.value] || [];
group.push(name);
}
return acc;
}, {});
return Object.keys(hosts).map((host) => {
return {
key: host,
value: host,
hint: `${hosts[host].join(', ')}`
};
});
}
);
function createRemotePathMappingSelector() {
return createSelector(
(state, { id }) => id,
(state) => state.settings.remotePathMappings,
(id, remotePathMappings) => {
selectDownloadClientHosts,
(id, remotePathMappings, downloadClientHosts) => {
const {
isFetching,
error,
@ -37,7 +65,8 @@ function createRemotePathMappingSelector() {
isSaving,
saveError,
item: settings.settings,
...settings
...settings,
downloadClientHosts
};
}
);
@ -55,8 +84,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
setRemotePathMappingValue,
saveRemotePathMapping
dispatchSetRemotePathMappingValue: setRemotePathMappingValue,
dispatchSaveRemotePathMapping: saveRemotePathMapping
};
class EditRemotePathMappingModalContentConnector extends Component {
@ -67,7 +96,7 @@ class EditRemotePathMappingModalContentConnector extends Component {
componentDidMount() {
if (!this.props.id) {
Object.keys(newRemotePathMapping).forEach((name) => {
this.props.setRemotePathMappingValue({
this.props.dispatchSetRemotePathMappingValue({
name,
value: newRemotePathMapping[name]
});
@ -85,11 +114,11 @@ class EditRemotePathMappingModalContentConnector extends Component {
// Listeners
onInputChange = ({ name, value }) => {
this.props.setRemotePathMappingValue({ name, value });
this.props.dispatchSetRemotePathMappingValue({ name, value });
}
onSavePress = () => {
this.props.saveRemotePathMapping({ id: this.props.id });
this.props.dispatchSaveRemotePathMapping({ id: this.props.id });
}
//
@ -111,8 +140,8 @@ EditRemotePathMappingModalContentConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setRemotePathMappingValue: PropTypes.func.isRequired,
saveRemotePathMapping: PropTypes.func.isRequired,
dispatchSetRemotePathMappingValue: PropTypes.func.isRequired,
dispatchSaveRemotePathMapping: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@ -49,7 +49,8 @@ function BackupSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="backupInterval"
helpText="Interval in days"
unit="days"
helpText="Interval between automatic backups"
onChange={onInputChange}
{...backupInterval}
/>
@ -64,7 +65,8 @@ function BackupSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="backupRetention"
helpText="Retention in days. Automatic backups older the retention will be cleaned up automatically"
unit="days"
helpText="Automatic backups older the retention will be cleaned up automatically"
onChange={onInputChange}
{...backupRetention}
/>

View File

@ -16,6 +16,19 @@ import ProxySettings from './ProxySettings';
import SecuritySettings from './SecuritySettings';
import UpdateSettings from './UpdateSettings';
const requiresRestartKeys = [
'bindAddress',
'port',
'urlBase',
'enableSsl',
'sslPort',
'sslCertHash',
'authenticationMethod',
'username',
'password',
'apiKey'
];
class GeneralSettings extends Component {
//
@ -42,20 +55,7 @@ class GeneralSettings extends Component {
const prevSettings = prevProps.settings;
const keys = [
'bindAddress',
'port',
'urlBase',
'enableSsl',
'sslPort',
'sslCertHash',
'authenticationMethod',
'username',
'password',
'apiKey'
];
const pendingRestart = _.some(keys, (key) => {
const pendingRestart = _.some(requiresRestartKeys, (key) => {
const setting = settings[key];
const prevSetting = prevSettings[key];
@ -98,6 +98,7 @@ class GeneralSettings extends Component {
isResettingApiKey,
isMono,
isWindows,
isWindowsService,
mode,
onInputChange,
onConfirmResetApiKey,
@ -177,7 +178,9 @@ class GeneralSettings extends Component {
isOpen={this.state.isRestartRequiredModalOpen}
kind={kinds.DANGER}
title="Restart Radarr"
message="Radarr requires a restart to apply changes, do you want to restart now?"
message={
`Radarr requires a restart to apply changes, do you want to restart now? ${isWindowsService ? 'Depending which user is running the Radarr service you may need to restart Radarr as admin once before the service will start automatically.' : ''}`
}
cancelLabel="I'll restart later"
confirmLabel="Restart Now"
onConfirm={this.onConfirmRestart}
@ -201,6 +204,7 @@ GeneralSettings.propTypes = {
hasSettings: PropTypes.bool.isRequired,
isMono: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
isWindowsService: PropTypes.bool.isRequired,
mode: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onConfirmResetApiKey: PropTypes.func.isRequired,

View File

@ -26,6 +26,7 @@ function createMapStateToProps() {
isResettingApiKey,
isMono: systemStatus.isMono,
isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
mode: systemStatus.mode,
...sectionSettings
};
@ -58,7 +59,7 @@ class GeneralSettingsConnector extends Component {
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: SECTION });
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
}
//

View File

@ -87,56 +87,59 @@ function HostSettings(props) {
</FormGroup>
{
enableSsl.value &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>SSL Port</FormLabel>
enableSsl.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>SSL Port</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="sslPort"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslPort}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.NUMBER}
name="sslPort"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslPort}
/>
</FormGroup> :
null
}
{
isWindows && enableSsl.value &&
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>SSL Cert Hash</FormLabel>
isWindows && enableSsl.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>SSL Cert Hash</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="sslCertHash"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslCertHash}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.TEXT}
name="sslCertHash"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslCertHash}
/>
</FormGroup> :
null
}
{
mode !== 'service' &&
<FormGroup size={sizes.MEDIUM}>
<FormLabel>Open browser on start</FormLabel>
isWindows && mode !== 'service' ?
<FormGroup size={sizes.MEDIUM}>
<FormLabel>Open browser on start</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="launchBrowser"
helpText=" Open a web browser and navigate to Radarr homepage on app start."
onChange={onInputChange}
{...launchBrowser}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="launchBrowser"
helpText=" Open a web browser and navigate to Radarr homepage on app start."
onChange={onInputChange}
{...launchBrowser}
/>
</FormGroup> :
null
}
</FieldSet>

View File

@ -3,10 +3,12 @@ import React, { Component } from 'react';
import ReactSlider from 'react-slider';
import formatBytes from 'Utilities/Number/formatBytes';
import roundNumber from 'Utilities/Number/roundNumber';
import { kinds } from 'Helpers/Props';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Label from 'Components/Label';
import NumberInput from 'Components/Form/NumberInput';
import TextInput from 'Components/Form/TextInput';
import Popover from 'Components/Tooltip/Popover';
import QualityDefinitionLimits from './QualityDefinitionLimits';
import styles from './QualityDefinition.css';
const MIN = 0;
@ -139,12 +141,10 @@ class QualityDefinition extends Component {
} = this.state;
const minBytes = minSize * 1024 * 1024;
const minThirty = formatBytes(minBytes * 90, 2);
const minSixty = formatBytes(minBytes * 140, 2);
const minSixty = `${formatBytes(minBytes * 60)}/h`;
const maxBytes = maxSize && maxSize * 1024 * 1024;
const maxThirty = maxBytes ? formatBytes(maxBytes * 90, 2) : 'Unlimited';
const maxSixty = maxBytes ? formatBytes(maxBytes * 140, 2) : 'Unlimited';
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/h` : 'Unlimited';
return (
<div className={styles.qualityDefinition}>
@ -178,13 +178,35 @@ class QualityDefinition extends Component {
<div className={styles.sizes}>
<div>
<Label kind={kinds.WARNING}>{minThirty}</Label>
<Label kind={kinds.INFO}>{minSixty}</Label>
<Popover
anchor={
<Label kind={kinds.INFO}>{minSixty}</Label>
}
title="Minimum Limits"
body={
<QualityDefinitionLimits
bytes={minBytes}
message="No minimum for any runtime"
/>
}
position={tooltipPositions.BOTTOM}
/>
</div>
<div>
<Label kind={kinds.WARNING}>{maxThirty}</Label>
<Label kind={kinds.INFO}>{maxSixty}</Label>
<Popover
anchor={
<Label kind={kinds.WARNING}>{maxSixty}</Label>
}
title="Maximum Limits"
body={
<QualityDefinitionLimits
bytes={maxBytes}
message="No limit for any runtime"
/>
}
position={tooltipPositions.BOTTOM}
/>
</div>
</div>
</div>

View File

@ -12,15 +12,15 @@ function QualityDefinitionLimits(props) {
return <div>{message}</div>;
}
const thirty = formatBytes(bytes * 30);
const fourtyFive = formatBytes(bytes * 45);
const sixty = formatBytes(bytes * 60);
const ninety = formatBytes(bytes * 90);
const hundredTwenty = formatBytes(bytes * 120);
return (
<div>
<div>30 Minutes: {thirty}</div>
<div>45 Minutes: {fourtyFive}</div>
<div>60 Minutes: {sixty}</div>
<div>90 Minutes: {ninety}</div>
<div>120 Minutes: {hundredTwenty}</div>
</div>
);
}

View File

@ -4,7 +4,6 @@ import * as blacklist from './blacklistActions';
import * as calendar from './calendarActions';
import * as captcha from './captchaActions';
import * as customFilters from './customFilterActions';
import * as devices from './deviceActions';
import * as commands from './commandActions';
import * as movieFiles from './movieFileActions';
import * as history from './historyActions';
@ -13,6 +12,7 @@ import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions';
import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions';
import * as queue from './queueActions';
import * as releases from './releaseActions';
import * as rootFolders from './rootFolderActions';
@ -32,7 +32,6 @@ export default [
captcha,
commands,
customFilters,
devices,
movieFiles,
history,
importMovie,
@ -40,6 +39,7 @@ export default [
oAuth,
organizePreview,
paths,
providerOptions,
queue,
releases,
rootFolders,

View File

@ -8,7 +8,7 @@ import { set } from './baseActions';
//
// Variables
export const section = 'devices';
export const section = 'providerOptions';
//
// State
@ -23,32 +23,27 @@ export const defaultState = {
//
// Actions Types
export const FETCH_DEVICES = 'devices/fetchDevices';
export const CLEAR_DEVICES = 'devices/clearDevices';
export const FETCH_OPTIONS = 'devices/fetchOptions';
export const CLEAR_OPTIONS = 'devices/clearOptions';
//
// Action Creators
export const fetchDevices = createThunk(FETCH_DEVICES);
export const clearDevices = createAction(CLEAR_DEVICES);
export const fetchOptions = createThunk(FETCH_OPTIONS);
export const clearOptions = createAction(CLEAR_OPTIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_DEVICES]: function(getState, payload, dispatch) {
const actionPayload = {
action: 'getDevices',
...payload
};
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
dispatch(set({
section,
isFetching: true
}));
const promise = requestAction(actionPayload);
const promise = requestAction(payload);
promise.done((data) => {
dispatch(set({
@ -56,7 +51,7 @@ export const actionHandlers = handleThunks({
isFetching: false,
isPopulated: true,
error: null,
items: data.devices || []
items: data.options || []
}));
});
@ -76,7 +71,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[CLEAR_DEVICES]: function(state) {
[CLEAR_OPTIONS]: function(state) {
return updateSectionState(state, section, defaultState);
}

View File

@ -5,7 +5,7 @@ function createMovieQualityProfileSelector() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
createMovieSelector(),
(qualityProfiles, movie) => {
(qualityProfiles, movie = {}) => {
return qualityProfiles.find((profile) => {
return profile.id === movie.qualityProfileId;
});

View File

@ -1,7 +1,6 @@
/* stylelint-disable */
@import '~normalize.css/normalize.css';
@import 'scaffolding.css';
@import '/Content/Fonts/fonts.css';
@import "~normalize.css/normalize.css";
@import "scaffolding.css";
/* stylelint-enable */

View File

@ -96,7 +96,7 @@ class BackupRow extends Component {
<TableRowCell>
<Link
to={path}
to={`${window.Radarr.urlBase}${path}`}
noRouter={true}
>
{name}

View File

@ -1,8 +1,4 @@
.updateAvailable {
display: flex;
}
.upToDate {
.messageContainer {
display: flex;
margin-bottom: 20px;
}
@ -12,7 +8,7 @@
font-size: 30px;
}
.upToDateMessage {
.message {
padding-left: 5px;
font-size: 18px;
line-height: 30px;
@ -49,7 +45,7 @@
font-size: 16px;
}
.branch {
.label {
composes: label from '~Components/Label.css';
margin-left: 10px;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -19,25 +19,35 @@ class Updates extends Component {
render() {
const {
currentVersion,
isFetching,
isPopulated,
error,
updatesError,
generalSettingsError,
items,
isInstallingUpdate,
updateMechanism,
shortDateFormat,
onInstallLatestPress
} = this.props;
const hasUpdates = isPopulated && !error && items.length > 0;
const noUpdates = isPopulated && !error && !items.length;
const hasError = !!(updatesError || generalSettingsError);
const hasUpdates = isPopulated && !hasError && items.length > 0;
const noUpdates = isPopulated && !hasError && !items.length;
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterMessages = {
external: 'Unable to update Radarr directly, Radarr is configured to use an external update mechanism',
apt: 'Unable to update Radarr directly, use apt to install the update',
docker: 'Unable to update Radarr directly, update the docker container to receive the update'
};
return (
<PageContent title="Updates">
<PageContentBodyConnector>
{
!isPopulated && !error &&
!isPopulated && !hasError &&
<LoadingIndicator />
}
@ -48,15 +58,30 @@ class Updates extends Component {
{
hasUpdateToInstall &&
<div className={styles.updateAvailable}>
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
Install Latest
</SpinnerButton>
<div className={styles.messageContainer}>
{
updateMechanism === 'builtIn' || updateMechanism === 'script' ?
<SpinnerButton
className={styles.updateAvailable}
kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
Install Latest
</SpinnerButton> :
<Fragment>
<Icon
name={icons.WARNING}
kind={kinds.WARNING}
size={30}
/>
<div className={styles.message}>
{externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external}
</div>
</Fragment>
}
{
isFetching &&
@ -70,13 +95,14 @@ class Updates extends Component {
{
noUpdateToInstall &&
<div className={styles.upToDate}>
<div className={styles.messageContainer}>
<Icon
className={styles.upToDateIcon}
name={icons.CHECK_CIRCLE}
size={30}
/>
<div className={styles.upToDateMessage}>
<div className={styles.message}>
The latest version of Radarr is already installed
</div>
@ -108,13 +134,25 @@ class Updates extends Component {
<div className={styles.date}>{formatDate(update.releaseDate, shortDateFormat)}</div>
{
update.branch !== 'master' &&
update.branch === 'master' ?
null:
<Label
className={styles.branch}
className={styles.label}
>
{update.branch}
</Label>
}
{
update.version === currentVersion ?
<Label
className={styles.label}
kind={kinds.SUCCESS}
>
Currently Installed
</Label> :
null
}
</div>
{
@ -144,11 +182,18 @@ class Updates extends Component {
}
{
!!error &&
!!updatesError &&
<div>
Failed to fetch updates
</div>
}
{
!!generalSettingsError &&
<div>
Failed to update settings
</div>
}
</PageContentBodyConnector>
</PageContent>
);
@ -157,11 +202,14 @@ class Updates extends Component {
}
Updates.propTypes = {
currentVersion: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
updatesError: PropTypes.object,
generalSettingsError: PropTypes.object,
items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired,
updateMechanism: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired
};

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import { fetchUpdates } from 'Store/Actions/systemActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
@ -11,23 +12,35 @@ import Updates from './Updates';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
(state) => state.system.updates,
(state) => state.settings.general,
createUISettingsSelector(),
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
(updates, uiSettings, isInstallingUpdate) => {
(
currentVersion,
updates,
generalSettings,
uiSettings,
isInstallingUpdate
) => {
const {
isFetching,
isPopulated,
error,
error: updatesError,
items
} = updates;
const isFetching = updates.isFetching || generalSettings.isFetching;
const isPopulated = updates.isPopulated && generalSettings.isPopulated;
return {
currentVersion,
isFetching,
isPopulated,
error,
updatesError,
generalSettingsError: generalSettings.error,
items,
isInstallingUpdate,
updateMechanism: generalSettings.item.updateMechanism,
shortDateFormat: uiSettings.shortDateFormat
};
}
@ -35,8 +48,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchUpdates,
executeCommand
dispatchFetchUpdates: fetchUpdates,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchExecuteCommand: executeCommand
};
class UpdatesConnector extends Component {
@ -45,14 +59,15 @@ class UpdatesConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchUpdates();
this.props.dispatchFetchUpdates();
this.props.dispatchFetchGeneralSettings();
}
//
// Listeners
onInstallLatestPress = () => {
this.props.executeCommand({ name: commandNames.APPLICATION_UPDATE });
this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
}
//
@ -69,8 +84,9 @@ class UpdatesConnector extends Component {
}
UpdatesConnector.propTypes = {
fetchUpdates: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
dispatchFetchUpdates: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);

View File

@ -1,30 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Chrome, Opera, and Firefox OS -->
<meta name="theme-color" content="#3a3f51"/>
<meta name="theme-color" content="#3a3f51" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51"/>
<meta name="msapplication-navbutton-color" content="#3a3f51" />
<meta name="description" content="Radarr">
<meta name="description" content="Radarr (Preview)" />
<link rel="apple-touch-icon" sizes="180x180" href="/Content/Images/Icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/Content/Images/Icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png">
<link rel="manifest" href="/Content/Images/Icons/manifest.json">
<link rel="mask-icon" href="/Content/Images/Icons/safari-pinned-tab.svg" color="#00ccff">
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" data-no-hash />
<meta name="msapplication-config" content="/Content/Images/Icons/browserconfig.xml">
<link
rel="apple-touch-icon"
sizes="180x180"
href="/Content/Images/Icons/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/Content/Images/Icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
<link rel="manifest" href="/Content/Images/Icons/manifest.json" />
<link
rel="mask-icon"
href="/Content/Images/Icons/safari-pinned-tab.svg"
color="#00ccff"
/>
<link
rel="shortcut icon"
type="image/ico"
href="/favicon.ico"
data-no-hash
/>
<meta
name="msapplication-config"
content="/Content/Images/Icons/browserconfig.xml"
/>
<link rel="stylesheet" type="text/css" href="/Content/styles.css">
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
<title>Radarr</title>
<title>Radarr (Preview)</title>
<!--
The super basic styling for .root will live here,
@ -57,5 +83,4 @@
<script src="/vendor.js"></script>
<script src="/preload.js"></script>
<script src="/index.js"></script>
</html>

View File

@ -1,232 +1,291 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<!-- Chrome, Opera, and Firefox OS -->
<meta name="theme-color" content="#3a3f51" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51" />
<!-- Chrome, Opera, and Firefox OS -->
<meta name="theme-color" content="#3a3f51"/>
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#3a3f51"/>
<meta name="description" content="Radarr (Preview)" />
<meta name="description" content="Radarr">
<link
rel="apple-touch-icon"
sizes="180x180"
href="/Content/Images/Icons/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/Content/Images/Icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
<link rel="manifest" href="/Content/Images/Icons/manifest.json" />
<link
rel="mask-icon"
href="/Content/Images/Icons/safari-pinned-tab.svg"
color="#00ccff"
/>
<link
rel="shortcut icon"
type="image/ico"
href="/favicon.ico"
data-no-hash
/>
<meta
name="msapplication-config"
content="/Content/Images/Icons/browserconfig.xml"
/>
<link rel="apple-touch-icon" sizes="180x180" href="/Content/Images/Icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/Content/Images/Icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/Content/Images/Icons/favicon-16x16.png">
<link rel="manifest" href="/Content/Images/Icons/manifest.json">
<link rel="mask-icon" href="/Content/Images/Icons/safari-pinned-tab.svg" color="#00ccff">
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" data-no-hash />
<meta name="msapplication-config" content="/Content/Images/Icons/browserconfig.xml">
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
<link rel="stylesheet" type="text/css" href="/Content/styles.css">
<title>Login - Radarr</title>
<title>Login - Radarr</title>
<style>
body {
background-color: #f5f7fa;
color: #656565;
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.center {
display: flex;
justify-content: center;
}
.content {
flex: 0 0 325px;
}
.logo {
width: 32px;
height: 32px;
}
.panel {
margin-top: 50px;
border-radius: 4px;
}
.panel-header {
display: flex;
justify-content: center;
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background-color: #3a3f51;
}
.panel-body {
padding: 20px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
}
.sign-in {
padding: 10px 0;
text-align: center;
}
.form-group {
margin: 20px 0;
}
.form-input {
box-sizing: border-box;
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid #dde6e9;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.form-input:focus {
outline: 0;
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.button {
overflow: hidden;
margin-top: 20px;
padding: 10px 0;
width: 100%;
border: 1px solid;
border-color: #5899eb;
border-radius: 4px;
background-color: #5d9cec;
color: #fff;
vertical-align: middle;
text-align: center;
white-space: nowrap;
line-height: normal;
}
.button:hover {
border-color: #3483e7;
background-color: #4b91ea;
color: #fff;
text-decoration: none;
}
.copy {
margin-top: 15px;
text-align: center;
font-size: 14px;
}
.remember-me-container {
display: flex;
justify-content: space-between;
line-height: 25px;
}
.remember-me {
font-size: 14px;
}
.forgot-password {
margin-left: auto;
color: #909fa7;
text-decoration: none;
font-size: 13px;
}
.forgot-password:focus,
.forgot-password:hover {
color: #748690;
text-decoration: underline;
}
.forgot-password:visited {
color: #748690;
}
.login-failed {
margin-top: 20px;
color: #f05050;
font-size: 14px;
}
.hidden {
display: none;
}
@media only screen and (min-device-width: 375px) and (max-device-width: 812px) {
.form-input {
font-size: 16px;
<style>
body {
background-color: #f5f7fa;
color: #656565;
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
sans-serif;
}
}
</style>
</head>
<body>
<div class="center">
<div class="content">
<div class="panel">
<div class="panel-header">
<img src="/Content/Images/logo.svg" alt="Image" class="logo">
</div>
.center {
display: flex;
justify-content: center;
}
<div class="panel-body">
<div class="sign-in">
SIGN IN TO CONTINUE
.content {
flex: 0 0 325px;
}
.logo {
width: 32px;
height: 32px;
}
.panel {
margin-top: 50px;
border-radius: 4px;
}
.panel-header {
display: flex;
justify-content: center;
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background-color: #3a3f51;
}
.panel-body {
padding: 20px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
}
.sign-in {
padding: 10px 0;
text-align: center;
}
.form-group {
margin: 20px 0;
}
.form-input {
box-sizing: border-box;
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid #dde6e9;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.form-input:focus {
outline: 0;
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(102, 175, 233, 0.6);
}
.button {
overflow: hidden;
margin-top: 20px;
padding: 10px 0;
width: 100%;
border: 1px solid;
border-color: #5899eb;
border-radius: 4px;
background-color: #5d9cec;
color: #fff;
vertical-align: middle;
text-align: center;
white-space: nowrap;
line-height: normal;
}
.button:hover {
border-color: #3483e7;
background-color: #4b91ea;
color: #fff;
text-decoration: none;
}
.copy {
margin-top: 15px;
text-align: center;
font-size: 14px;
}
.remember-me-container {
display: flex;
justify-content: space-between;
line-height: 25px;
}
.remember-me {
font-size: 14px;
}
.forgot-password {
margin-left: auto;
color: #909fa7;
text-decoration: none;
font-size: 13px;
}
.forgot-password:focus,
.forgot-password:hover {
color: #748690;
text-decoration: underline;
}
.forgot-password:visited {
color: #748690;
}
.login-failed {
margin-top: 20px;
color: #f05050;
font-size: 14px;
}
.hidden {
display: none;
}
@media only screen and (min-device-width: 375px) and (max-device-width: 812px) {
.form-input {
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="center">
<div class="content">
<div class="panel">
<div class="panel-header">
<img src="/Content/Images/logo.svg" alt="Image" class="logo" />
</div>
<div class="panel-body">
<div class="sign-in">
SIGN IN TO CONTINUE
</div>
<form
role="form"
data-parsley-validate=""
novalidate=""
class="mb-lg"
method="POST"
>
<div class="form-group">
<input
type="email"
name="username"
class="form-input"
placeholder="Username"
autocomplete="off"
pattern=".{1,}"
required
title="User name is required"
autoFocus="true"
/>
</div>
<div class="form-group">
<input
type="password"
name="password"
class="form-input"
placeholder="Password"
required
/>
</div>
<div class="remember-me-container">
<span class="remember-me">
<input
type="checkbox"
name="rememberMe"
id="rememberMe"
checked="checked"
/>
<label for="rememberMe">Remember Me</label>
</span>
<a
href="https://github.com/Radarr/Radarr/wiki/Forgot-my-Password"
class="forgot-password"
>Forgot your password?</a
>
</div>
<button type="submit" class="button">Login</button>
<div id="login-failed" class="login-failed hidden">
Incorrect Username or Password
</div>
</form>
</div>
</div>
<form role="form" data-parsley-validate="" novalidate="" class="mb-lg" method="POST">
<div class="form-group">
<input type="email" name="username" class="form-input" placeholder="Username" autocomplete="off" pattern=".{1,}" required title="User name is required" autoFocus="true">
</div>
<div class="form-group">
<input type="password" name="password" class="form-input" placeholder="Password" required>
</div>
<div class="remember-me-container">
<span class="remember-me">
<input type="checkbox" name="rememberMe" id="rememberMe" checked="checked">
<label for="rememberMe">Remember Me</label>
</span>
<a href="https://github.com/Radarr/Radarr/wiki/Forgot-my-Password" class="forgot-password">Forgot your password?</a>
</div>
<button type="submit" class="button">Login</button>
<div id="login-failed" class="login-failed hidden">
Incorrect Username or Password
</div>
</form>
<div id="copy" class="copy hidden">
<span>&copy;</span>
<span id="year"></span>
<span>-</span>
<span>Radarr</span>
</div>
</div>
</div>
</body>
<div id="copy" class="copy hidden">
<span>&copy;</span>
<span id="year"></span>
<span>-</span>
<span>Radarr</span>
</div>
</div>
</div>
</body>
<script type="text/javascript">
var yearSpan = document.getElementById("year");
yearSpan.innerHTML = "2010-" + new Date().getFullYear();
<script type="text/javascript">
var yearSpan = document.getElementById('year');
yearSpan.innerHTML = '2010-' + new Date().getFullYear();
var copyDiv = document.getElementById("copy");
copyDiv.classList.remove("hidden");
var copyDiv = document.getElementById('copy');
copyDiv.classList.remove('hidden');
if (window.location.search.indexOf("loginFailed=true") > -1) {
var loginFailedDiv = document.getElementById("login-failed");
if (window.location.search.indexOf('loginFailed=true') > -1) {
var loginFailedDiv = document.getElementById('login-failed');
loginFailedDiv.classList.remove('hidden');
}
</script>
loginFailedDiv.classList.remove("hidden");
}
</script>
</html>

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Api.DownloadClient
resource.Enable = definition.Enable;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
}
protected override void MapToModel(DownloadClientDefinition definition, DownloadClientResource resource)
@ -23,6 +24,7 @@ namespace NzbDrone.Api.DownloadClient
definition.Enable = resource.Enable;
definition.Protocol = resource.Protocol;
definition.Priority = resource.Priority;
}
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)

View File

@ -6,5 +6,6 @@ namespace NzbDrone.Api.DownloadClient
{
public bool Enable { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
}
}

View File

@ -86,7 +86,7 @@ namespace NzbDrone.Common.Test
{
first.AsOsAgnostic().PathEquals(second.AsOsAgnostic()).Should().BeFalse();
}
[Test]
public void should_return_false_when_not_a_child()
{
@ -113,6 +113,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"C:\Test\", @"C:\Test\mydir")]
[TestCase(@"C:\Test\", @"C:\Test\mydir\")]
[TestCase(@"C:\Test", @"C:\Test\30.Rock.S01E01.Pilot.avi")]
[TestCase(@"C:\", @"C:\Test\30.Rock.S01E01.Pilot.avi")]
public void path_should_be_parent(string parentPath, string childPath)
{
parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeTrue();
@ -137,18 +138,34 @@ namespace NzbDrone.Common.Test
}
[TestCase(@"C:\Test\mydir", @"C:\Test")]
[TestCase(@"C:\Test\", @"C:")]
[TestCase(@"C:\Test\", @"C:\")]
[TestCase(@"C:\", null)]
public void path_should_return_parent(string path, string parentPath)
[TestCase(@"\\server\share", null)]
[TestCase(@"\\server\share\test", @"\\server\share")]
public void path_should_return_parent_windows(string path, string parentPath)
{
WindowsOnly();
path.GetParentPath().Should().Be(parentPath);
}
[TestCase(@"/", null)]
[TestCase(@"/test", "/")]
public void path_should_return_parent_mono(string path, string parentPath)
{
MonoOnly();
path.GetParentPath().Should().Be(parentPath);
}
[Test]
public void path_should_return_parent_for_oversized_path()
{
var path = @"/media/2e168617-f2ae-43fb-b88c-3663af1c8eea/downloads/sabnzbd/nzbdrone/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories";
var parentPath = @"/media/2e168617-f2ae-43fb-b88c-3663af1c8eea/downloads/sabnzbd/nzbdrone/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing";
MonoOnly();
// This test will fail on Windows if long path support is not enabled: https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/
// It will also fail if the app isn't configured to use long path (such as resharper): https://blogs.msdn.microsoft.com/jeremykuhne/2016/07/30/net-4-6-2-and-long-paths-on-windows-10/
var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic();
var parentPath = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing".AsOsAgnostic();
path.GetParentPath().Should().Be(parentPath);
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Disk
{
public static class SystemFolders
{
public static List<string> GetSystemFolders()
{
if (OsInfo.IsWindows)
{
return new List<string> { Environment.GetFolderPath(Environment.SpecialFolder.Windows) };
}
if (OsInfo.IsOsx)
{
return new List<string> { "/System" };
}
return new List<string>
{
"/bin",
"/boot",
"/lib",
"/sbin",
"/proc"
};
}
}
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Common.Exceptions
{
@ -10,19 +7,16 @@ namespace NzbDrone.Common.Exceptions
public RadarrStartupException(string message, params object[] args)
: base("Radarr failed to start: " + string.Format(message, args))
{
}
public RadarrStartupException(string message)
: base("Radarr failed to start: " + message)
{
}
public RadarrStartupException()
: base("Radarr failed to start")
{
}
public RadarrStartupException(Exception innerException, string message, params object[] args)
@ -38,7 +32,6 @@ namespace NzbDrone.Common.Exceptions
public RadarrStartupException(Exception innerException)
: base("Radarr failed to start: " + innerException.Message)
{
}
}
}

View File

@ -98,6 +98,7 @@
<Compile Include="Disk\RelativeFileSystemModel.cs" />
<Compile Include="Disk\FileSystemModel.cs" />
<Compile Include="Disk\FileSystemResult.cs" />
<Compile Include="Disk\SystemFolders.cs" />
<Compile Include="EnvironmentInfo\IOperatingSystemVersionInfo.cs" />
<Compile Include="EnvironmentInfo\IOsVersionAdapter.cs" />
<Compile Include="EnvironmentInfo\IPlatformInfo.cs" />

View File

@ -27,7 +27,7 @@ namespace NzbDrone.Common.Processes
bool Exists(string processName);
ProcessPriorityClass GetCurrentProcessPriority();
Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action<string> onOutputDataReceived = null, Action<string> onErrorDataReceived = null);
Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null);
Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null, bool noWindow = false);
ProcessOutput StartAndCapture(string path, string args = null, StringDictionary environmentVariables = null);
}
@ -108,11 +108,7 @@ namespace NzbDrone.Common.Processes
public Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action<string> onOutputDataReceived = null, Action<string> onErrorDataReceived = null)
{
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
args = GetMonoArgs(path, args);
path = "mono";
}
(path, args) = GetPathAndArgs(path, args);
var logger = LogManager.GetLogger(new FileInfo(path).Name);
@ -190,17 +186,16 @@ namespace NzbDrone.Common.Processes
return process;
}
public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null)
public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null, bool noWindow = false)
{
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
args = GetMonoArgs(path, args);
path = "mono";
}
(path, args) = GetPathAndArgs(path, args);
_logger.Debug("Starting {0} {1}", path, args);
var startInfo = new ProcessStartInfo(path, args);
startInfo.CreateNoWindow = noWindow;
startInfo.UseShellExecute = !noWindow;
var process = new Process
{
StartInfo = startInfo
@ -333,7 +328,6 @@ namespace NzbDrone.Common.Processes
var monoProcesses = Process.GetProcessesByName("mono")
.Union(Process.GetProcessesByName("mono-sgen"))
.Union(Process.GetProcessesByName("mono-sgen32"))
.Where(process =>
process.Modules.Cast<ProcessModule>()
.Any(module =>
@ -359,9 +353,19 @@ namespace NzbDrone.Common.Processes
return processes;
}
private string GetMonoArgs(string path, string args)
private (string Path, string Args) GetPathAndArgs(string path, string args)
{
return string.Format("--debug {0} {1}", path, args);
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
return ("mono", $"--debug {path} {args}");
}
if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase))
{
return ("cmd.exe", $"/c {path} {args}");
}
return (path, args);
}
}
}

View File

@ -6,6 +6,8 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Movies;
using System.Collections.Generic;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Test.Datastore
{
@ -64,10 +66,12 @@ namespace NzbDrone.Core.Test.Datastore
public void embedded_document_as_json()
{
var quality = new QualityModel { Quality = Quality.Bluray720p, Revision = new Revision(version: 2 )};
var languages = new List<Language> { Language.English };
var history = Builder<History.History>.CreateNew()
.With(c => c.Id = 0)
.With(c => c.Quality = quality)
.With(c => c.Languages = languages)
.Build();
Db.Insert(history);
@ -79,14 +83,18 @@ namespace NzbDrone.Core.Test.Datastore
[Test]
public void embedded_list_of_document_with_json()
{
var languages = new List<Language> { Language.English };
var history = Builder<History.History>.CreateListOfSize(2)
.All().With(c => c.Id = 0)
.With(c => c.Languages = languages)
.Build().ToList();
history[0].Quality = new QualityModel { Quality = Quality.HDTV1080p, Revision = new Revision(version: 2)};
history[1].Quality = new QualityModel { Quality = Quality.Bluray720p, Revision = new Revision(version: 2)};
Db.InsertMany(history);
var returnedHistory = Db.All<History.History>();

View File

@ -0,0 +1,153 @@
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class add_download_client_priorityFixture : MigrationTest<add_download_client_priority>
{
[Test]
public void should_set_prio_to_one()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
});
});
var items = db.Query<DownloadClientDefinition132>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().Priority.Should().Be(1);
}
[Test]
public void should_renumber_prio_for_enabled_clients()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
}).Row(new
{
Enable = 1,
Name = "Deluge2",
Implementation = "Deluge",
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
}).Row(new
{
Enable = 1,
Name = "sab",
Implementation = "Sabnzbd",
Settings = new SabnzbdSettings81
{
Host = "127.0.0.1",
TvCategory = "abc"
}.ToJson(),
ConfigContract = "SabnzbdSettings"
});
});
var items = db.Query<DownloadClientDefinition132>("SELECT * FROM DownloadClients");
items.Should().HaveCount(3);
items[0].Priority.Should().Be(1);
items[1].Priority.Should().Be(2);
items[2].Priority.Should().Be(1);
}
[Test]
public void should_not_renumber_prio_for_disabled_clients()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 0,
Name = "Deluge",
Implementation = "Deluge",
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
}).Row(new
{
Enable = 0,
Name = "Deluge2",
Implementation = "Deluge",
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
TvCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
}).Row(new
{
Enable = 0,
Name = "sab",
Implementation = "Sabnzbd",
Settings = new SabnzbdSettings81
{
Host = "127.0.0.1",
TvCategory = "abc"
}.ToJson(),
ConfigContract = "SabnzbdSettings"
});
});
var items = db.Query<DownloadClientDefinition132>("SELECT * FROM DownloadClients");
items.Should().HaveCount(3);
items[0].Priority.Should().Be(1);
items[1].Priority.Should().Be(1);
items[1].Priority.Should().Be(1);
}
}
public class DownloadClientDefinition132
{
public int Id { get; set; }
public bool Enable { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string Implementation { get; set; }
public JObject Settings { get; set; }
public string ConfigContract { get; set; }
}
}

View File

@ -19,7 +19,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
_remoteMovie = new RemoteMovie
{
Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Torrent }
Release = new ReleaseInfo
{
Title = "Movie.title.1998",
DownloadProtocol = DownloadProtocol.Torrent
}
};
}
@ -69,5 +73,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
[TestCase("How the Earth Was Made S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")]
[TestCase("The Universe S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")]
[TestCase("HELL ON WHEELS S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")]
[TestCase("Game.of.Thrones.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")]
[TestCase("Game of Thrones S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")]
public void should_return_false_if_matches_disc_format(string title)
{
_remoteMovie.Release.Title = title;
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
}
}

View File

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download
{
[TestFixture]
public class DownloadClientProviderFixture : CoreTest<DownloadClientProvider>
{
private List<IDownloadClient> _downloadClients;
private List<DownloadClientStatus> _blockedProviders;
private int _nextId;
[SetUp]
public void SetUp()
{
_downloadClients = new List<IDownloadClient>();
_blockedProviders = new List<DownloadClientStatus>();
_nextId = 1;
Mocker.GetMock<IDownloadClientFactory>()
.Setup(v => v.GetAvailableProviders())
.Returns(_downloadClients);
Mocker.GetMock<IDownloadClientStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(_blockedProviders);
}
private Mock<IDownloadClient> WithUsenetClient(int priority = 0)
{
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
mock.SetupGet(s => s.Definition)
.Returns(Builder<DownloadClientDefinition>
.CreateNew()
.With(v => v.Id = _nextId++)
.With(v => v.Priority = priority)
.Build());
_downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Usenet);
return mock;
}
private Mock<IDownloadClient> WithTorrentClient(int priority = 0)
{
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
mock.SetupGet(s => s.Definition)
.Returns(Builder<DownloadClientDefinition>
.CreateNew()
.With(v => v.Id = _nextId++)
.With(v => v.Priority = priority)
.Build());
_downloadClients.Add(mock.Object);
mock.SetupGet(v => v.Protocol).Returns(DownloadProtocol.Torrent);
return mock;
}
private void GivenBlockedClient(int id)
{
_blockedProviders.Add(new DownloadClientStatus
{
ProviderId = id,
DisabledTill = DateTime.UtcNow.AddHours(3)
});
}
[Test]
public void should_roundrobin_over_usenet_client()
{
WithUsenetClient();
WithUsenetClient();
WithUsenetClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(1);
client5.Definition.Id.Should().Be(2);
}
[Test]
public void should_roundrobin_over_torrent_client()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(4);
client4.Definition.Id.Should().Be(2);
client5.Definition.Id.Should().Be(3);
}
[Test]
public void should_roundrobin_over_protocol_separately()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Usenet);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(1);
client2.Definition.Id.Should().Be(2);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(2);
}
[Test]
public void should_skip_blocked_torrent_client()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
GivenBlockedClient(3);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(4);
client3.Definition.Id.Should().Be(2);
client4.Definition.Id.Should().Be(4);
}
[Test]
public void should_not_skip_blocked_torrent_client_if_all_blocked()
{
WithUsenetClient();
WithTorrentClient();
WithTorrentClient();
WithTorrentClient();
GivenBlockedClient(2);
GivenBlockedClient(3);
GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(4);
client4.Definition.Id.Should().Be(2);
}
[Test]
public void should_skip_secondary_prio_torrent_client()
{
WithUsenetClient();
WithTorrentClient(2);
WithTorrentClient();
WithTorrentClient();
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(3);
client2.Definition.Id.Should().Be(4);
client3.Definition.Id.Should().Be(3);
client4.Definition.Id.Should().Be(4);
}
[Test]
public void should_not_skip_secondary_prio_torrent_client_if_primary_blocked()
{
WithUsenetClient();
WithTorrentClient(2);
WithTorrentClient(2);
WithTorrentClient();
GivenBlockedClient(4);
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
client1.Definition.Id.Should().Be(2);
client2.Definition.Id.Should().Be(3);
client3.Definition.Id.Should().Be(2);
client4.Definition.Id.Should().Be(3);
}
}
}

View File

@ -10,6 +10,7 @@ using NzbDrone.Test.Common;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
{
@ -81,6 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
DownloadRate = 7000000
});
Mocker.GetMock<INzbgetProxy>()
.Setup(v => v.GetVersion(It.IsAny<NzbgetSettings>()))
.Returns("14.0");
@ -277,16 +279,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
}
[Test]
public void should_report_deletestatus_copy_as_failed()
public void should_skip_deletestatus_copy()
{
_completed.DeleteStatus = "COPY";
GivenQueue(null);
GivenHistory(_completed);
var result = Subject.GetItems().Single();
var result = Subject.GetItems().SingleOrDefault();
result.Status.Should().Be(DownloadItemStatus.Failed);
result.Should().BeNull();
}
[Test]
@ -350,7 +352,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
var remoteMovie = CreateRemoteMovie();
Assert.Throws<DownloadClientException>(() => Subject.Download(remoteMovie));
Assert.Throws<DownloadClientRejectedReleaseException>(() => Subject.Download(remoteMovie));
}
[Test]

View File

@ -7,6 +7,8 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Languages;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
@ -16,30 +18,32 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
[Test]
public void should_delete_orphaned_episode_files()
{
var episodeFile = Builder<MovieFile>.CreateNew()
var movieFile = Builder<MovieFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English})
.BuildNew();
Db.Insert(episodeFile);
Db.Insert(movieFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_unorphaned_episode_files()
public void should_not_delete_unorphaned_movie_files()
{
var episodeFiles = Builder<MovieFile>.CreateListOfSize(2)
var movieFiles = Builder<MovieFile>.CreateListOfSize(2)
.All()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildListOfNew();
Db.InsertMany(episodeFiles);
Db.InsertMany(movieFiles);
var episode = Builder<Movie>.CreateNew()
.With(e => e.MovieFileId = episodeFiles.First().Id)
var movie = Builder<Movie>.CreateNew()
.With(e => e.MovieFileId = movieFiles.First().Id)
.BuildNew();
Db.Insert(episode);
Db.Insert(movie);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);

View File

@ -1,9 +1,11 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles
{
@ -16,12 +18,13 @@ namespace NzbDrone.Core.Test.MediaFiles
}
[Test]
public void get_files_by_series()
public void get_files_by_movie()
{
var files = Builder<MovieFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality =new QualityModel())
.With(c => c.Quality = new QualityModel())
.With(c => c.Languages = new List<Language> { Language.English })
.Random(4)
.With(s => s.MovieId = 12)
.BuildListOfNew();
@ -29,10 +32,10 @@ namespace NzbDrone.Core.Test.MediaFiles
Db.InsertMany(files);
var seriesFiles = Subject.GetFilesByMovie(12);
var movieFiles = Subject.GetFilesByMovie(12);
seriesFiles.Should().HaveCount(4);
seriesFiles.Should().OnlyContain(c => c.MovieId == 12);
movieFiles.Should().HaveCount(4);
movieFiles.Should().OnlyContain(c => c.MovieId == 12);
}
}

View File

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
Subject.GetRunTime(path).Seconds.Should().Be(10);
Subject.GetRunTime(path).Value.Seconds.Should().Be(10);
}
[Test]

View File

@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.MovieTests
private void GivenMovieLastRefreshedMonthsAgo()
{
_movie.LastInfoSync = DateTime.UtcNow.AddDays(-90);
_movie.LastInfoSync = DateTime.UtcNow.AddDays(-190);
}
private void GivenMovieLastRefreshedYesterday()

View File

@ -147,6 +147,7 @@
<Compile Include="Datastore\Migration\147_custom_formatsFixture.cs" />
<Compile Include="Datastore\Migration\149_regex_required_tagsFixture.cs" />
<Compile Include="Datastore\Migration\150_fix_format_tags_double_underscoreFixture.cs" />
<Compile Include="Datastore\Migration\156_add_download_client_priorityFixture.cs" />
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
@ -177,6 +178,7 @@
<Compile Include="DiskSpace\DiskSpaceServiceFixture.cs" />
<Compile Include="Download\CompletedDownloadServiceFixture.cs" />
<Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" />
<Compile Include="Download\DownloadClientProviderFixture.cs" />
<Compile Include="Download\DownloadClientStatusServiceFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\ScanWatchFolderFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\TorrentBlackholeFixture.cs" />

View File

@ -87,6 +87,7 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests
}
[TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("My-Server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")]
[TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")]
public void should_remap_remote_to_local(string host, string remotePath, string expectedLocalPath)
@ -101,6 +102,7 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests
}
[TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("My-Server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("my-server.localdomain", "/mnt/storage/", @"D:\mountedstorage")]
[TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")]
[TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")]

View File

@ -19,6 +19,7 @@ namespace NzbDrone.Core.Annotations
public bool Advanced { get; set; }
public Type SelectOptions { get; set; }
public string Section { get; set; }
public HiddenType Hidden { get; set; }
}
public enum FieldType
@ -30,7 +31,6 @@ namespace NzbDrone.Core.Annotations
Select,
Path,
FilePath,
Hidden,
Tag,
Action,
Url,
@ -38,4 +38,11 @@ namespace NzbDrone.Core.Annotations
OAuth,
Device
}
public enum HiddenType
{
Visible,
Hidden,
HiddenIfNotSet
}
}

View File

@ -12,7 +12,7 @@ namespace NzbDrone.Core.Datastore.Converters
{
if (context.DbValue == DBNull.Value)
{
return null;
return Quality.Unknown;
}
var val = Convert.ToInt32(context.DbValue);
@ -27,7 +27,7 @@ namespace NzbDrone.Core.Datastore.Converters
public object ToDB(object clrValue)
{
if(clrValue == DBNull.Value) return null;
if (clrValue == DBNull.Value) return 0;
if (clrValue as Quality == null)
{

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(156)]
public class add_download_client_priority : NzbDroneMigrationBase
{
// Need snapshot in time without having to instantiate.
private static HashSet<string> _usenetImplementations = new HashSet<string>
{
"Sabnzbd", "NzbGet", "NzbVortex", "UsenetBlackhole", "UsenetDownloadStation"
};
protected override void MainDbUpgrade()
{
Alter.Table("DownloadClients").AddColumn("Priority").AsInt32().WithDefaultValue(1);
Execute.WithConnection(InitPriorityForBackwardCompatibility);
}
private void InitPriorityForBackwardCompatibility(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT Id, Implementation FROM DownloadClients WHERE Enable = 1";
using (var reader = cmd.ExecuteReader())
{
int nextUsenet = 1;
int nextTorrent = 1;
while (reader.Read())
{
var id = reader.GetInt32(0);
var implName = reader.GetString(1);
var isUsenet = _usenetImplementations.Contains(implName);
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE DownloadClients SET Priority = ? WHERE Id = ?";
updateCmd.AddParameter(isUsenet ? nextUsenet++ : nextTorrent++);
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Datastore
{
public class ModelConflictException : NzbDroneException
{
public ModelConflictException(Type modelType, int modelId)
: base("{0} with ID {1} cannot be modified", modelType.Name, modelId)
{
}
public ModelConflictException(Type modelType, int modelId, string message)
: base("{0} with ID {1} {2}", modelType.Name, modelId, message)
{
}
}
}

View File

@ -33,13 +33,38 @@ namespace NzbDrone.Core.Download.Clients.Deluge
_proxy = proxy;
}
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
// set post-import category
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MovieImportedCategory != Settings.MovieCategory)
{
try
{
_proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieImportedCategory, Settings);
}
catch (DownloadClientUnavailableException)
{
_logger.Warn("Failed to set torrent post-import label \"{0}\" for {1} in Deluge. Does the label exist?",
Settings.MovieImportedCategory, downloadClientItem.Title);
}
}
}
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
if (!Settings.MovieCategory.IsNullOrWhiteSpace())
if (actualHash.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.MovieCategory, Settings);
throw new DownloadClientException("Deluge failed to add magnet " + magnetLink);
}
_proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings);
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(actualHash, Settings.MovieCategory, Settings);
}
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
@ -64,9 +89,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
_proxy.SetTorrentSeedingConfiguration(actualHash, remoteMovie.SeedConfiguration, Settings);
if (!Settings.MovieCategory.IsNullOrWhiteSpace())
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.MovieCategory, Settings);
_proxy.SetTorrentLabel(actualHash, Settings.MovieCategory, Settings);
}
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
@ -86,21 +111,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{
IEnumerable<DelugeTorrent> torrents;
try
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
if (!Settings.MovieCategory.IsNullOrWhiteSpace())
{
torrents = _proxy.GetTorrentsByLabel(Settings.MovieCategory, Settings);
}
else
{
torrents = _proxy.GetTorrents(Settings);
}
torrents = _proxy.GetTorrentsByLabel(Settings.MovieCategory, Settings);
}
catch (DownloadClientException ex)
else
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
torrents = _proxy.GetTorrents(Settings);
}
var items = new List<DownloadClientItem>();
@ -110,7 +127,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
if (torrent.Hash == null) continue;
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash?.ToUpper();
item.DownloadId = torrent.Hash.ToUpper();
item.Title = torrent.Name;
item.Category = Settings.MovieCategory;
@ -253,7 +270,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
private ValidationFailure TestCategory()
{
if (Settings.MovieCategory.IsNullOrWhiteSpace())
if (Settings.MovieCategory.IsNullOrWhiteSpace() && Settings.MovieImportedCategory.IsNullOrWhiteSpace())
{
return null;
}
@ -262,7 +279,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
if (!enabledPlugins.Contains("Label"))
{
return new NzbDroneValidationFailure("TvCategory", "Label plugin not activated")
return new NzbDroneValidationFailure("MovieCategory", "Label plugin not activated")
{
DetailedDescription = "You must have the Label plugin enabled in Deluge to use categories."
};
@ -270,7 +287,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var labels = _proxy.GetAvailableLabels(Settings);
if (!labels.Contains(Settings.MovieCategory))
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.MovieCategory))
{
_proxy.AddLabel(Settings.MovieCategory, Settings);
labels = _proxy.GetAvailableLabels(Settings);
@ -279,7 +296,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{
return new NzbDroneValidationFailure("MovieCategory", "Configuration of label failed")
{
DetailedDescription = "Radarr as unable to add the label to Deluge."
DetailedDescription = "Radarr was unable to add the label to Deluge."
};
}
}
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() && !labels.Contains(Settings.MovieImportedCategory))
{
_proxy.AddLabel(Settings.MovieImportedCategory, Settings);
labels = _proxy.GetAvailableLabels(Settings);
if (!labels.Contains(Settings.MovieImportedCategory))
{
return new NzbDroneValidationFailure("MovieImportedCategory", "Configuration of label failed")
{
DetailedDescription = "Radarr was unable to add the label to Deluge."
};
}
}

View File

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
string[] GetAvailablePlugins(DelugeSettings settings);
string[] GetEnabledPlugins(DelugeSettings settings);
string[] GetAvailableLabels(DelugeSettings settings);
void SetLabel(string hash, string label, DelugeSettings settings);
void SetTorrentLabel(string hash, string label, DelugeSettings settings);
void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings);
void AddLabel(string label, DelugeSettings settings);
@ -185,7 +185,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
ProcessRequest<object>(settings, "label.add", label);
}
public void SetLabel(string hash, string label, DelugeSettings settings)
public void SetTorrentLabel(string hash, string label, DelugeSettings settings)
{
ProcessRequest<object>(settings, "label.set_torrent", hash, label);
}

View File

@ -13,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.MovieCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -");
RuleFor(c => c.MovieImportedCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -");
}
}
@ -43,16 +44,19 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
public string MovieCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing movies that released within the last 21 days")]
[FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Radarr to set after it has imported the download. Leave blank to disable this feature.")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing movies that aired within the last 14 days")]
public int RecentMoviePriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing movies that released over 21 days ago")]
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing movies that aired over 14 days ago")]
public int OlderMoviePriority { get; set; }
[FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox)]
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()

View File

@ -9,6 +9,7 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using NzbDrone.Core.RemotePathMappings;
@ -43,12 +44,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var priority = remoteMovie.Movie.IsRecentMovie ? Settings.RecentMoviePriority : Settings.OlderMoviePriority;
var addpaused = Settings.AddPaused;
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, addpaused, Settings);
if (response == null)
{
throw new DownloadClientException("Failed to add nzb {0}", filename);
throw new DownloadClientRejectedReleaseException(remoteMovie.Release, "NZBGet rejected the NZB for an unknown reason");
}
return response;
@ -135,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
historyItem.CanMoveFiles = true;
historyItem.CanBeRemoved = true;
if (item.DeleteStatus == "MANUAL")
if (item.DeleteStatus == "MANUAL" || item.DeleteStatus == "COPY")
{
continue;
}

View File

@ -35,6 +35,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
// set post-import category
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MovieImportedCategory != Settings.MovieCategory)
{
try
{
Proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieImportedCategory, Settings);
}
catch (DownloadClientException)
{
_logger.Warn("Failed to set post-import torrent label \"{0}\" for {1} in qBittorrent. Does the label exist?",
Settings.MovieImportedCategory, downloadClientItem.Title);
}
}
}
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink)
{
if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr="))
@ -44,11 +62,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Proxy.AddTorrentFromUrl(magnetLink, Settings);
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
Proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
}
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
if (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First ||
@ -71,18 +84,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
try
{
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
Proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set the torrent label for {0}.", filename);
}
try
{
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
@ -218,6 +219,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
failures.AddIfNotNull(TestConnection());
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestCategory());
failures.AddIfNotNull(TestPrioritySupport());
failures.AddIfNotNull(TestGetTorrents());
}
@ -249,7 +251,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
else if (Settings.MovieCategory.IsNullOrWhiteSpace())
{
// warn if labels are supported, but category is not provided
return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
return new NzbDroneValidationFailure("MovieCategory", "Category is recommended")
{
IsWarning = true,
DetailedDescription = "Radarr will not attempt to import completed downloads without a category."
@ -295,6 +297,53 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return null;
}
private ValidationFailure TestCategory()
{
if (Settings.MovieCategory.IsNullOrWhiteSpace() && Settings.MovieImportedCategory.IsNullOrWhiteSpace())
{
return null;
}
// api v1 doesn't need to check/add categories as it's done on set
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
if (version < Version.Parse("2.0"))
{
return null;
}
Dictionary<string, QBittorrentLabel> labels = Proxy.GetLabels(Settings);
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MovieCategory))
{
Proxy.AddLabel(Settings.MovieCategory, Settings);
labels = Proxy.GetLabels(Settings);
if (!labels.ContainsKey(Settings.MovieCategory))
{
return new NzbDroneValidationFailure("MovieCategory", "Configuration of label failed")
{
DetailedDescription = "Radarr was unable to add the label to qBittorrent."
};
}
}
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MovieImportedCategory))
{
Proxy.AddLabel(Settings.MovieImportedCategory, Settings);
labels = Proxy.GetLabels(Settings);
if (!labels.ContainsKey(Settings.MovieImportedCategory))
{
return new NzbDroneValidationFailure("MovieImportedCategory", "Configuration of label failed")
{
DetailedDescription = "Radarr was unable to add the label to qBittorrent."
};
}
}
return null;
}
private ValidationFailure TestPrioritySupport()
{
var recentPriorityDefault = Settings.RecentMoviePriority == (int)QBittorrentPriority.Last;

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrentLabel
{
public string Name { get; set; }
public string SavePath { get; set; }
}
}

View File

@ -23,6 +23,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void AddLabel(string label, QBittorrentSettings settings);
Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);

View File

@ -190,6 +190,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void AddLabel(string label, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/addCategory")
.Post()
.AddFormParameter("category", label);
ProcessRequest(request, settings);
}
public Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings)
{
throw new NotSupportedException("qBittorrent api v1 does not support getting all torrent categories");
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
// Not supported on api v1

View File

@ -177,6 +177,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(request, settings);
}
public void AddLabel(string label, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/createCategory")
.Post()
.AddFormParameter("category", label);
ProcessRequest(request, settings);
}
public Dictionary<string, QBittorrentLabel> GetLabels(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/categories");
return Json.Deserialize<Dictionary<string, QBittorrentLabel>>(ProcessRequest(request, settings));
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;

View File

@ -11,6 +11,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.MovieCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'");
RuleFor(c => c.MovieImportedCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'");
}
}
@ -37,19 +40,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string MovieCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing movies that released within the last 21 days")]
[FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Leave blank to disable this feature.")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentMoviePriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing movies that released over 21 days ago")]
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderMoviePriority { get; set; }
[FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
[FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
public int InitialState { get; set; }
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()

View File

@ -39,6 +39,24 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
}
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
// set post-import category
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MovieImportedCategory != Settings.MovieCategory)
{
try
{
_proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieImportedCategory, Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to set torrent post-import label \"{0}\" for {1} in rTorrent. Does the label exist?",
Settings.MovieImportedCategory, downloadClientItem.Title);
}
}
}
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink)
{
var priority = (RTorrentPriority)(remoteMovie.Movie.IsRecentMovie ? Settings.RecentMoviePriority : Settings.OlderMoviePriority);

View File

@ -18,6 +18,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings);
void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings);
void RemoveTorrent(string hash, RTorrentSettings settings);
void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
bool HasHashTorrent(string hash, RTorrentSettings settings);
}
@ -44,6 +45,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("system.client_version")]
string GetVersion();
}
@ -90,20 +94,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
foreach (object[] torrent in ret)
{
var labelDecoded = System.Web.HttpUtility.UrlDecode((string) torrent[3]);
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
var item = new RTorrentTorrent();
item.Name = (string) torrent[0];
item.Hash = (string) torrent[1];
item.Path = (string) torrent[2];
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long) torrent[4];
item.RemainingSize = (long) torrent[5];
item.DownRate = (long) torrent[6];
item.Ratio = (long) torrent[7];
item.IsOpen = Convert.ToBoolean((long) torrent[8]);
item.IsActive = Convert.ToBoolean((long) torrent[9]);
item.IsFinished = Convert.ToBoolean((long) torrent[10]);
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
items.Add(item);
}
@ -157,6 +161,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
}
}
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.custom1.set");
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
{
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
}
}
public void RemoveTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.erase");

View File

@ -27,6 +27,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
Port = 8080;
UrlBase = "RPC2";
MovieCategory = "radarr";
OlderMoviePriority = (int)RTorrentPriority.Normal;
RecentMoviePriority = (int)RTorrentPriority.Normal;
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@ -50,16 +52,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional.")]
public string MovieCategory { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")]
[FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Leave blank to disable this feature.")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")]
public string MovieDirectory { get; set; }
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing movies that released within the last 21 days")]
[FieldDefinition(9, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentMoviePriority { get; set; }
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing movies that released over 21 days ago")]
[FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderMoviePriority { get; set; }
[FieldDefinition(10, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")]
[FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")]
public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate()

View File

@ -38,12 +38,32 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
_torrentCache = cacheManager.GetCache<UTorrentTorrentCache>(GetType(), "differentialTorrents");
}
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
// set post-import category
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.MovieImportedCategory != Settings.MovieCategory)
{
_proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieImportedCategory, Settings);
// old label must be explicitly removed
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.RemoveTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieCategory, Settings);
}
}
}
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
_proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteMovie.SeedConfiguration, Settings);
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings);
}
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
if (isRecentMovie && Settings.RecentMoviePriority == (int)UTorrentPriority.First ||
@ -60,13 +80,17 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
_proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteMovie.SeedConfiguration, Settings);
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings);
}
if (isRecentMovie && Settings.RecentMoviePriority == (int)UTorrentPriority.First ||
!isRecentMovie && Settings.OlderMoviePriority == (int)UTorrentPriority.First)
var isRecentEpisode = remoteMovie.Movie.IsRecentMovie;
if (isRecentEpisode && Settings.RecentMoviePriority == (int)UTorrentPriority.First ||
!isRecentEpisode && Settings.OlderMoviePriority == (int)UTorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash, Settings);
}

View File

@ -21,6 +21,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings);
void SetTorrentLabel(string hash, string label, UTorrentSettings settings);
void RemoveTorrentLabel(string hash, string label, UTorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings);
void SetState(string hash, UTorrentState state, UTorrentSettings settings);
}
@ -151,6 +152,20 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
ProcessRequest(requestBuilder, settings);
}
public void RemoveTorrentLabel(string hash, string label, UTorrentSettings settings)
{
var requestBuilder = BuildRequest(settings)
.AddQueryParam("action", "setprops")
.AddQueryParam("hash", hash);
requestBuilder.AddQueryParam("s", "label")
.AddQueryParam("v", label)
.AddQueryParam("s", "label")
.AddQueryParam("v", "");
ProcessRequest(requestBuilder, settings);
}
public void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings)
{
var requestBuilder = BuildRequest(settings)

View File

@ -41,13 +41,16 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
public string MovieCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing movies that released within the last 21 days")]
[FieldDefinition(5, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Leave blank to disable this feature.")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentMoviePriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing movies that released over 21 days ago")]
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderMoviePriority { get; set; }
[FieldDefinition(7, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")]
[FieldDefinition(8, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")]
public int IntialState { get; set; }
public NzbDroneValidationResult Validate()

View File

@ -151,6 +151,10 @@ namespace NzbDrone.Core.Download
return null;
}
public virtual void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
throw new NotSupportedException(this.Name + " does not support marking items as imported");
}
}
}

View File

@ -6,5 +6,6 @@ namespace NzbDrone.Core.Download
public class DownloadClientDefinition : ProviderDefinition
{
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } = 1;
}
}

View File

@ -1,6 +1,8 @@
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
using NzbDrone.Common.Cache;
using NLog;
namespace NzbDrone.Core.Download
{
@ -13,16 +15,53 @@ namespace NzbDrone.Core.Download
public class DownloadClientProvider : IProvideDownloadClient
{
private readonly Logger _logger;
private readonly IDownloadClientFactory _downloadClientFactory;
private readonly IDownloadClientStatusService _downloadClientStatusService;
private readonly ICached<int> _lastUsedDownloadClient;
public DownloadClientProvider(IDownloadClientFactory downloadClientFactory)
public DownloadClientProvider(IDownloadClientStatusService downloadClientStatusService, IDownloadClientFactory downloadClientFactory, ICacheManager cacheManager, Logger logger)
{
_logger = logger;
_downloadClientFactory = downloadClientFactory;
_downloadClientStatusService = downloadClientStatusService;
_lastUsedDownloadClient = cacheManager.GetCache<int>(GetType(), "lastDownloadClientId");
}
public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol)
{
return _downloadClientFactory.GetAvailableProviders().FirstOrDefault(v => v.Protocol == downloadProtocol);
var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList();
if (!availableProviders.Any()) return null;
var blockedProviders = new HashSet<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
if (blockedProviders.Any())
{
var nonBlockedProviders = availableProviders.Where(v => !blockedProviders.Contains(v.Definition.Id)).ToList();
if (nonBlockedProviders.Any())
{
availableProviders = nonBlockedProviders;
}
else
{
_logger.Trace("No non-blocked Download Client available, retrying blocked one.");
}
}
// Use the first priority clients first
availableProviders = availableProviders.GroupBy(v => (v.Definition as DownloadClientDefinition).Priority)
.OrderBy(v => v.Key)
.First().OrderBy(v => v.Definition.Id).ToList();
var lastId = _lastUsedDownloadClient.Find(downloadProtocol.ToString());
var provider = availableProviders.FirstOrDefault(v => v.Definition.Id > lastId) ?? availableProviders.First();
_lastUsedDownloadClient.Set(downloadProtocol.ToString(), provider.Definition.Id);
return provider;
}
public IEnumerable<IDownloadClient> GetDownloadClients()

View File

@ -35,15 +35,17 @@ namespace NzbDrone.Core.Download
public void Handle(DownloadCompletedEvent message)
{
if (!_configService.RemoveCompletedDownloads ||
message.TrackedDownload.DownloadItem.Removed ||
!message.TrackedDownload.DownloadItem.CanBeRemoved ||
message.TrackedDownload.DownloadItem.Status == DownloadItemStatus.Downloading)
if (_configService.RemoveCompletedDownloads &&
!message.TrackedDownload.DownloadItem.Removed &&
message.TrackedDownload.DownloadItem.CanBeRemoved &&
message.TrackedDownload.DownloadItem.Status != DownloadItemStatus.Downloading)
{
return;
RemoveFromDownloadClient(message.TrackedDownload);
}
RemoveFromDownloadClient(message.TrackedDownload);
else
{
MarkItemAsImported(message.TrackedDownload);
}
}
public void Handle(DownloadFailedEvent message)
@ -74,7 +76,25 @@ namespace NzbDrone.Core.Download
}
catch (Exception e)
{
_logger.Error(e, "Couldn't remove item from client {0}", trackedDownload.DownloadItem.Title);
_logger.Error(e, "Couldn't remove item {0} from client {1}", trackedDownload.DownloadItem.Title, downloadClient.Name);
}
}
private void MarkItemAsImported(TrackedDownload trackedDownload)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
try
{
_logger.Debug("[{0}] Marking download as imported from {1}", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClient);
downloadClient.MarkItemAsImported(trackedDownload.DownloadItem);
}
catch (NotSupportedException e)
{
_logger.Debug(e.Message);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't mark item {0} as imported from client {1}", trackedDownload.DownloadItem.Title, downloadClient.Name);
}
}
}

View File

@ -13,5 +13,6 @@ namespace NzbDrone.Core.Download
IEnumerable<DownloadClientItem> GetItems();
void RemoveItem(string downloadId, bool deleteData);
DownloadClientInfo GetStatus();
void MarkItemAsImported(DownloadClientItem downloadClientItem);
}
}

View File

@ -72,9 +72,28 @@ namespace NzbDrone.Core.Extras
.Select(e => e.Trim(' ', '.'))
.ToList();
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase));
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)).ToList();
var filteredFilenames = new List<string>();
var hasNfo = false;
foreach (var matchingFilename in matchingFilenames)
{
// Filter out duplicate NFO files
if (matchingFilename.EndsWith(".nfo", StringComparison.InvariantCultureIgnoreCase))
{
if (hasNfo)
{
continue;
}
hasNfo = true;
}
filteredFilenames.Add(matchingFilename);
}
foreach (var matchingFilename in filteredFilenames)
{
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e));

View File

@ -30,13 +30,13 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version");
}
if (monoVersion >= new Version("4.8.0"))
if (monoVersion >= new Version("4.4.2"))
{
_logger.Debug("Mono version is 4.8.0 or better: {0}", monoVersion);
_logger.Debug("Mono version is 4.4.2 or better: {0}", monoVersion);
return new HealthCheck(GetType());
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Your version of mono which is required by Radarr is deprecated and no longer supported. Core functionality of Radarr including automatic updates may be and will remain broken until you upgrade. You must manually upgrade your mono version to restore automatic update functionality.");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability.");
}
public override bool CheckOnSchedule => false;

View File

@ -33,11 +33,12 @@ namespace NzbDrone.Core.History
{
Unknown = 0,
Grabbed = 1,
SeriesFolderImported = 2, // to be deprecate
// SeriesFolderImported = 2, // deprecated
DownloadFolderImported = 3,
DownloadFailed = 4,
EpisodeFileDeleted = 5, // deprecated
// EpisodeFileDeleted = 5, // deprecated
MovieFileDeleted = 6,
MovieFolderImported = 7, // not used yet
MovieFileRenamed = 8
}
}

View File

@ -35,6 +35,7 @@ namespace NzbDrone.Core.History
IHandle<MovieImportedEvent>,
IHandle<DownloadFailedEvent>,
IHandle<MovieFileDeletedEvent>,
IHandle<MovieFileRenamedEvent>,
IHandle<MovieDeletedEvent>
{
private readonly IHistoryRepository _historyRepository;
@ -195,6 +196,30 @@ namespace NzbDrone.Core.History
_historyRepository.Insert(history);
}
public void Handle(MovieFileRenamedEvent message)
{
var sourcePath = message.OriginalPath;
var sourceRelativePath = message.Movie.Path.GetRelativePath(message.OriginalPath);
var path = Path.Combine(message.Movie.Path, message.MovieFile.RelativePath);
var relativePath = message.MovieFile.RelativePath;
var history = new History
{
EventType = HistoryEventType.MovieFileRenamed,
Date = DateTime.UtcNow,
Quality = message.MovieFile.Quality,
SourceTitle = message.OriginalPath,
MovieId = message.MovieFile.MovieId,
};
history.Data.Add("SourcePath", sourcePath);
history.Data.Add("SourceRelativePath", sourceRelativePath);
history.Data.Add("Path", path);
history.Data.Add("RelativePath", relativePath);
_historyRepository.Insert(history);
}
public void Handle(MovieDeletedEvent message)
{
_historyRepository.DeleteForMovie(message.Movie.Id);

View File

@ -48,7 +48,6 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws"));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("Nzbs.org", GetSettings("http://nzbs.org"));
yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me"));
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com"));

View File

@ -15,17 +15,10 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
public override bool SupportsSearch => true;
public override int PageSize => 50;
private readonly IHttpClient _httpClient;
private readonly IIndexerStatusService _indexerStatusService;
private readonly Logger _logger;
public PassThePopcorn(IHttpClient httpClient, ICacheManager cacheManager, IIndexerStatusService indexerStatusService,
IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_httpClient = httpClient;
_logger = logger;
_indexerStatusService = indexerStatusService;
}
public override IIndexerRequestGenerator GetRequestGenerator()
@ -42,20 +35,5 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
{
return new PassThePopcornParser(Settings, _logger);
}
/*protected override IndexerResponse FetchIndexerResponse(IndexerRequest request)
{
_logger.Debug("Downloading Feed " + request.HttpRequest.ToString(false));
if (request.HttpRequest.RateLimit < RateLimit)
{
request.HttpRequest.RateLimit = RateLimit;
}
//Potentially dangerous though if ptp moves domains!
request.HttpRequest.AllowAutoRedirect = false;
return new IndexerResponse(request, _httpClient.Execute(request.HttpRequest));
}*/
}
}

View File

@ -5,6 +5,7 @@
MissingFromDisk,
Manual,
Upgrade,
NoLinkedEpisodes
NoLinkedEpisodes,
ManualOverride
}
}

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.MovieImport;
@ -32,6 +33,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedMovie _importApprovedMovie;
private readonly IDetectSample _detectSample;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigService _config;
private readonly IHistoryService _historyService;
private readonly Logger _logger;
@ -43,6 +45,7 @@ namespace NzbDrone.Core.MediaFiles
IMakeImportDecision importDecisionMaker,
IImportApprovedMovie importApprovedMovie,
IDetectSample detectSample,
IRuntimeInfo runtimeInfo,
IConfigService config,
IHistoryService historyService,
Logger logger)
@ -54,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles
_importDecisionMaker = importDecisionMaker;
_importApprovedMovie = importApprovedMovie;
_detectSample = detectSample;
_runtimeInfo = runtimeInfo;
_config = config;
_historyService = historyService;
_logger = logger;
@ -104,7 +108,7 @@ namespace NzbDrone.Core.MediaFiles
return ProcessFile(fileInfo, importMode, movie, downloadClientItem);
}
_logger.Error("Import failed, path does not exist or is not accessible by Radarr: {0}", path);
LogInaccessiblePathError(path);
return new List<ImportResult>();
}
@ -273,5 +277,31 @@ namespace NzbDrone.Core.MediaFiles
return new ImportResult(new ImportDecision(localMovie, new Rejection("Unknown Movie")), message);
}
private void LogInaccessiblePathError(string path)
{
if (_runtimeInfo.IsWindowsService)
{
var mounts = _diskProvider.GetMounts();
var mount = mounts.FirstOrDefault(m => m.RootDirectory == Path.GetPathRoot(path));
if (mount.DriveType == DriveType.Network)
{
_logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}. It's recommended to avoid mapped network drives when running as a Windows service. See the FAQ for more info", path);
return;
}
}
if (OsInfo.IsWindows)
{
if (path.StartsWith(@"\\"))
{
_logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}. Ensure the user running Sonarr has access to the network share", path);
return;
}
}
_logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}. Ensure the path exists and the user running Sonarr has the correct permissions to access this file/folder", path);
}
}
}

View File

@ -0,0 +1,19 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieFileRenamedEvent : IEvent
{
public Movie Movie { get; private set; }
public MovieFile MovieFile { get; private set; }
public string OriginalPath { get; private set; }
public MovieFileRenamedEvent(Movie movie, MovieFile movieFile, string originalPath)
{
Movie = movie;
MovieFile = movieFile;
OriginalPath = originalPath;
}
}
}

View File

@ -92,7 +92,6 @@ namespace NzbDrone.Core.MediaFiles
{
_logger.Warn(ex, "Unable to apply permissions to: " + path);
_logger.Debug(ex, ex.Message);
}
}
}

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