New: Upstream Updates

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
pull/2/head
Qstick 5 years ago
parent bfc467dd96
commit 23670bca12

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@ -120,8 +121,13 @@ class AddNewMovie extends Component {
} }
{ {
!isFetching && !!error && !isFetching && !!error ?
<div>Failed to load search results, please try again.</div> <div className={styles.message}>
<div className={styles.helpText}>
Failed to load search results, please try again.
</div>
<div>{getErrorMessage(error)}</div>
</div> : null
} }
{ {

@ -99,10 +99,10 @@ class ImportMovieSelectFolder extends Component {
Some tips to ensure the import goes smoothly: Some tips to ensure the import goes smoothly:
<ul> <ul>
<li className={styles.tip}> <li className={styles.tip}>
Make sure your files include the quality in the name. eg. <span className={styles.code}>movie.2008.bluray.mkv</span> Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>movie.2008.bluray.mkv</span>
</li> </li>
<li className={styles.tip}> <li className={styles.tip}>
Point Radarr to the folder containing all of your movies not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span> Point Radarr to the folder containing all of your movies, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span>
</li> </li>
</ul> </ul>
</div> </div>

@ -16,10 +16,13 @@
display: flex; display: flex;
} }
.episodeInfo {
color: $calendarTextDim;
}
.seriesTitle, .seriesTitle,
.episodeTitle { .episodeTitle {
@add-mixin truncate; @add-mixin truncate;
flex: 1 0 1px; flex: 1 0 1px;
margin-right: 10px; margin-right: 10px;
} }
@ -37,6 +40,10 @@
margin-left: 3px; margin-left: 3px;
} }
.airTime {
color: $calendarTextDim;
}
/* /*
* Status * Status
*/ */

@ -14,7 +14,6 @@
.seriesTitle { .seriesTitle {
@add-mixin truncate; @add-mixin truncate;
flex: 1 0 1px; flex: 1 0 1px;
margin-right: 10px; margin-right: 10px;
color: #3a3f51; color: #3a3f51;
@ -23,10 +22,12 @@
.airTime { .airTime {
flex: 1 0 1px; flex: 1 0 1px;
color: $calendarTextDim;
} }
.episodeInfo { .episodeInfo {
margin-left: 10px; margin-left: 10px;
color: $calendarTextDim;
} }
.absoluteEpisodeNumber { .absoluteEpisodeNumber {
@ -80,3 +81,7 @@
.premiere { .premiere {
composes: premiere from '~Calendar/Events/CalendarEvent.css'; composes: premiere from '~Calendar/Events/CalendarEvent.css';
} }
.unaired {
composes: unaired from '~Calendar/Events/CalendarEvent.css';
}

@ -144,6 +144,7 @@ class FileBrowserModalContent extends Component {
<Scroller <Scroller
ref={this.setScrollerRef} ref={this.setScrollerRef}
className={styles.scroller} className={styles.scroller}
scrollDirection={scrollDirections.BOTH}
> >
{ {
!!error && !!error &&
@ -152,7 +153,10 @@ class FileBrowserModalContent extends Component {
{ {
isPopulated && !error && isPopulated && !error &&
<Table columns={columns}> <Table
horizontalScroll={false}
columns={columns}
>
<TableBody> <TableBody>
{ {
emptyParent && emptyParent &&

@ -132,6 +132,7 @@ class FilterBuilderModalContent extends Component {
filterBuilderProps, filterBuilderProps,
isSaving, isSaving,
saveError, saveError,
onCancelPress,
onModalClose onModalClose
} = this.props; } = this.props;
@ -190,7 +191,7 @@ class FilterBuilderModalContent extends Component {
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onCancelPress}>
Cancel Cancel
</Button> </Button>
@ -220,6 +221,7 @@ FilterBuilderModalContent.propTypes = {
dispatchDeleteCustomFilter: PropTypes.func.isRequired, dispatchDeleteCustomFilter: PropTypes.func.isRequired,
onSaveCustomFilterPress: PropTypes.func.isRequired, onSaveCustomFilterPress: PropTypes.func.isRequired,
dispatchSetFilter: PropTypes.func.isRequired, dispatchSetFilter: PropTypes.func.isRequired,
onCancelPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -34,6 +34,17 @@ class FilterModal extends Component {
}); });
} }
onCancelPress = () => {
if (this.state.filterBuilder) {
this.setState({
filterBuilder: false,
id: null
});
} else {
this.onModalClose();
}
}
onModalClose = () => { onModalClose = () => {
this.setState({ this.setState({
filterBuilder: false, filterBuilder: false,
@ -67,6 +78,7 @@ class FilterModal extends Component {
<FilterBuilderModalContentConnector <FilterBuilderModalContentConnector
{...otherProps} {...otherProps}
id={id} id={id}
onCancelPress={this.onCancelPress}
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
/> : /> :
<CustomFiltersModalContentConnector <CustomFiltersModalContentConnector

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames'; import classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import isMobileUtil from 'Utilities/isMobile'; import { isMobile as isMobileUtil } from 'Utilities/mobile';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, sizes, scrollDirections } from 'Helpers/Props'; import { icons, sizes, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';

@ -18,10 +18,19 @@ class PathInput extends Component {
this._node = document.getElementById('portal-root'); this._node = document.getElementById('portal-root');
this.state = { this.state = {
value: props.value,
isFileBrowserModalOpen: false isFileBrowserModalOpen: false
}; };
} }
componentDidUpdate(prevProps) {
const { value } = this.props;
if (prevProps.value !== value) {
this.setState({ value });
}
}
// //
// Control // Control
@ -51,11 +60,8 @@ class PathInput extends Component {
// //
// Listeners // Listeners
onInputChange = (event, { newValue }) => { onInputChange = ({ value }) => {
this.props.onChange({ this.setState({ value });
name: this.props.name,
value: newValue
});
} }
onInputKeyDown = (event) => { onInputKeyDown = (event) => {
@ -77,6 +83,11 @@ class PathInput extends Component {
} }
onInputBlur = () => { onInputBlur = () => {
this.props.onChange({
name: this.props.name,
value: this.state.value
});
this.props.onClearPaths(); this.props.onClearPaths();
} }
@ -108,13 +119,18 @@ class PathInput extends Component {
const { const {
className, className,
name, name,
value,
paths, paths,
includeFiles, includeFiles,
hasFileBrowser, hasFileBrowser,
onChange, onChange,
...otherProps ...otherProps
} = this.props; } = this.props;
const {
value,
isFileBrowserModalOpen
} = this.state;
return ( return (
<div className={className}> <div className={className}>
<AutoSuggestInput <AutoSuggestInput
@ -130,7 +146,7 @@ class PathInput extends Component {
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onChange={onChange} onChange={this.onInputChange}
/> />
{ {
@ -144,7 +160,7 @@ class PathInput extends Component {
</FormInputButton> </FormInputButton>
<FileBrowserModal <FileBrowserModal
isOpen={this.state.isFileBrowserModalOpen} isOpen={isFileBrowserModalOpen}
name={name} name={name}
value={value} value={value}
includeFiles={includeFiles} includeFiles={includeFiles}

@ -14,7 +14,6 @@
.internalInput { .internalInput {
flex: 1 1 0%; flex: 1 1 0%;
margin-top: -6px;
margin-left: 3px; margin-left: 3px;
min-width: 20%; min-width: 20%;
max-width: 100%; max-width: 100%;

@ -4,8 +4,9 @@
bottom: -1px; bottom: -1px;
left: -1px; left: -1px;
display: flex; display: flex;
align-items: start;
flex-wrap: wrap; flex-wrap: wrap;
padding: 6px 16px; padding: 1px 16px;
height: 33px; min-height: 33px;
cursor: default; cursor: default;
} }

@ -0,0 +1,5 @@
.tag {
composes: link from '~Components/Link/Link.css';
height: 31px;
}

@ -4,6 +4,7 @@ import { kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape'; import tagShape from 'Helpers/Props/Shapes/tagShape';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import styles from './TagInputTag.css';
class TagInputTag extends Component { class TagInputTag extends Component {
@ -31,9 +32,9 @@ class TagInputTag extends Component {
tag, tag,
kind kind
} = this.props; } = this.props;
return ( return (
<Link <Link
className={styles.tag}
tabIndex={-1} tabIndex={-1}
onPress={this.onDelete} onPress={this.onDelete}
> >

@ -39,6 +39,7 @@ class Menu extends Component {
this._scheduleUpdate = null; this._scheduleUpdate = null;
this._menuButtonId = getUniqueElememtId(); this._menuButtonId = getUniqueElememtId();
this._menuContentId = getUniqueElememtId();
this.state = { this.state = {
isMenuOpen: false, isMenuOpen: false,
@ -99,12 +100,14 @@ class Menu extends Component {
window.addEventListener('resize', this.onWindowResize); window.addEventListener('resize', this.onWindowResize);
window.addEventListener('scroll', this.onWindowScroll, { capture: true }); window.addEventListener('scroll', this.onWindowScroll, { capture: true });
window.addEventListener('click', this.onWindowClick); window.addEventListener('click', this.onWindowClick);
window.addEventListener('touchstart', this.onTouchStart);
} }
_removeListener() { _removeListener() {
window.removeEventListener('resize', this.onWindowResize); window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('scroll', this.onWindowScroll, { capture: true }); window.removeEventListener('scroll', this.onWindowScroll, { capture: true });
window.removeEventListener('click', this.onWindowClick); window.removeEventListener('click', this.onWindowClick);
window.removeEventListener('touchstart', this.onTouchStart);
} }
// //
@ -123,6 +126,30 @@ class Menu extends Component {
} }
} }
onTouchStart = (event) => {
const menuButton = document.getElementById(this._menuButtonId);
const menuContent = document.getElementById(this._menuContentId);
if (!menuButton || !menuContent) {
return;
}
if (event.targetTouches.length !== 1) {
return;
}
const target = event.targetTouches[0].target;
if (
!menuButton.contains(target) &&
!menuContent.contains(target) &&
this.state.isMenuOpen
) {
this.setState({ isMenuOpen: false });
this._removeListener();
}
}
onWindowResize = () => { onWindowResize = () => {
this.setMaxHeight(); this.setMaxHeight();
} }

@ -12,6 +12,7 @@ class MenuContent extends Component {
const { const {
forwardedRef, forwardedRef,
className, className,
id,
children, children,
style, style,
isOpen isOpen
@ -19,6 +20,7 @@ class MenuContent extends Component {
return ( return (
<div <div
id={id}
ref={forwardedRef} ref={forwardedRef}
className={className} className={className}
style={style} style={style}
@ -38,6 +40,7 @@ class MenuContent extends Component {
MenuContent.propTypes = { MenuContent.propTypes = {
forwardedRef: PropTypes.func, forwardedRef: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
style: PropTypes.object, style: PropTypes.object,
isOpen: PropTypes.bool isOpen: PropTypes.bool

@ -1,6 +1,5 @@
.menuItem { .menuItem {
@add-mixin truncate; @add-mixin truncate;
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
padding: 10px 20px; padding: 10px 20px;
@ -17,3 +16,8 @@
text-decoration: none; text-decoration: none;
} }
} }
.isDisabled {
color: $disabledColor;
pointer-events: none;
}

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import styles from './MenuItem.css'; import styles from './MenuItem.css';
@ -12,12 +13,17 @@ class MenuItem extends Component {
const { const {
className, className,
children, children,
isDisabled,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<Link <Link
className={className} className={classNames(
className,
isDisabled && styles.isDisabled
)}
isDisabled={isDisabled}
{...otherProps} {...otherProps}
> >
{children} {children}
@ -28,11 +34,13 @@ class MenuItem extends Component {
MenuItem.propTypes = { MenuItem.propTypes = {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired,
isDisabled: PropTypes.bool.isRequired
}; };
MenuItem.defaultProps = { MenuItem.defaultProps = {
className: styles.menuItem className: styles.menuItem,
isDisabled: false
}; };
export default MenuItem; export default MenuItem;

@ -29,6 +29,12 @@
overflow: hidden !important; overflow: hidden !important;
} }
.modalOpenIOS {
position: fixed;
right: 0;
left: 0;
}
/* /*
* Sizes * Sizes
*/ */

@ -4,6 +4,8 @@ import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import elementClass from 'element-class'; import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { setScrollLock } from 'Utilities/scrollLock';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import ErrorBoundary from 'Components/Error/ErrorBoundary'; import ErrorBoundary from 'Components/Error/ErrorBoundary';
@ -31,6 +33,7 @@ class Modal extends Component {
this._node = document.getElementById('portal-root'); this._node = document.getElementById('portal-root');
this._backgroundRef = null; this._backgroundRef = null;
this._modalId = getUniqueElememtId(); this._modalId = getUniqueElememtId();
this._bodyScrollTop = 0;
} }
componentDidMount() { componentDidMount() {
@ -69,7 +72,14 @@ class Modal extends Component {
window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keydown', this.onKeyDown);
if (openModals.length === 1) { if (openModals.length === 1) {
elementClass(document.body).add(styles.modalOpen); if (isIOS()) {
setScrollLock(true);
const scrollTop = document.body.scrollTop;
this._bodyScrollTop = scrollTop;
elementClass(document.body).add(styles.modalOpenIOS);
} else {
elementClass(document.body).add(styles.modalOpen);
}
} }
} }
@ -78,7 +88,14 @@ class Modal extends Component {
window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keydown', this.onKeyDown);
if (openModals.length === 0) { if (openModals.length === 0) {
elementClass(document.body).remove(styles.modalOpen); setScrollLock(false);
if (isIOS()) {
elementClass(document.body).remove(styles.modalOpenIOS);
document.body.scrollTop = this._bodyScrollTop;
} else {
elementClass(document.body).remove(styles.modalOpen);
}
} }
} }

@ -48,7 +48,7 @@ ModalBody.propTypes = {
className: PropTypes.string, className: PropTypes.string,
innerClassName: PropTypes.string, innerClassName: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]) scrollDirection: PropTypes.oneOf(scrollDirections.all)
}; };
ModalBody.defaultProps = { ModalBody.defaultProps = {

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import OverlayScroller from 'Components/Scroller/OverlayScroller'; import OverlayScroller from 'Components/Scroller/OverlayScroller';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
@ -7,6 +8,17 @@ import styles from './PageContentBody.css';
class PageContentBody extends Component { class PageContentBody extends Component {
//
// Listeners
onScroll = (props) => {
const { onScroll } = this.props;
if (this.props.onScroll && !isLocked()) {
onScroll(props);
}
}
// //
// Render // Render
@ -27,6 +39,7 @@ class PageContentBody extends Component {
className={className} className={className}
scrollDirection={scrollDirections.VERTICAL} scrollDirection={scrollDirections.VERTICAL}
{...otherProps} {...otherProps}
onScroll={this.onScroll}
> >
<div className={innerClassName}> <div className={innerClassName}>
{children} {children}
@ -41,6 +54,7 @@ PageContentBody.propTypes = {
innerClassName: PropTypes.string, innerClassName: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
onScroll: PropTypes.func,
dispatch: PropTypes.func dispatch: PropTypes.func
}; };

@ -2,6 +2,7 @@
@add-mixin scrollbar; @add-mixin scrollbar;
@add-mixin scrollbarTrack; @add-mixin scrollbarTrack;
@add-mixin scrollbarThumb; @add-mixin scrollbarThumb;
-webkit-overflow-scrolling: touch;
} }
.none { .none {
@ -26,3 +27,11 @@
overflow-x: auto; overflow-x: auto;
} }
} }
.both {
overflow: scroll;
&.autoScroll {
overflow: auto;
}
}

@ -66,7 +66,7 @@ class Scroller extends Component {
Scroller.propTypes = { Scroller.propTypes = {
className: PropTypes.string, className: PropTypes.string,
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
autoScroll: PropTypes.bool.isRequired, autoScroll: PropTypes.bool.isRequired,
scrollTop: PropTypes.number, scrollTop: PropTypes.number,
children: PropTypes.node, children: PropTypes.node,

@ -9,6 +9,7 @@ import titleCase from 'Utilities/String/titleCase';
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions'; import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
import { setAppValue, setVersion } from 'Store/Actions/appActions'; import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { update, updateItem, removeItem } from 'Store/Actions/baseActions'; import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchHealth } from 'Store/Actions/systemActions'; import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions'; import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
@ -72,6 +73,7 @@ const mapDispatchToProps = {
dispatchFetchQueue: fetchQueue, dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails, dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders, dispatchFetchRootFolders: fetchRootFolders,
dispatchFetchMovies: fetchMovies,
dispatchFetchTags: fetchTags, dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails dispatchFetchTagDetails: fetchTagDetails
}; };
@ -258,6 +260,7 @@ class SignalRConnector extends Component {
const { const {
dispatchFetchCommands, dispatchFetchCommands,
dispatchFetchMovies,
dispatchSetAppValue dispatchSetAppValue
} = this.props; } = this.props;
@ -265,6 +268,7 @@ class SignalRConnector extends Component {
// are in sync after reconnecting. // are in sync after reconnecting.
if (this.props.isReconnecting || this.props.isDisconnected) { if (this.props.isReconnecting || this.props.isDisconnected) {
dispatchFetchMovies();
dispatchFetchCommands(); dispatchFetchCommands();
repopulatePage(); repopulatePage();
} }
@ -346,6 +350,7 @@ SignalRConnector.propTypes = {
dispatchFetchQueue: PropTypes.func.isRequired, dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired, dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchMovies: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired dispatchFetchTagDetails: PropTypes.func.isRequired
}; };

@ -1,5 +1,7 @@
.tableContainer { .tableContainer {
overflow-x: auto; &.horizontalScroll {
overflow-x: auto;
}
} }
.table { .table {
@ -10,7 +12,12 @@
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.tableContainer { .tableContainer {
overflow-y: hidden; min-width: 100%;
width: 100%; width: fit-content;
&.horizontalScroll {
overflow-y: hidden;
width: 100%;
}
} }
} }

@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { icons, scrollDirections } from 'Helpers/Props'; import { icons, scrollDirections } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
@ -28,6 +29,7 @@ function getTableHeaderCellProps(props) {
function Table(props) { function Table(props) {
const { const {
className, className,
horizontalScroll,
selectAll, selectAll,
columns, columns,
optionsComponent, optionsComponent,
@ -41,14 +43,22 @@ function Table(props) {
return ( return (
<Scroller <Scroller
className={styles.tableContainer} className={classNames(
scrollDirection={scrollDirections.HORIZONTAL} styles.tableContainer,
horizontalScroll && styles.horizontalScroll
)}
scrollDirection={
horizontalScroll ?
scrollDirections.HORIZONTAL :
scrollDirections.NONE
}
> >
<table className={className}> <table className={className}>
<TableHeader> <TableHeader>
{ {
selectAll && selectAll ?
<TableSelectAllHeaderCell {...otherProps} /> <TableSelectAllHeaderCell {...otherProps} /> :
null
} }
{ {
@ -111,6 +121,7 @@ function Table(props) {
Table.propTypes = { Table.propTypes = {
className: PropTypes.string, className: PropTypes.string,
horizontalScroll: PropTypes.bool.isRequired,
selectAll: PropTypes.bool.isRequired, selectAll: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
optionsComponent: PropTypes.elementType, optionsComponent: PropTypes.elementType,
@ -123,6 +134,7 @@ Table.propTypes = {
Table.defaultProps = { Table.defaultProps = {
className: styles.table, className: styles.table,
horizontalScroll: true,
selectAll: false selectAll: false
}; };

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { WindowScroller } from 'react-virtualized'; import { WindowScroller } from 'react-virtualized';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure'; import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
@ -83,6 +84,16 @@ class VirtualTable extends Component {
} }
} }
onScroll = (props) => {
if (isLocked()) {
return;
}
const { onScroll } = this.props;
onScroll(props);
}
// //
// Render // Render
@ -107,7 +118,7 @@ class VirtualTable extends Component {
<Measure onMeasure={this.onMeasure}> <Measure onMeasure={this.onMeasure}>
<WindowScroller <WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode} scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll} onScroll={this.onScroll}
> >
{({ height, isScrolling }) => { {({ height, isScrolling }) => {
return ( return (

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames'; import classNames from 'classnames';
import isMobileUtil from 'Utilities/isMobile'; import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { kinds, tooltipPositions } from 'Helpers/Props'; import { kinds, tooltipPositions } from 'Helpers/Props';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import styles from './Tooltip.css'; import styles from './Tooltip.css';

@ -1,5 +1,6 @@
export const NONE = 'none'; export const NONE = 'none';
export const BOTH = 'both';
export const HORIZONTAL = 'horizontal'; export const HORIZONTAL = 'horizontal';
export const VERTICAL = 'vertical'; export const VERTICAL = 'vertical';
export const all = [NONE, HORIZONTAL, VERTICAL]; export const all = [NONE, HORIZONTAL, VERTICAL, BOTH];

@ -5,7 +5,7 @@ import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -73,7 +73,7 @@ const filterExistingFilesOptions = {
const importModeOptions = [ const importModeOptions = [
{ key: 'move', value: 'Move Files' }, { key: 'move', value: 'Move Files' },
{ key: 'copy', value: 'Copy Files' } { key: 'copy', value: 'Hardlink/Copy Files' }
]; ];
const SELECT = 'select'; const SELECT = 'select';
@ -217,7 +217,7 @@ class InteractiveImportModalContent extends Component {
Manual Import - {title || folder} Manual Import - {title || folder}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody scrollDirection={scrollDirections.BOTH}>
{ {
showFilterExistingFiles && showFilterExistingFiles &&
<div className={styles.filterContainer}> <div className={styles.filterContainer}>
@ -270,6 +270,7 @@ class InteractiveImportModalContent extends Component {
isPopulated && !!items.length && !isFetching && !isFetching && isPopulated && !!items.length && !isFetching && !isFetching &&
<Table <Table
columns={columns} columns={columns}
horizontalScroll={false}
selectAll={true} selectAll={true}
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}

@ -43,6 +43,7 @@ class InteractiveImportModalContentConnector extends Component {
componentDidMount() { componentDidMount() {
const { const {
downloadId, downloadId,
movieId,
folder folder
} = this.props; } = this.props;
@ -52,6 +53,7 @@ class InteractiveImportModalContentConnector extends Component {
this.props.dispatchFetchInteractiveImportItems({ this.props.dispatchFetchInteractiveImportItems({
downloadId, downloadId,
movieId,
folder, folder,
filterExistingFiles filterExistingFiles
}); });
@ -65,11 +67,13 @@ class InteractiveImportModalContentConnector extends Component {
if (prevState.filterExistingFiles !== filterExistingFiles) { if (prevState.filterExistingFiles !== filterExistingFiles) {
const { const {
downloadId, downloadId,
movieId,
folder folder
} = this.props; } = this.props;
this.props.dispatchFetchInteractiveImportItems({ this.props.dispatchFetchInteractiveImportItems({
downloadId, downloadId,
movieId,
folder, folder,
filterExistingFiles filterExistingFiles
}); });
@ -172,6 +176,7 @@ class InteractiveImportModalContentConnector extends Component {
InteractiveImportModalContentConnector.propTypes = { InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string, downloadId: PropTypes.string,
movieId: PropTypes.number,
folder: PropTypes.string, folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired, filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -1,3 +1,15 @@
.protocol {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.indexer {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px;
}
.quality, .quality,
.language { .language {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
@ -20,3 +32,9 @@
white-space: nowrap; white-space: nowrap;
} }
.peers {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}

@ -124,7 +124,7 @@ class InteractiveSearchRow extends Component {
return ( return (
<TableRow> <TableRow>
<TableRowCell> <TableRowCell className={styles.protocol}>
<ProtocolLabel <ProtocolLabel
protocol={protocol} protocol={protocol}
/> />
@ -143,7 +143,7 @@ class InteractiveSearchRow extends Component {
</Link> </Link>
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell className={styles.indexer}>
{indexer} {indexer}
</TableRowCell> </TableRowCell>
@ -151,7 +151,7 @@ class InteractiveSearchRow extends Component {
{formatBytes(size)} {formatBytes(size)}
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell className={styles.peers}>
{ {
protocol === 'torrent' && protocol === 'torrent' &&
<Peers <Peers

@ -22,6 +22,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
@ -35,7 +36,6 @@ import MovieDetailsLinks from './MovieDetailsLinks';
import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable'; import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable';
// import MovieTagsConnector from './MovieTagsConnector'; // import MovieTagsConnector from './MovieTagsConnector';
import styles from './MovieDetails.css'; import styles from './MovieDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
const defaultFontSize = parseInt(fonts.defaultFontSize); const defaultFontSize = parseInt(fonts.defaultFontSize);
@ -528,6 +528,7 @@ class MovieDetails extends Component {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
movieId={id}
folder={path} folder={path}
allowMovieChange={false} allowMovieChange={false}
showFilterExistingFiles={true} showFilterExistingFiles={true}

@ -1,7 +1,16 @@
.link { .link {
composes: link from '~Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
}
.unavailablePath {
display: flex;
align-items: center;
}
.unavailableLabel {
composes: label from '~Components/Label.css';
display: block; margin-left: 10px;
} }
.freeSpace, .freeSpace,

@ -1,7 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import { icons } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
@ -12,30 +13,45 @@ function RootFolderRow(props) {
const { const {
id, id,
path, path,
accessible,
freeSpace, freeSpace,
unmappedFolders, unmappedFolders,
onDeletePress onDeletePress
} = props; } = props;
const unmappedFoldersCount = unmappedFolders.length || '-'; const isUnavailable = !accessible;
return ( return (
<TableRow> <TableRow>
<TableRowCell> <TableRowCell>
<Link {
className={styles.link} isUnavailable ?
to={`/add/import/${id}`} <div className={styles.unavailablePath}>
> {path}
{path}
</Link> <Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
Unavailable
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.freeSpace}> <TableRowCell className={styles.freeSpace}>
{formatBytes(freeSpace) || '-'} {(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.unmappedFolders}> <TableRowCell className={styles.unmappedFolders}>
{unmappedFoldersCount} {isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.actions}> <TableRowCell className={styles.actions}>
@ -52,13 +68,13 @@ function RootFolderRow(props) {
RootFolderRow.propTypes = { RootFolderRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
freeSpace: PropTypes.number.isRequired, accessible: PropTypes.bool.isRequired,
freeSpace: PropTypes.number,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired, unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired onDeletePress: PropTypes.func.isRequired
}; };
RootFolderRow.defaultProps = { RootFolderRow.defaultProps = {
freeSpace: 0,
unmappedFolders: [] unmappedFolders: []
}; };

@ -6,6 +6,12 @@ import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
const logLevelOptions = [
{ key: 'info', value: 'Info' },
{ key: 'debug', value: 'Debug' },
{ key: 'trace', value: 'Trace' }
];
function LoggingSettings(props) { function LoggingSettings(props) {
const { const {
settings, settings,
@ -16,12 +22,6 @@ function LoggingSettings(props) {
logLevel logLevel
} = settings; } = settings;
const logLevelOptions = [
{ key: 'info', value: 'Info' },
{ key: 'debug', value: 'Debug' },
{ key: 'trace', value: 'Trace' }
];
return ( return (
<FieldSet legend="Logging"> <FieldSet legend="Logging">
<FormGroup> <FormGroup>

@ -12,6 +12,7 @@ import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import RootFoldersConnector from 'RootFolder/RootFoldersConnector'; import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
import NamingConnector from './Naming/NamingConnector'; import NamingConnector from './Naming/NamingConnector';
import AddRootFolderConnector from './RootFolder/AddRootFolderConnector';
const rescanAfterRefreshOptions = [ const rescanAfterRefreshOptions = [
{ key: 'always', value: 'Always' }, { key: 'always', value: 'Always' },
@ -135,6 +136,23 @@ class MediaManagement extends Component {
</FormGroup> </FormGroup>
} }
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>Minimum Free Space</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
unit='MB'
name="minimumFreeSpaceWhenImporting"
helpText="Prevent import if it would leave less than this amount of disk space available"
onChange={onInputChange}
{...settings.minimumFreeSpaceWhenImporting}
/>
</FormGroup>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
@ -281,6 +299,23 @@ class MediaManagement extends Component {
{...settings.recycleBin} {...settings.recycleBin}
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Recycling Bin Cleanup</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="recycleBinCleanupDays"
helpText="Set to 0 to disable automatic cleanup"
helpTextWarning="Files in the recycle bin older than the selected number of days will be cleaned up automatically"
min={0}
onChange={onInputChange}
{...settings.recycleBinCleanupDays}
/>
</FormGroup>
</FieldSet> </FieldSet>
{ {
@ -374,6 +409,7 @@ class MediaManagement extends Component {
<FieldSet legend="Root Folders"> <FieldSet legend="Root Folders">
<RootFoldersConnector /> <RootFoldersConnector />
<AddRootFolderConnector />
</FieldSet> </FieldSet>
</PageContentBodyConnector> </PageContentBodyConnector>
</PageContent> </PageContent>

@ -0,0 +1,7 @@
.addRootFolderButtonContainer {
margin-top: 20px;
}
.importButtonIcon {
margin-right: 8px;
}

@ -0,0 +1,71 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import styles from './AddRootFolder.css';
class AddRootFolder extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNewRootFolderModalOpen: false
};
}
//
// Lifecycle
onAddNewRootFolderPress = () => {
this.setState({ isAddNewRootFolderModalOpen: true });
}
onNewRootFolderSelect = ({ value }) => {
this.props.onNewRootFolderSelect(value);
}
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
}
//
// Render
render() {
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Add Root Folder
</Button>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
);
}
}
AddRootFolder.propTypes = {
onNewRootFolderSelect: PropTypes.func.isRequired
};
export default AddRootFolder;

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import AddRootFolder from './AddRootFolder';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
function createMapDispatchToProps(dispatch) {
return {
onNewRootFolderSelect(path) {
dispatch(addRootFolder({ path }));
}
};
}
export default connect(null, createMapDispatchToProps)(AddRootFolder);

@ -238,7 +238,11 @@ class EditQualityProfileModalContent extends Component {
id ? id ?
<div <div
className={styles.deleteButtonContainer} className={styles.deleteButtonContainer}
title={isInUse ? 'Can\'t delete a quality profile that is attached to a movie' : undefined} title={
isInUse ?
'Can\'t delete a quality profile that is attached to a movie' :
undefined
}
> >
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}

@ -147,6 +147,7 @@ function TagDetailsModalContent(props) {
<Button <Button
className={styles.deleteButton} className={styles.deleteButton}
kind={kinds.DANGER} kind={kinds.DANGER}
title={isTagUsed ? 'Cannot be deleted while in use' : undefined}
isDisabled={isTagUsed} isDisabled={isTagUsed}
onPress={onDeleteTagPress} onPress={onDeleteTagPress}
> >

@ -1,11 +1,12 @@
.tag { .tag {
composes: card from '~Components/Card.css'; composes: card from '~Components/Card.css';
width: 150px; flex: 150px 0 1;
} }
.label { .label {
margin-bottom: 20px; margin-bottom: 20px;
white-space: nowrap;
font-weight: 300; font-weight: 300;
font-size: 24px; font-size: 24px;
} }

@ -176,7 +176,7 @@ export const toggleMovieMonitored = createThunk(TOGGLE_MOVIE_MONITORED);
export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => { export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
return { return {
section: 'movies', section,
...payload ...payload
}; };
}); });

@ -174,6 +174,7 @@ module.exports = {
calendarTodayBackgroundColor: '#ddd', calendarTodayBackgroundColor: '#ddd',
calendarBorderColor: '#cecece', calendarBorderColor: '#cecece',
calendarTextDim: '#666',
// //
// Table // Table

@ -26,6 +26,7 @@ function getInternalLink(source) {
/> />
); );
case 'DownloadClientCheck': case 'DownloadClientCheck':
case 'DownloadClientStatusCheck':
case 'ImportMechanismCheck': case 'ImportMechanismCheck':
return ( return (
<IconButton <IconButton
@ -67,6 +68,7 @@ function getTestLink(source, props) {
/> />
); );
case 'DownloadClientCheck': case 'DownloadClientCheck':
case 'DownloadClientStatusCheck':
return ( return (
<SpinnerIconButton <SpinnerIconButton
name={icons.TEST} name={icons.TEST}

@ -27,5 +27,5 @@
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 20px; width: 60px;
} }

@ -1,7 +0,0 @@
import MobileDetect from 'mobile-detect';
export default function isMobile() {
const mobileDetect = new MobileDetect(window.navigator.userAgent);
return mobileDetect.mobile() != null;
}

@ -0,0 +1,12 @@
import MobileDetect from 'mobile-detect';
const mobileDetect = new MobileDetect(window.navigator.userAgent);
export function isMobile() {
return mobileDetect.mobile() != null;
}
export function isIOS() {
return mobileDetect.is('iOS');
}

@ -0,0 +1,13 @@
// Allow iOS devices to disable scrolling of the body/virtual table
// when a modal is open. This will prevent focusing an input in a
// modal causing the modal to close due to scrolling.
let scrollLock = false;
export function isLocked() {
return scrollLock;
}
export function setScrollLock(locked) {
scrollLock = locked;
}

@ -28,7 +28,7 @@ namespace NzbDrone.Api.ManualImport
var downloadId = (string)downloadIdQuery.Value; var downloadId = (string)downloadIdQuery.Value;
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); return _manualImportService.GetMediaFiles(folder, downloadId, null, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
} }
private ManualImportResource AddQualityWeight(ManualImportResource item) private ManualImportResource AddQualityWeight(ManualImportResource item)

@ -46,7 +46,7 @@ namespace NzbDrone.Api.Qualities
CreateResource = Create; CreateResource = Create;
DeleteResource = Delete; DeleteResource = DeleteFormat;
Get["/test"] = x => Test(); Get["/test"] = x => Test();
@ -77,7 +77,7 @@ namespace NzbDrone.Api.Qualities
return _formatService.All().ToResource(); return _formatService.All().ToResource();
} }
private void Delete(int id) private void DeleteFormat(int id)
{ {
_formatService.Delete(id); _formatService.Delete(id);
} }

@ -31,7 +31,7 @@ namespace NzbDrone.Automation.Test
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", NLog.LogLevel.Trace, consoleTarget)); LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", NLog.LogLevel.Trace, consoleTarget));
} }
[TestFixtureSetUp] [OneTimeSetUp]
public void SmokeTestSetup() public void SmokeTestSetup()
{ {
driver = new FirefoxDriver(); driver = new FirefoxDriver();
@ -56,7 +56,7 @@ namespace NzbDrone.Automation.Test
.Select(e => e.Text); .Select(e => e.Text);
} }
[TestFixtureTearDown] [OneTimeTearDown]
public void SmokeTestTearDown() public void SmokeTestTearDown()
{ {
_runner.KillAll(); _runner.KillAll();

@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider
{ {
[Test] [Test]
[Retry(5)]
public void directory_exist_should_be_able_to_find_existing_folder() public void directory_exist_should_be_able_to_find_existing_folder()
{ {
Subject.FolderExists(TempFolder).Should().BeTrue(); Subject.FolderExists(TempFolder).Should().BeTrue();
@ -32,6 +33,7 @@ namespace NzbDrone.Common.Test.DiskTests
protected abstract void SetWritePermissions(string path, bool writable); protected abstract void SetWritePermissions(string path, bool writable);
[Test] [Test]
[Retry(5)]
public void FolderWritable_should_return_true_for_writable_directory() public void FolderWritable_should_return_true_for_writable_directory()
{ {
var tempFolder = GetTempFilePath(); var tempFolder = GetTempFilePath();
@ -62,6 +64,7 @@ namespace NzbDrone.Common.Test.DiskTests
} }
[Test] [Test]
[Retry(5)]
public void MoveFile_should_overwrite_existing_file() public void MoveFile_should_overwrite_existing_file()
{ {
var source1 = GetTempFilePath(); var source1 = GetTempFilePath();
@ -122,6 +125,7 @@ namespace NzbDrone.Common.Test.DiskTests
} }
[Test] [Test]
[Retry(5)]
public void empty_folder_should_return_folder_modified_date() public void empty_folder_should_return_folder_modified_date()
{ {
var tempfolder = new DirectoryInfo(TempFolder); var tempfolder = new DirectoryInfo(TempFolder);

@ -616,6 +616,7 @@ namespace NzbDrone.Common.Test.DiskTests
} }
[Test] [Test]
[Retry(5)]
public void CopyFolder_should_copy_folder() public void CopyFolder_should_copy_folder()
{ {
WithRealDiskProvider(); WithRealDiskProvider();

@ -1,9 +0,0 @@
namespace NzbDrone.Common.EnvironmentInfo
{
public interface IOperatingSystemVersionInfo
{
string Version { get; }
string Name { get; }
string FullName { get; }
}
}

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

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore.Converters
{ {
var i = 5; var i = 5;
Subject.ToDB(i).Should().Be(5); Subject.ToDB(i).Should().Be(i);
} }
[Test] [Test]

@ -83,5 +83,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format") FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
}, old, newQ).Should().BeFalse(); }, old, newQ).Should().BeFalse();
} }
[Test]
public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade()
{
Profile _profile = new Profile
{
Cutoff = Quality.HDTV1080p.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
};
Subject.CutoffNotMet(
_profile,
new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)),
new QualityModel(Quality.WEBDL1080p, new Revision(version: 2))).Should().BeTrue();
}
} }
} }

@ -279,16 +279,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
} }
[Test] [Test]
public void should_skip_deletestatus_copy() public void should_report_deletestatus_copy_as_failed()
{ {
_completed.DeleteStatus = "COPY"; _completed.DeleteStatus = "COPY";
GivenQueue(null); GivenQueue(null);
GivenHistory(_completed); GivenHistory(_completed);
var result = Subject.GetItems().SingleOrDefault(); var result = Subject.GetItems().Single();
result.Should().BeNull(); result.Status.Should().Be(DownloadItemStatus.Failed);
} }
[Test] [Test]

@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using NLog;
using NUnit.Framework;
namespace NzbDrone.Core.Test.Framework.AutoMoq
{
[TestFixture]
class TestBaseTests : TestBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
[Test]
public void Test_should_pass_when_no_exceptions_are_logged()
{
Logger.Info("Everything is fine and dandy!");
}
[Test]
public void Test_should_pass_when_errors_are_excpected()
{
Logger.Error("I knew this would happer");
ExceptionVerification.ExcpectedErrors(1);
}
[Test]
public void Test_should_pass_when_warns_are_excpected()
{
Logger.Warn("I knew this would happer");
ExceptionVerification.ExcpectedWarns(1);
}
[Test]
public void Test_should_pass_when_warns_are_ignored()
{
Logger.Warn("I knew this would happer");
Logger.Warn("I knew this would happer");
Logger.Warn("I knew this would happer");
ExceptionVerification.IgnoreWarns();
}
[Test]
public void Test_should_pass_when_errors_are_ignored()
{
Logger.Error("I knew this would happer");
Logger.Error("I knew this would happer");
Logger.Error("I knew this would happer");
ExceptionVerification.IgnoreErrors();
}
[Test]
public void Test_should_pass_when_exception_type_is_ignored()
{
Logger.ErrorException("bad exception", new WebException("Test"));
ExceptionVerification.MarkInconclusive(typeof(WebException));
}
}
}

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class FixFutureIndexerStatusTimesFixture : CoreTest<FixFutureIndexerStatusTimes>
{
[Test]
public void should_set_disabled_till_when_its_too_far_in_the_future()
{
var disabledTillTime = EscalationBackOff.Periods[1];
var indexerStatuses = Builder<IndexerStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IIndexerStatusRepository>()
.Setup(s => s.All())
.Returns(indexerStatuses);
Subject.Clean();
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<IndexerStatus>>(i => i.All(
s => s.DisabledTill.Value < DateTime.UtcNow.AddMinutes(disabledTillTime)))
)
);
}
[Test]
public void should_set_initial_failure_when_its_in_the_future()
{
var indexerStatuses = Builder<IndexerStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IIndexerStatusRepository>()
.Setup(s => s.All())
.Returns(indexerStatuses);
Subject.Clean();
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<IndexerStatus>>(i => i.All(
s => s.InitialFailure.Value <= DateTime.UtcNow))
)
);
}
[Test]
public void should_set_most_recent_failure_when_its_in_the_future()
{
var indexerStatuses = Builder<IndexerStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(5))
.With(t => t.EscalationLevel = 1)
.BuildListOfNew();
Mocker.GetMock<IIndexerStatusRepository>()
.Setup(s => s.All())
.Returns(indexerStatuses);
Subject.Clean();
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<IndexerStatus>>(i => i.All(
s => s.MostRecentFailure.Value <= DateTime.UtcNow))
)
);
}
[Test]
public void should_not_change_statuses_when_times_are_in_the_past()
{
var indexerStatuses = Builder<IndexerStatus>.CreateListOfSize(5)
.All()
.With(t => t.DisabledTill = DateTime.UtcNow.AddDays(-5))
.With(t => t.InitialFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.MostRecentFailure = DateTime.UtcNow.AddDays(-5))
.With(t => t.EscalationLevel = 0)
.BuildListOfNew();
Mocker.GetMock<IIndexerStatusRepository>()
.Setup(s => s.All())
.Returns(indexerStatuses);
Subject.Clean();
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<IndexerStatus>>(i => i.Count == 0)
)
);
}
}
}

@ -1,78 +0,0 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerSearchTests
{
public class NzbSearchServiceFixture : CoreTest<NzbSearchService>
{
private List<IIndexer> _indexers;
private Series _searchTargetSeries;
[SetUp]
public void Setup()
{
_searchTargetSeries = Builder<Series>.CreateNew().BuildNew();
_indexers = new List<IIndexer>();
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
Mocker.GetMock<ISeriesService>().Setup(c => c.GetSeries(It.IsAny<int>()))
.Returns(_searchTargetSeries);
}
[Test]
public void should_call_fetch_on_all_indexers_at_the_same_time()
{
var counter = new ConcurrencyCounter(_indexers.Count);
Mocker.GetMock<IFetchFeedFromIndexers>().Setup(c => c.Fetch(It.IsAny<IIndexer>(), It.IsAny<SingleEpisodeSearchDefinition>()))
.Returns(new List<ReportInfo>())
.Callback((() => counter.SimulateWork(500)));
Mocker.GetMock<IIndexerService>().Setup(c => c.GetAvailableIndexers()).Returns(_indexers);
Mocker.GetMock<IMakeDownloadDecision>()
.Setup(c => c.GetSearchDecision(It.IsAny<IEnumerable<ReportInfo>>(), It.IsAny<SearchDefinitionBase>()))
.Returns(new List<DownloadDecision>());
Subject.SearchSingle(0, 0, 0);
counter.WaitForAllItems();
counter.MaxThreads.Should().Be(_indexers.Count);
}
}
}

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests
{
public class FetchAndParseRssServiceFixture : CoreTest<FetchAndParseRssService>
{
private List<IIndexer> _indexers;
[SetUp]
public void Setup()
{
_indexers = new List<IIndexer>();
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
_indexers.Add(new Newznab());
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
}
[Test]
[Explicit]
public void should_call_fetch_on_all_indexers_at_the_same_time()
{
var counter = new ConcurrencyCounter(_indexers.Count);
Mocker.GetMock<IFetchFeedFromIndexers>().Setup(c => c.FetchRss(It.IsAny<IIndexer>()))
.Returns(new List<ReportInfo>())
.Callback((() => counter.SimulateWork(500)));
Mocker.GetMock<IIndexerService>().Setup(c => c.GetAvailableIndexers()).Returns(_indexers);
Subject.Fetch();
counter.WaitForAllItems();
counter.MaxThreads.Should().Be(_indexers.Count);
}
}
}

@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT"); torrentInfo.Title.Should().Be("Sense8.S01E01.WEBRip.x264-FGT");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce"); torrentInfo.DownloadUrl.Should().Be("magnet:?xt=urn:btih:d8bde635f573acb390c7d7e7efc1556965fdc802&dn=Sense8.S01E01.WEBRip.x264-FGT&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5"); torrentInfo.InfoUrl.Should().Be("https://torrentapi.org/redirect_to_info.php?token=i5cx7b9agd&p=8_6_4_4_5_6__d8bde635f5&app_id=Radarr");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name); torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime()); torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
torrentInfo.Size.Should().Be(564198371); torrentInfo.Size.Should().Be(564198371);

@ -0,0 +1,34 @@
using System;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests
{
[TestFixture]
public class SeedConfigProviderFixture : CoreTest<SeedConfigProvider>
{
[Test]
public void should_not_return_config_for_non_existent_indexer()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(It.IsAny<int>()))
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), 0));
var result = Subject.GetSeedConfiguration(new RemoteMovie
{
Release = new ReleaseInfo()
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 0
}
});
result.Should().BeNull();
}
}
}

@ -60,8 +60,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Specifications
} }
[Test] [Test]
public void should_reject_when_there_isnt_enough_space_for_file_plus_100mb_padding() public void should_reject_when_there_isnt_enough_space_for_file_plus_min_free_space()
{ {
Mocker.GetMock<IConfigService>()
.Setup(s => s.MinimumFreeSpaceWhenImporting)
.Returns(100);
GivenFileSize(100.Megabytes()); GivenFileSize(100.Megabytes());
GivenFreeSpace(150.Megabytes()); GivenFreeSpace(150.Megabytes());

@ -63,5 +63,16 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Specifications
Subject.IsSatisfiedBy(_localMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_localMovie, null).Accepted.Should().BeFalse();
} }
[Test]
public void should_be_accepted_if_file_cannot_be_fetched()
{
_localMovie.Movie = Builder<Movie>.CreateNew()
.With(e => e.MovieFileId = 1)
.With(e => e.MovieFile = new LazyLoaded<MovieFile>((MovieFile)null))
.Build();
Subject.IsSatisfiedBy(_localMovie, null).Accepted.Should().BeTrue();
}
} }
} }

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.UpgradeMovieFile(_movieFile, _localMovie); Subject.UpgradeMovieFile(_movieFile, _localMovie);
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(It.IsAny<string>()), Times.Once()); Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(It.IsAny<string>(), It.IsAny<string>()), Times.Once());
} }
@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.UpgradeMovieFile(_movieFile, _localMovie); Subject.UpgradeMovieFile(_movieFile, _localMovie);
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(It.IsAny<string>()), Times.Never()); Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
} }
[Test] [Test]

@ -1,63 +0,0 @@
using NUnit.Framework;
using NzbDrone.Core.Notifications.Growl;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.NotificationTests
{
[Explicit]
[TestFixture]
public class GrowlProviderTest : CoreTest
{
[Test]
public void Register_should_add_new_application_to_local_growl_instance()
{
Mocker.Resolve<GrowlProvider>().Register("localhost", 23053, "");
Mocker.VerifyAllMocks();
}
[Test]
public void TestNotification_should_send_a_message_to_local_growl_instance()
{
Mocker.Resolve<GrowlProvider>().TestNotification("localhost", 23053, "");
Mocker.VerifyAllMocks();
}
[Test]
public void OnGrab_should_send_a_message_to_local_growl_instance()
{
Mocker.Resolve<GrowlProvider>().SendNotification("Episode Grabbed", "Series Title - 1x05 - Episode Title", "GRAB", "localhost", 23053, "");
Mocker.VerifyAllMocks();
}
[Test]
public void OnDownload_should_send_a_message_to_local_growl_instance()
{
Mocker.Resolve<GrowlProvider>().SendNotification("Episode Downloaded", "Series Title - 1x05 - Episode Title", "DOWNLOAD", "localhost", 23053, "");
Mocker.VerifyAllMocks();
}
}
}

@ -256,6 +256,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupUnusedTagsFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupUnusedTagsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleasesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleasesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureIndexerStatusTimesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
<Compile Include="Http\HttpProxySettingsProviderFixture.cs" /> <Compile Include="Http\HttpProxySettingsProviderFixture.cs" />
<Compile Include="Http\TorCacheHttpRequestInterceptorFixture.cs" /> <Compile Include="Http\TorCacheHttpRequestInterceptorFixture.cs" />
@ -267,6 +268,7 @@
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" /> <Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\NewznabTests\NewznabCapabilitiesProviderFixture.cs" /> <Compile Include="IndexerTests\NewznabTests\NewznabCapabilitiesProviderFixture.cs" />
<Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" /> <Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" />
<Compile Include="IndexerTests\SeedConfigProviderFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" /> <Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.cs" /> <Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.cs" />
<Compile Include="IndexerTests\TorznabTests\TorznabFixture.cs" /> <Compile Include="IndexerTests\TorznabTests\TorznabFixture.cs" />

@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetDirectories(RecycleBin)) Mocker.GetMock<IDiskProvider>().Setup(s => s.GetDirectories(RecycleBin))
.Returns(new [] { @"C:\Test\RecycleBin\Folder1", @"C:\Test\RecycleBin\Folder2", @"C:\Test\RecycleBin\Folder3" }); .Returns(new [] { @"C:\Test\RecycleBin\Folder1", @"C:\Test\RecycleBin\Folder2", @"C:\Test\RecycleBin\Folder3" });
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(RecycleBin, SearchOption.TopDirectoryOnly)) Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(RecycleBin, SearchOption.AllDirectories))
.Returns(new [] { @"C:\Test\RecycleBin\File1.avi", @"C:\Test\RecycleBin\File2.mkv" }); .Returns(new [] { @"C:\Test\RecycleBin\File1.avi", @"C:\Test\RecycleBin\File2.mkv" });
} }
@ -56,12 +56,13 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
} }
[Test] [Test]
public void should_delete_all_expired_folders() public void should_return_if_recycleBinCleanupDays_is_zero()
{ {
WithExpired(); Mocker.GetMock<IConfigService>().SetupGet(s => s.RecycleBinCleanupDays).Returns(0);
Mocker.Resolve<RecycleBinProvider>().Cleanup(); Mocker.Resolve<RecycleBinProvider>().Cleanup();
Mocker.GetMock<IDiskProvider>().Verify(v => v.DeleteFolder(It.IsAny<string>(), true), Times.Exactly(3)); Mocker.GetMock<IDiskProvider>().Verify(v => v.GetDirectories(It.IsAny<string>()), Times.Never());
} }
[Test] [Test]

@ -7,7 +7,6 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;

@ -180,7 +180,7 @@ namespace NzbDrone.Core.Configuration
// TODO: Change back to "master" for the first stable release. // TODO: Change back to "master" for the first stable release.
public string Branch => GetValue("Branch", "develop").ToLowerInvariant(); public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "Info"); public string LogLevel => GetValue("LogLevel", "info");
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string SslCertHash => GetValue("SslCertHash", ""); public string SslCertHash => GetValue("SslCertHash", "");

@ -93,6 +93,12 @@ namespace NzbDrone.Core.Configuration
set { SetValue("RecycleBin", value); } set { SetValue("RecycleBin", value); }
} }
public int RecycleBinCleanupDays
{
get { return GetValueInt("RecycleBinCleanupDays", 7); }
set { SetValue("RecycleBinCleanupDays", value); }
}
public int RssSyncInterval public int RssSyncInterval
{ {
get { return GetValueInt("RssSyncInterval", 60); } get { return GetValueInt("RssSyncInterval", 60); }
@ -287,6 +293,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); } set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
} }
public int MinimumFreeSpaceWhenImporting
{
get { return GetValueInt("MinimumFreeSpaceWhenImporting", 100); }
set { SetValue("MinimumFreeSpaceWhenImporting", value); }
}
public bool CopyUsingHardlinks public bool CopyUsingHardlinks
{ {
get { return GetValueBoolean("CopyUsingHardlinks", true); } get { return GetValueBoolean("CopyUsingHardlinks", true); }

@ -27,11 +27,13 @@ namespace NzbDrone.Core.Configuration
//Media Management //Media Management
bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; } bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }
string RecycleBin { get; set; } string RecycleBin { get; set; }
int RecycleBinCleanupDays { get; set; }
bool AutoDownloadPropers { get; set; } bool AutoDownloadPropers { get; set; }
bool CreateEmptyMovieFolders { get; set; } bool CreateEmptyMovieFolders { get; set; }
bool DeleteEmptyFolders { get; set; } bool DeleteEmptyFolders { get; set; }
FileDateType FileDate { get; set; } FileDateType FileDate { get; set; }
bool SkipFreeSpaceCheckWhenImporting { get; set; } bool SkipFreeSpaceCheckWhenImporting { get; set; }
int MinimumFreeSpaceWhenImporting { get; set; }
bool CopyUsingHardlinks { get; set; } bool CopyUsingHardlinks { get; set; }
bool EnableMediaInfo { get; set; } bool EnableMediaInfo { get; set; }
bool ImportExtraFiles { get; set; } bool ImportExtraFiles { get; set; }

@ -39,7 +39,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
var cdhEnabled = _configService.EnableCompletedDownloadHandling; var cdhEnabled = _configService.EnableCompletedDownloadHandling;
_logger.Debug("Performing history status check on report"); _logger.Debug("Performing history status check on report");
_logger.Debug("Checking current status of movie [{0}] in history", subject.Movie.Id); _logger.Debug("Checking current status of movie [{0}] in history", subject.Movie.Id);
var mostRecent = _historyService.MostRecentForMovie(subject.Movie.Id); var mostRecent = _historyService.MostRecentForMovie(subject.Movie.Id);
@ -51,7 +51,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
if (!recent && cdhEnabled) if (!recent && cdhEnabled)
{ {
return Decision.Accept(); return Decision.Accept();
} }
if (!cutoffUnmet) if (!cutoffUnmet)

@ -48,9 +48,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null)
{ {
var comparer = new QualityModelComparer(profile); var comparer = new QualityModelComparer(profile);
var compare = comparer.Compare(currentQuality.Quality.Id, profile.Cutoff); var cutoffCompare = comparer.Compare(currentQuality.Quality.Id, profile.Cutoff);
if (compare < 0) if (cutoffCompare < 0)
{ {
return true; return true;
} }

@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
} }
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
_logger.ErrorException(ex.Message, ex); _logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>(); return Enumerable.Empty<DownloadClientItem>();
} }
@ -172,7 +172,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
} }
catch (DownloadClientAuthenticationException ex) catch (DownloadClientAuthenticationException ex)
{ {
_logger.ErrorException(ex.Message, ex); _logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Password", "Authentication failed"); return new NzbDroneValidationFailure("Password", "Authentication failed");
} }
@ -188,7 +188,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.ErrorException(ex.Message, ex); _logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message); return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message);
} }

@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.ErrorException("Failed to map Hadouken torrent data.", ex); _logger.Error(ex, "Failed to map Hadouken torrent data.");
} }
return torrent; return torrent;

@ -135,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
historyItem.CanMoveFiles = true; historyItem.CanMoveFiles = true;
historyItem.CanBeRemoved = true; historyItem.CanBeRemoved = true;
if (item.DeleteStatus == "MANUAL" || item.DeleteStatus == "COPY") if (item.DeleteStatus == "MANUAL")
{ {
continue; continue;
} }

@ -53,18 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
private IEnumerable<DownloadClientItem> GetQueue() private IEnumerable<DownloadClientItem> GetQueue()
{ {
SabnzbdQueue sabQueue; var sabQueue = _proxy.GetQueue(0, 0, Settings);
try
{
sabQueue = _proxy.GetQueue(0, 0, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn(ex, "Couldn't get download queue. {0}", ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>(); var queueItems = new List<DownloadClientItem>();
foreach (var sabQueueItem in sabQueue.Items) foreach (var sabQueueItem in sabQueue.Items)

@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
torrentInfo.Title = torrent.title; torrentInfo.Title = torrent.title;
torrentInfo.Size = torrent.size; torrentInfo.Size = torrent.size;
torrentInfo.DownloadUrl = torrent.download; torrentInfo.DownloadUrl = torrent.download;
torrentInfo.InfoUrl = torrent.info_page; torrentInfo.InfoUrl = torrent.info_page + "&app_id=Radarr"; ;
torrentInfo.PublishDate = torrent.pubdate.ToUniversalTime(); torrentInfo.PublishDate = torrent.pubdate.ToUniversalTime();
torrentInfo.Seeders = torrent.seeders; torrentInfo.Seeders = torrent.seeders;
torrentInfo.Peers = torrent.leechers + torrent.seeders; torrentInfo.Peers = torrent.leechers + torrent.seeders;

@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{ {
public interface IManualImportService public interface IManualImportService
{ {
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? movieId, bool filterExistingFiles);
} }
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -71,7 +71,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
_logger = logger; _logger = logger;
} }
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles) public List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? movieId, bool filterExistingFiles)
{ {
if (downloadId.IsNotNullOrWhiteSpace()) if (downloadId.IsNotNullOrWhiteSpace())
{ {
@ -96,14 +96,17 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
return new List<ManualImportItem> { ProcessFile(rootFolder, rootFolder, path, downloadId) }; return new List<ManualImportItem> { ProcessFile(rootFolder, rootFolder, path, downloadId) };
} }
return ProcessFolder(path, path, downloadId, filterExistingFiles); return ProcessFolder(path, path, downloadId, movieId, filterExistingFiles);
} }
private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolder, string downloadId, bool filterExistingFiles) private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolder, string downloadId, int? movieId, bool filterExistingFiles)
{ {
DownloadClientItem downloadClientItem = null; DownloadClientItem downloadClientItem = null;
var directoryInfo = new DirectoryInfo(baseFolder); var directoryInfo = new DirectoryInfo(baseFolder);
var movie = _parsingService.GetMovie(directoryInfo.Name);
var movie = movieId.HasValue ?
_movieService.GetMovie(movieId.Value) :
_parsingService.GetMovie(directoryInfo.Name);
if (downloadId.IsNotNullOrWhiteSpace()) if (downloadId.IsNotNullOrWhiteSpace())
{ {
@ -116,20 +119,13 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
} }
} }
// Try a lookup by the path if the movie is still unknown, this will handle
// the case where the movie folder doesn't match the movie title.
if (movie == null)
{
movie = _movieService.FindByPath(rootFolder);
}
if (movie == null) if (movie == null)
{ {
var files = _diskScanService.FilterFiles(baseFolder, _diskScanService.GetVideoFiles(baseFolder, false)); var files = _diskScanService.FilterFiles(baseFolder, _diskScanService.GetVideoFiles(baseFolder, false));
var subfolders = _diskScanService.FilterFiles(baseFolder, _diskProvider.GetDirectories(baseFolder)); var subfolders = _diskScanService.FilterFiles(baseFolder, _diskProvider.GetDirectories(baseFolder));
var processedFiles = files.Select(file => ProcessFile(rootFolder, baseFolder, file, downloadId)); var processedFiles = files.Select(file => ProcessFile(rootFolder, baseFolder, file, downloadId));
var processedFolders = subfolders.SelectMany(subfolder => ProcessFolder(rootFolder, subfolder, downloadId, filterExistingFiles)); var processedFolders = subfolders.SelectMany(subfolder => ProcessFolder(rootFolder, subfolder, downloadId, null, filterExistingFiles));
return processedFiles.Concat(processedFolders).Where(i => i != null).ToList(); return processedFiles.Concat(processedFolders).Where(i => i != null).ToList();
} }
@ -159,11 +155,11 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
if (downloadId.IsNotNullOrWhiteSpace()) if (downloadId.IsNotNullOrWhiteSpace())
{ {
var trackedDownload = _trackedDownloadService.Find(downloadId); var trackedDownload = _trackedDownloadService.Find(downloadId);
downloadClientItem = trackedDownload.DownloadItem; downloadClientItem = trackedDownload?.DownloadItem;
if (movie == null) if (movie == null)
{ {
movie = trackedDownload.RemoteMovie.Movie; movie = trackedDownload?.RemoteMovie?.Movie;
} }
} }

@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
return Decision.Accept(); return Decision.Accept();
} }
if (freeSpace < localMovie.Size + 100.Megabytes()) if (freeSpace < localMovie.Size + _configService.MinimumFreeSpaceWhenImporting.Megabytes())
{ {
_logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size); _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size);
return Decision.Reject("Not enough free space"); return Decision.Reject("Not enough free space");

@ -19,12 +19,20 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{ {
var movieFile = localMovie.Movie.MovieFile; var movieFile = localMovie.Movie.MovieFile;
if (localMovie.Movie.MovieFileId == 0 || movieFile == null) if (localMovie.Movie.MovieFileId == 0)
{ {
_logger.Debug("No existing movie file, skipping"); _logger.Debug("No existing movie file, skipping");
return Decision.Accept(); return Decision.Accept();
} }
if (movieFile == null)
{
var movie = localMovie.Movie;
_logger.Trace("Unable to get movie file details from the DB. MovieId: {0} MovieFileId: {1}", movie.Id, movie.MovieFileId);
return Decision.Accept();
}
if (movieFile.Size == localMovie.Size) if (movieFile.Size == localMovie.Size)
{ {
_logger.Debug("'{0}' Has the same filesize as existing file", localMovie.Path); _logger.Debug("'{0}' Has the same filesize as existing file", localMovie.Path);

@ -15,7 +15,7 @@ namespace NzbDrone.Core.MediaFiles
public interface IRecycleBinProvider public interface IRecycleBinProvider
{ {
void DeleteFolder(string path); void DeleteFolder(string path);
void DeleteFile(string path); void DeleteFile(string path, string subfolder = "");
void Empty(); void Empty();
void Cleanup(); void Cleanup();
} }
@ -62,18 +62,14 @@ namespace NzbDrone.Core.MediaFiles
_diskProvider.FolderSetLastWriteTime(destination, DateTime.UtcNow); _diskProvider.FolderSetLastWriteTime(destination, DateTime.UtcNow);
foreach (var file in _diskProvider.GetFiles(destination, SearchOption.AllDirectories)) foreach (var file in _diskProvider.GetFiles(destination, SearchOption.AllDirectories))
{ {
if (OsInfo.IsWindows) SetLastWriteTime(file, DateTime.UtcNow);
{
//TODO: Better fix than this for non-Windows?
_diskProvider.FileSetLastWriteTime(file, DateTime.UtcNow);
}
} }
_logger.Debug("Folder has been moved to the recycling bin: {0}", destination); _logger.Debug("Folder has been moved to the recycling bin: {0}", destination);
} }
} }
public void DeleteFile(string path) public void DeleteFile(string path, string subfolder = "")
{ {
_logger.Debug("Attempting to send '{0}' to recycling bin", path); _logger.Debug("Attempting to send '{0}' to recycling bin", path);
var recyclingBin = _configService.RecycleBin; var recyclingBin = _configService.RecycleBin;
@ -94,7 +90,10 @@ namespace NzbDrone.Core.MediaFiles
else else
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
var destination = Path.Combine(recyclingBin, fileInfo.Name); var destinationFolder = Path.Combine(recyclingBin, subfolder);
var destination = Path.Combine(destinationFolder, fileInfo.Name);
_diskProvider.CreateFolder(destinationFolder);
var index = 1; var index = 1;
while (_diskProvider.FileExists(destination)) while (_diskProvider.FileExists(destination))
@ -102,11 +101,11 @@ namespace NzbDrone.Core.MediaFiles
index++; index++;
if (fileInfo.Extension.IsNullOrWhiteSpace()) if (fileInfo.Extension.IsNullOrWhiteSpace())
{ {
destination = Path.Combine(recyclingBin, fileInfo.Name + "_" + index); destination = Path.Combine(destinationFolder, fileInfo.Name + "_" + index);
} }
else else
{ {
destination = Path.Combine(recyclingBin, Path.GetFileNameWithoutExtension(fileInfo.Name) + "_" + index + fileInfo.Extension); destination = Path.Combine(destinationFolder, Path.GetFileNameWithoutExtension(fileInfo.Name) + "_" + index + fileInfo.Extension);
} }
} }
@ -117,16 +116,11 @@ namespace NzbDrone.Core.MediaFiles
} }
catch (IOException e) catch (IOException e)
{ {
var message = string.Format("Unable to move '{0}' to the recycling bin: '{1}'", path, destination); _logger.Error(e, "Unable to move '{0}' to the recycling bin: '{1}'", path, destination);
_logger.Error(e, message);
throw; throw;
} }
//TODO: Better fix than this for non-Windows? SetLastWriteTime(destination, DateTime.UtcNow);
if (OsInfo.IsWindows)
{
_diskProvider.FileSetLastWriteTime(destination, DateTime.UtcNow);
}
_logger.Debug("File has been moved to the recycling bin: {0}", destination); _logger.Debug("File has been moved to the recycling bin: {0}", destination);
} }
@ -163,22 +157,19 @@ namespace NzbDrone.Core.MediaFiles
return; return;
} }
_logger.Info("Removing items older than 7 days from the recycling bin"); var cleanupDays = _configService.RecycleBinCleanupDays;
foreach (var folder in _diskProvider.GetDirectories(_configService.RecycleBin)) if (cleanupDays == 0)
{ {
if (_diskProvider.FolderGetLastWrite(folder).AddDays(7) > DateTime.UtcNow) _logger.Info("Automatic cleanup of Recycle Bin is disabled");
{ return;
_logger.Debug("Folder hasn't expired yet, skipping: {0}", folder);
continue;
}
_diskProvider.DeleteFolder(folder, true);
} }
foreach (var file in _diskProvider.GetFiles(_configService.RecycleBin, SearchOption.TopDirectoryOnly)) _logger.Info("Removing items older than {0} days from the recycling bin", cleanupDays);
foreach (var file in _diskProvider.GetFiles(_configService.RecycleBin, SearchOption.AllDirectories))
{ {
if (_diskProvider.FileGetLastWrite(file).AddDays(7) > DateTime.UtcNow) if (_diskProvider.FileGetLastWrite(file).AddDays(cleanupDays) > DateTime.UtcNow)
{ {
_logger.Debug("File hasn't expired yet, skipping: {0}", file); _logger.Debug("File hasn't expired yet, skipping: {0}", file);
continue; continue;
@ -187,9 +178,26 @@ namespace NzbDrone.Core.MediaFiles
_diskProvider.DeleteFile(file); _diskProvider.DeleteFile(file);
} }
_diskProvider.RemoveEmptySubfolders(_configService.RecycleBin);
_logger.Debug("Recycling Bin has been cleaned up."); _logger.Debug("Recycling Bin has been cleaned up.");
} }
private void SetLastWriteTime(string file, DateTime dateTime)
{
// Swallow any IOException that may be thrown due to "Invalid parameter"
try
{
_diskProvider.FileSetLastWriteTime(file, dateTime);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
public void HandleAsync(MovieDeletedEvent message) public void HandleAsync(MovieDeletedEvent message)
{ {
if (message.DeleteFiles) if (message.DeleteFiles)

@ -53,7 +53,7 @@ namespace NzbDrone.Core.Messaging.Commands
_logger.Error(ex, "Thread aborted: " + ex.Message); _logger.Error(ex, "Thread aborted: " + ex.Message);
Thread.ResetAbort(); Thread.ResetAbort();
} }
catch (OperationCanceledException ex) catch (OperationCanceledException)
{ {
_logger.Trace("Stopped one command execution pipeline"); _logger.Trace("Stopped one command execution pipeline");
} }

@ -385,7 +385,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
return new List<Movie> { GetMovieInfo(parserResult.ImdbId) }; return new List<Movie> { GetMovieInfo(parserResult.ImdbId) };
} }
catch (Exception e) catch (Exception)
{ {
return new List<Movie>(); return new List<Movie>();
} }
@ -538,7 +538,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster); var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster);
imdbMovie.Images.Add(imdbPoster); imdbMovie.Images.Add(imdbPoster);
} }
catch (Exception e) catch (Exception)
{ {
_logger.Debug(result); _logger.Debug(result);
} }

@ -136,7 +136,7 @@ namespace NzbDrone.Core.Movies
movie.SecondaryYearSourceId = 0; movie.SecondaryYearSourceId = 0;
} }
} }
catch (RadarrAPIException ex) catch (RadarrAPIException)
{ {
//Not that wild, could just be a 404. //Not that wild, could just be a 404.
} }

@ -68,7 +68,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
{ {
throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv"); throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv");
} }
catch (WebException ex) catch (WebException)
{ {
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv"); throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
} }

@ -34,7 +34,7 @@ namespace NzbDrone.Core.Parser.Augmenters
try { try {
indexerSettings = _indexerFactory.Get(releaseInfo.IndexerId)?.Settings as IIndexerSettings; indexerSettings = _indexerFactory.Get(releaseInfo.IndexerId)?.Settings as IIndexerSettings;
} }
catch (Exception e) catch (Exception)
{ {
//_logger.Debug("Indexer with id {0} does not exist, skipping minimum seeder checks.", subject.Release.IndexerId); //_logger.Debug("Indexer with id {0} does not exist, skipping minimum seeder checks.", subject.Release.IndexerId);
} // First, let's augment the language! } // First, let's augment the language!

@ -161,7 +161,7 @@ namespace NzbDrone.Core.Parser
Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName); Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName);
#endif #endif
} }
catch (Exception ex) catch (Exception)
{ {
#if !LIBRARY #if !LIBRARY
Logger.Debug("Failed parsing langauge from subtitle file: {0}", fileName); Logger.Debug("Failed parsing langauge from subtitle file: {0}", fileName);

@ -113,7 +113,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])", private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost|Rakuv[a-z]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n))+$", private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n))+$",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$", private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$",
@ -135,7 +135,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled); private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new Regex(@"\[.+?\]", RegexOptions.Compiled); private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
private static readonly Regex ReportYearRegex = new Regex(@"^.*(?<year>(19|20)\d{2}).*$", RegexOptions.Compiled); private static readonly Regex ReportYearRegex = new Regex(@"^.*(?<year>(19|20)\d{2}).*$", RegexOptions.Compiled);

@ -17,7 +17,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SourceRegex = new Regex(@"\b(?: private static readonly Regex SourceRegex = new Regex(@"\b(?:
(?<bluray>M?BluRay|Blu-Ray|HDDVD|BD(?!$)|BDISO|BD25|BD50|BR.?DISK)| (?<bluray>M?BluRay|Blu-Ray|HDDVD|BD(?!$)|BDISO|BD25|BD50|BR.?DISK)|
(?<webdl>WEB[-_. ]DL|HDRIP|WEBDL|WebRip|Web-Rip|iTunesHD|WebHD|WEBMux|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[-. ]WEB[-. ]|WEB-DLMux|\b\s\/\sWEB\s\/\s\b)| (?<webdl>WEB[-_. ]DL|HDRIP|WEBDL|WebRip|Web-Rip|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|WEBMux|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[-. ]WEB[-. ]|WEB-DLMux|\b\s\/\sWEB\s\/\s\b)|
(?<hdtv>HDTV)| (?<hdtv>HDTV)|
(?<bdrip>BDRip)|(?<brrip>BRRip)| (?<bdrip>BDRip)|(?<brrip>BRRip)|
(?<dvdr>DVD-R|DVDR)| (?<dvdr>DVD-R|DVDR)|

@ -2,6 +2,8 @@ using System.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@ -71,11 +73,9 @@ namespace NzbDrone.Core.RootFolders
{ {
try try
{ {
if (folder.Path.IsPathValid() && _diskProvider.FolderExists(folder.Path)) if (folder.Path.IsPathValid())
{ {
folder.FreeSpace = _diskProvider.GetAvailableSpace(folder.Path); GetDetails(folder);
folder.TotalSpace = _diskProvider.GetTotalSize(folder.Path);
folder.UnmappedFolders = GetUnmappedFolders(folder.Path);
} }
} }
//We don't want an exception to prevent the root folders from loading in the UI, so they can still be deleted //We don't want an exception to prevent the root folders from loading in the UI, so they can still be deleted
@ -115,9 +115,7 @@ namespace NzbDrone.Core.RootFolders
_rootFolderRepository.Insert(rootFolder); _rootFolderRepository.Insert(rootFolder);
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path); GetDetails(rootFolder);
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
return rootFolder; return rootFolder;
} }
@ -168,9 +166,8 @@ namespace NzbDrone.Core.RootFolders
public RootFolder Get(int id) public RootFolder Get(int id)
{ {
var rootFolder = _rootFolderRepository.Get(id); var rootFolder = _rootFolderRepository.Get(id);
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path); GetDetails(rootFolder);
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
return rootFolder; return rootFolder;
} }
@ -187,5 +184,19 @@ namespace NzbDrone.Core.RootFolders
return possibleRootFolder.Path; return possibleRootFolder.Path;
} }
private void GetDetails(RootFolder rootFolder)
{
Task.Run(() =>
{
if (_diskProvider.FolderExists(rootFolder.Path))
{
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path);
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
}
})
.Wait(5000);
}
} }
} }

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation;
using FluentValidation.Results;
namespace NzbDrone.Core.Validation
{
public abstract class NzbDroneValidator<T> : AbstractValidator<T>
{
public override ValidationResult Validate(T instance)
{
return new NzbDroneValidationResult(base.Validate(instance).Errors);
}
}
}

@ -36,7 +36,7 @@ namespace Radarr.Host.Owin.MiddleWare
context.Response.Headers.Add(_versionHeader); context.Response.Headers.Add(_versionHeader);
await Next.Invoke(context); await Next.Invoke(context);
} }
catch (Exception ex) catch (Exception)
{ {
Logger.Debug("Unable to set version header"); Logger.Debug("Unable to set version header");
} }

@ -9,9 +9,11 @@ namespace Radarr.Api.V2.Config
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator) public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator)
: base(configService) : base(configService)
{ {
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.FileChmod).NotEmpty(); SharedValidator.RuleFor(c => c.FileChmod).NotEmpty();
SharedValidator.RuleFor(c => c.FolderChmod).NotEmpty(); SharedValidator.RuleFor(c => c.FolderChmod).NotEmpty();
SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin)); SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin));
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
} }
protected override MediaManagementConfigResource ToResource(IConfigService model) protected override MediaManagementConfigResource ToResource(IConfigService model)

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

Loading…
Cancel
Save