mirror of
https://github.com/Radarr/Radarr.git
synced 2025-04-24 06:27:08 -04:00
New: Upstream Updates
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
bfc467dd96
commit
23670bca12
109 changed files with 1060 additions and 712 deletions
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
@ -120,8 +121,13 @@ class AddNewMovie extends Component {
|
|||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Failed to load search results, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<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:
|
||||
<ul>
|
||||
<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 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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.seriesTitle,
|
||||
.episodeTitle {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -37,6 +40,10 @@
|
|||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.airTime {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
.seriesTitle {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
color: #3a3f51;
|
||||
|
@ -23,10 +22,12 @@
|
|||
|
||||
.airTime {
|
||||
flex: 1 0 1px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
margin-left: 10px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.absoluteEpisodeNumber {
|
||||
|
@ -80,3 +81,7 @@
|
|||
.premiere {
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unaired {
|
||||
composes: unaired from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
|
|
@ -144,6 +144,7 @@ class FileBrowserModalContent extends Component {
|
|||
<Scroller
|
||||
ref={this.setScrollerRef}
|
||||
className={styles.scroller}
|
||||
scrollDirection={scrollDirections.BOTH}
|
||||
>
|
||||
{
|
||||
!!error &&
|
||||
|
@ -152,7 +153,10 @@ class FileBrowserModalContent extends Component {
|
|||
|
||||
{
|
||||
isPopulated && !error &&
|
||||
<Table columns={columns}>
|
||||
<Table
|
||||
horizontalScroll={false}
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
emptyParent &&
|
||||
|
|
|
@ -132,6 +132,7 @@ class FilterBuilderModalContent extends Component {
|
|||
filterBuilderProps,
|
||||
isSaving,
|
||||
saveError,
|
||||
onCancelPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
@ -190,7 +191,7 @@ class FilterBuilderModalContent extends Component {
|
|||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
<Button onPress={onCancelPress}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
|
@ -220,6 +221,7 @@ FilterBuilderModalContent.propTypes = {
|
|||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onCancelPress: 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 = () => {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
|
@ -67,6 +78,7 @@ class FilterModal extends Component {
|
|||
<FilterBuilderModalContentConnector
|
||||
{...otherProps}
|
||||
id={id}
|
||||
onCancelPress={this.onCancelPress}
|
||||
onModalClose={this.onModalClose}
|
||||
/> :
|
||||
<CustomFiltersModalContentConnector
|
||||
|
|
|
@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
|||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
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 { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
|
|
|
@ -18,10 +18,19 @@ class PathInput extends Component {
|
|||
this._node = document.getElementById('portal-root');
|
||||
|
||||
this.state = {
|
||||
value: props.value,
|
||||
isFileBrowserModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { value } = this.props;
|
||||
|
||||
if (prevProps.value !== value) {
|
||||
this.setState({ value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
@ -51,11 +60,8 @@ class PathInput extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (event, { newValue }) => {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
onInputChange = ({ value }) => {
|
||||
this.setState({ value });
|
||||
}
|
||||
|
||||
onInputKeyDown = (event) => {
|
||||
|
@ -77,6 +83,11 @@ class PathInput extends Component {
|
|||
}
|
||||
|
||||
onInputBlur = () => {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: this.state.value
|
||||
});
|
||||
|
||||
this.props.onClearPaths();
|
||||
}
|
||||
|
||||
|
@ -108,13 +119,18 @@ class PathInput extends Component {
|
|||
const {
|
||||
className,
|
||||
name,
|
||||
value,
|
||||
paths,
|
||||
includeFiles,
|
||||
hasFileBrowser,
|
||||
onChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
value,
|
||||
isFileBrowserModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<AutoSuggestInput
|
||||
|
@ -130,7 +146,7 @@ class PathInput extends Component {
|
|||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onChange={onChange}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
|
||||
{
|
||||
|
@ -144,7 +160,7 @@ class PathInput extends Component {
|
|||
</FormInputButton>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isFileBrowserModalOpen}
|
||||
isOpen={isFileBrowserModalOpen}
|
||||
name={name}
|
||||
value={value}
|
||||
includeFiles={includeFiles}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-top: -6px;
|
||||
margin-left: 3px;
|
||||
min-width: 20%;
|
||||
max-width: 100%;
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
bottom: -1px;
|
||||
left: -1px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 16px;
|
||||
height: 33px;
|
||||
padding: 1px 16px;
|
||||
min-height: 33px;
|
||||
cursor: default;
|
||||
}
|
||||
|
|
5
frontend/src/Components/Form/TagInputTag.css
Normal file
5
frontend/src/Components/Form/TagInputTag.css
Normal file
|
@ -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 Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './TagInputTag.css';
|
||||
|
||||
class TagInputTag extends Component {
|
||||
|
||||
|
@ -31,9 +32,9 @@ class TagInputTag extends Component {
|
|||
tag,
|
||||
kind
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.tag}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
|
|
|
@ -39,6 +39,7 @@ class Menu extends Component {
|
|||
|
||||
this._scheduleUpdate = null;
|
||||
this._menuButtonId = getUniqueElememtId();
|
||||
this._menuContentId = getUniqueElememtId();
|
||||
|
||||
this.state = {
|
||||
isMenuOpen: false,
|
||||
|
@ -99,12 +100,14 @@ class Menu extends Component {
|
|||
window.addEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('scroll', this.onWindowScroll, { capture: true });
|
||||
window.addEventListener('click', this.onWindowClick);
|
||||
window.addEventListener('touchstart', this.onTouchStart);
|
||||
}
|
||||
|
||||
_removeListener() {
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.removeEventListener('scroll', this.onWindowScroll, { capture: true });
|
||||
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 = () => {
|
||||
this.setMaxHeight();
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ class MenuContent extends Component {
|
|||
const {
|
||||
forwardedRef,
|
||||
className,
|
||||
id,
|
||||
children,
|
||||
style,
|
||||
isOpen
|
||||
|
@ -19,6 +20,7 @@ class MenuContent extends Component {
|
|||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={forwardedRef}
|
||||
className={className}
|
||||
style={style}
|
||||
|
@ -38,6 +40,7 @@ class MenuContent extends Component {
|
|||
MenuContent.propTypes = {
|
||||
forwardedRef: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
isOpen: PropTypes.bool
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.menuItem {
|
||||
@add-mixin truncate;
|
||||
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
padding: 10px 20px;
|
||||
|
@ -17,3 +16,8 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: $disabledColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './MenuItem.css';
|
||||
|
||||
|
@ -12,12 +13,17 @@ class MenuItem extends Component {
|
|||
const {
|
||||
className,
|
||||
children,
|
||||
isDisabled,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={className}
|
||||
className={classNames(
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
@ -28,11 +34,13 @@ class MenuItem extends Component {
|
|||
|
||||
MenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
MenuItem.defaultProps = {
|
||||
className: styles.menuItem
|
||||
className: styles.menuItem,
|
||||
isDisabled: false
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
|
|
|
@ -29,6 +29,12 @@
|
|||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.modalOpenIOS {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sizes
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,8 @@ import ReactDOM from 'react-dom';
|
|||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isIOS } from 'Utilities/mobile';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
|
@ -31,6 +33,7 @@ class Modal extends Component {
|
|||
this._node = document.getElementById('portal-root');
|
||||
this._backgroundRef = null;
|
||||
this._modalId = getUniqueElememtId();
|
||||
this._bodyScrollTop = 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -69,7 +72,14 @@ class Modal extends Component {
|
|||
window.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
innerClassName: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL])
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all)
|
||||
};
|
||||
|
||||
ModalBody.defaultProps = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
|
@ -7,6 +8,17 @@ import styles from './PageContentBody.css';
|
|||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScroll = (props) => {
|
||||
const { onScroll } = this.props;
|
||||
|
||||
if (this.props.onScroll && !isLocked()) {
|
||||
onScroll(props);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -27,6 +39,7 @@ class PageContentBody extends Component {
|
|||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
|
@ -41,6 +54,7 @@ PageContentBody.propTypes = {
|
|||
innerClassName: PropTypes.string,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@add-mixin scrollbar;
|
||||
@add-mixin scrollbarTrack;
|
||||
@add-mixin scrollbarThumb;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.none {
|
||||
|
@ -26,3 +27,11 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.both {
|
||||
overflow: scroll;
|
||||
|
||||
&.autoScroll {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class Scroller extends Component {
|
|||
|
||||
Scroller.propTypes = {
|
||||
className: PropTypes.string,
|
||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired,
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
|
|
|
@ -9,6 +9,7 @@ import titleCase from 'Utilities/String/titleCase';
|
|||
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
|
@ -72,6 +73,7 @@ const mapDispatchToProps = {
|
|||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchFetchMovies: fetchMovies,
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails
|
||||
};
|
||||
|
@ -258,6 +260,7 @@ class SignalRConnector extends Component {
|
|||
|
||||
const {
|
||||
dispatchFetchCommands,
|
||||
dispatchFetchMovies,
|
||||
dispatchSetAppValue
|
||||
} = this.props;
|
||||
|
||||
|
@ -265,6 +268,7 @@ class SignalRConnector extends Component {
|
|||
// are in sync after reconnecting.
|
||||
|
||||
if (this.props.isReconnecting || this.props.isDisconnected) {
|
||||
dispatchFetchMovies();
|
||||
dispatchFetchCommands();
|
||||
repopulatePage();
|
||||
}
|
||||
|
@ -346,6 +350,7 @@ SignalRConnector.propTypes = {
|
|||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchFetchMovies: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
.tableContainer {
|
||||
overflow-x: auto;
|
||||
&.horizontalScroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
|
@ -10,7 +12,12 @@
|
|||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.tableContainer {
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
&.horizontalScroll {
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
|
@ -28,6 +29,7 @@ function getTableHeaderCellProps(props) {
|
|||
function Table(props) {
|
||||
const {
|
||||
className,
|
||||
horizontalScroll,
|
||||
selectAll,
|
||||
columns,
|
||||
optionsComponent,
|
||||
|
@ -41,14 +43,22 @@ function Table(props) {
|
|||
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.tableContainer}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
className={classNames(
|
||||
styles.tableContainer,
|
||||
horizontalScroll && styles.horizontalScroll
|
||||
)}
|
||||
scrollDirection={
|
||||
horizontalScroll ?
|
||||
scrollDirections.HORIZONTAL :
|
||||
scrollDirections.NONE
|
||||
}
|
||||
>
|
||||
<table className={className}>
|
||||
<TableHeader>
|
||||
{
|
||||
selectAll &&
|
||||
<TableSelectAllHeaderCell {...otherProps} />
|
||||
selectAll ?
|
||||
<TableSelectAllHeaderCell {...otherProps} /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -111,6 +121,7 @@ function Table(props) {
|
|||
|
||||
Table.propTypes = {
|
||||
className: PropTypes.string,
|
||||
horizontalScroll: PropTypes.bool.isRequired,
|
||||
selectAll: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
optionsComponent: PropTypes.elementType,
|
||||
|
@ -123,6 +134,7 @@ Table.propTypes = {
|
|||
|
||||
Table.defaultProps = {
|
||||
className: styles.table,
|
||||
horizontalScroll: true,
|
||||
selectAll: false
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { WindowScroller } from 'react-virtualized';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
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
|
||||
|
||||
|
@ -107,7 +118,7 @@ class VirtualTable extends Component {
|
|||
<Measure onMeasure={this.onMeasure}>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
|
||||
onScroll={onScroll}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{({ height, isScrolling }) => {
|
||||
return (
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import isMobileUtil from 'Utilities/isMobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Portal from 'Components/Portal';
|
||||
import styles from './Tooltip.css';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const NONE = 'none';
|
||||
export const BOTH = 'both';
|
||||
export const HORIZONTAL = 'horizontal';
|
||||
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 selectAll from 'Utilities/Table/selectAll';
|
||||
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 Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
@ -73,7 +73,7 @@ const filterExistingFilesOptions = {
|
|||
|
||||
const importModeOptions = [
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Copy Files' }
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
||||
];
|
||||
|
||||
const SELECT = 'select';
|
||||
|
@ -217,7 +217,7 @@ class InteractiveImportModalContent extends Component {
|
|||
Manual Import - {title || folder}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
{
|
||||
showFilterExistingFiles &&
|
||||
<div className={styles.filterContainer}>
|
||||
|
@ -270,6 +270,7 @@ class InteractiveImportModalContent extends Component {
|
|||
isPopulated && !!items.length && !isFetching && !isFetching &&
|
||||
<Table
|
||||
columns={columns}
|
||||
horizontalScroll={false}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
|
|
|
@ -43,6 +43,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
componentDidMount() {
|
||||
const {
|
||||
downloadId,
|
||||
movieId,
|
||||
folder
|
||||
} = this.props;
|
||||
|
||||
|
@ -52,6 +53,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
movieId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
|
@ -65,11 +67,13 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
if (prevState.filterExistingFiles !== filterExistingFiles) {
|
||||
const {
|
||||
downloadId,
|
||||
movieId,
|
||||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
movieId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
|
@ -172,6 +176,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
|
||||
InteractiveImportModalContentConnector.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
movieId: PropTypes.number,
|
||||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.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,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
@ -20,3 +32,9 @@
|
|||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.peers {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 75px;
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ class InteractiveSearchRow extends Component {
|
|||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
|
@ -143,7 +143,7 @@ class InteractiveSearchRow extends Component {
|
|||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TableRowCell className={styles.indexer}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
|
||||
|
@ -151,7 +151,7 @@ class InteractiveSearchRow extends Component {
|
|||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TableRowCell className={styles.peers}>
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
|
|
|
@ -22,6 +22,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
|
@ -35,7 +36,6 @@ import MovieDetailsLinks from './MovieDetailsLinks';
|
|||
import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable';
|
||||
// import MovieTagsConnector from './MovieTagsConnector';
|
||||
import styles from './MovieDetails.css';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
|
@ -528,6 +528,7 @@ class MovieDetails extends Component {
|
|||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
movieId={id}
|
||||
folder={path}
|
||||
allowMovieChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
}
|
||||
|
||||
display: block;
|
||||
.unavailablePath {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unavailableLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.freeSpace,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
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 Link from 'Components/Link/Link';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
|
@ -12,30 +13,45 @@ function RootFolderRow(props) {
|
|||
const {
|
||||
id,
|
||||
path,
|
||||
accessible,
|
||||
freeSpace,
|
||||
unmappedFolders,
|
||||
onDeletePress
|
||||
} = props;
|
||||
|
||||
const unmappedFoldersCount = unmappedFolders.length || '-';
|
||||
const isUnavailable = !accessible;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/add/import/${id}`}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
{
|
||||
isUnavailable ?
|
||||
<div className={styles.unavailablePath}>
|
||||
{path}
|
||||
|
||||
<Label
|
||||
className={styles.unavailableLabel}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
Unavailable
|
||||
</Label>
|
||||
</div> :
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/add/import/${id}`}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace) || '-'}
|
||||
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.unmappedFolders}>
|
||||
{unmappedFoldersCount}
|
||||
{isUnavailable ? '-' : unmappedFolders.length}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
|
@ -52,13 +68,13 @@ function RootFolderRow(props) {
|
|||
RootFolderRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number.isRequired,
|
||||
accessible: PropTypes.bool.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RootFolderRow.defaultProps = {
|
||||
freeSpace: 0,
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,12 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
const logLevelOptions = [
|
||||
{ key: 'info', value: 'Info' },
|
||||
{ key: 'debug', value: 'Debug' },
|
||||
{ key: 'trace', value: 'Trace' }
|
||||
];
|
||||
|
||||
function LoggingSettings(props) {
|
||||
const {
|
||||
settings,
|
||||
|
@ -16,12 +22,6 @@ function LoggingSettings(props) {
|
|||
logLevel
|
||||
} = settings;
|
||||
|
||||
const logLevelOptions = [
|
||||
{ key: 'info', value: 'Info' },
|
||||
{ key: 'debug', value: 'Debug' },
|
||||
{ key: 'trace', value: 'Trace' }
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldSet legend="Logging">
|
||||
<FormGroup>
|
||||
|
|
|
@ -12,6 +12,7 @@ import FormLabel from 'Components/Form/FormLabel';
|
|||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import RootFoldersConnector from 'RootFolder/RootFoldersConnector';
|
||||
import NamingConnector from './Naming/NamingConnector';
|
||||
import AddRootFolderConnector from './RootFolder/AddRootFolderConnector';
|
||||
|
||||
const rescanAfterRefreshOptions = [
|
||||
{ key: 'always', value: 'Always' },
|
||||
|
@ -135,6 +136,23 @@ class MediaManagement extends Component {
|
|||
</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
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
@ -281,6 +299,23 @@ class MediaManagement extends Component {
|
|||
{...settings.recycleBin}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{
|
||||
|
@ -374,6 +409,7 @@ class MediaManagement extends Component {
|
|||
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFoldersConnector />
|
||||
<AddRootFolderConnector />
|
||||
</FieldSet>
|
||||
</PageContentBodyConnector>
|
||||
</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 ?
|
||||
<div
|
||||
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
|
||||
kind={kinds.DANGER}
|
||||
|
|
|
@ -147,6 +147,7 @@ function TagDetailsModalContent(props) {
|
|||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
title={isTagUsed ? 'Cannot be deleted while in use' : undefined}
|
||||
isDisabled={isTagUsed}
|
||||
onPress={onDeleteTagPress}
|
||||
>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
.tag {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 150px;
|
||||
flex: 150px 0 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 20px;
|
||||
white-space: nowrap;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ export const toggleMovieMonitored = createThunk(TOGGLE_MOVIE_MONITORED);
|
|||
|
||||
export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
|
||||
return {
|
||||
section: 'movies',
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
|
|
@ -174,6 +174,7 @@ module.exports = {
|
|||
|
||||
calendarTodayBackgroundColor: '#ddd',
|
||||
calendarBorderColor: '#cecece',
|
||||
calendarTextDim: '#666',
|
||||
|
||||
//
|
||||
// Table
|
||||
|
|
|
@ -26,6 +26,7 @@ function getInternalLink(source) {
|
|||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'DownloadClientStatusCheck':
|
||||
case 'ImportMechanismCheck':
|
||||
return (
|
||||
<IconButton
|
||||
|
@ -67,6 +68,7 @@ function getTestLink(source, props) {
|
|||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'DownloadClientStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
|
|
|
@ -27,5 +27,5 @@
|
|||
.actions {
|
||||
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;
|
||||
}
|
12
frontend/src/Utilities/mobile.js
Normal file
12
frontend/src/Utilities/mobile.js
Normal file
|
@ -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');
|
||||
}
|
13
frontend/src/Utilities/scrollLock.js
Normal file
13
frontend/src/Utilities/scrollLock.js
Normal file
|
@ -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 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)
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace NzbDrone.Api.Qualities
|
|||
|
||||
CreateResource = Create;
|
||||
|
||||
DeleteResource = Delete;
|
||||
DeleteResource = DeleteFormat;
|
||||
|
||||
Get["/test"] = x => Test();
|
||||
|
||||
|
@ -77,7 +77,7 @@ namespace NzbDrone.Api.Qualities
|
|||
return _formatService.All().ToResource();
|
||||
}
|
||||
|
||||
private void Delete(int id)
|
||||
private void DeleteFormat(int id)
|
||||
{
|
||||
_formatService.Delete(id);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace NzbDrone.Automation.Test
|
|||
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", NLog.LogLevel.Trace, consoleTarget));
|
||||
}
|
||||
|
||||
[TestFixtureSetUp]
|
||||
[OneTimeSetUp]
|
||||
public void SmokeTestSetup()
|
||||
{
|
||||
driver = new FirefoxDriver();
|
||||
|
@ -56,7 +56,7 @@ namespace NzbDrone.Automation.Test
|
|||
.Select(e => e.Text);
|
||||
}
|
||||
|
||||
[TestFixtureTearDown]
|
||||
[OneTimeTearDown]
|
||||
public void SmokeTestTearDown()
|
||||
{
|
||||
_runner.KillAll();
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider
|
||||
{
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void directory_exist_should_be_able_to_find_existing_folder()
|
||||
{
|
||||
Subject.FolderExists(TempFolder).Should().BeTrue();
|
||||
|
@ -32,6 +33,7 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||
protected abstract void SetWritePermissions(string path, bool writable);
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void FolderWritable_should_return_true_for_writable_directory()
|
||||
{
|
||||
var tempFolder = GetTempFilePath();
|
||||
|
@ -62,6 +64,7 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void MoveFile_should_overwrite_existing_file()
|
||||
{
|
||||
var source1 = GetTempFilePath();
|
||||
|
@ -122,6 +125,7 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void empty_folder_should_return_folder_modified_date()
|
||||
{
|
||||
var tempfolder = new DirectoryInfo(TempFolder);
|
||||
|
|
|
@ -616,6 +616,7 @@ namespace NzbDrone.Common.Test.DiskTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Retry(5)]
|
||||
public void CopyFolder_should_copy_folder()
|
||||
{
|
||||
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\FileSystemResult.cs" />
|
||||
<Compile Include="Disk\SystemFolders.cs" />
|
||||
<Compile Include="EnvironmentInfo\IOperatingSystemVersionInfo.cs" />
|
||||
<Compile Include="EnvironmentInfo\IOsVersionAdapter.cs" />
|
||||
<Compile Include="EnvironmentInfo\IPlatformInfo.cs" />
|
||||
<Compile Include="EnvironmentInfo\OsVersionModel.cs" />
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore.Converters
|
|||
{
|
||||
var i = 5;
|
||||
|
||||
Subject.ToDB(i).Should().Be(5);
|
||||
Subject.ToDB(i).Should().Be(i);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -83,5 +83,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
||||
}, 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]
|
||||
public void should_skip_deletestatus_copy()
|
||||
public void should_report_deletestatus_copy_as_failed()
|
||||
{
|
||||
_completed.DeleteStatus = "COPY";
|
||||
|
||||
GivenQueue(null);
|
||||
GivenHistory(_completed);
|
||||
|
||||
var result = Subject.GetItems().SingleOrDefault();
|
||||
var result = Subject.GetItems().Single();
|
||||
|
||||
result.Should().BeNull();
|
||||
result.Status.Should().Be(DownloadItemStatus.Failed);
|
||||
}
|
||||
|
||||
[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.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.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.PublishDate.Should().Be(DateTime.Parse("2015-06-05 16:58:11 +0000").ToUniversalTime());
|
||||
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]
|
||||
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());
|
||||
GivenFreeSpace(150.Megabytes());
|
||||
|
||||
|
|
|
@ -63,5 +63,16 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Specifications
|
|||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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]
|
||||
|
|
|
@ -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\CleanupUnusedTagsFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleasesFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\FixFutureIndexerStatusTimesFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
||||
<Compile Include="Http\HttpProxySettingsProviderFixture.cs" />
|
||||
<Compile Include="Http\TorCacheHttpRequestInterceptorFixture.cs" />
|
||||
|
@ -267,6 +268,7 @@
|
|||
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
|
||||
<Compile Include="IndexerTests\NewznabTests\NewznabCapabilitiesProviderFixture.cs" />
|
||||
<Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" />
|
||||
<Compile Include="IndexerTests\SeedConfigProviderFixture.cs" />
|
||||
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" />
|
||||
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.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))
|
||||
.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" });
|
||||
}
|
||||
|
||||
|
@ -56,12 +56,13 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_all_expired_folders()
|
||||
{
|
||||
WithExpired();
|
||||
public void should_return_if_recycleBinCleanupDays_is_zero()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(s => s.RecycleBinCleanupDays).Returns(0);
|
||||
|
||||
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]
|
||||
|
|
|
@ -7,7 +7,6 @@ using FluentAssertions;
|
|||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
|
|
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Configuration
|
|||
// TODO: Change back to "master" for the first stable release.
|
||||
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 SslCertHash => GetValue("SslCertHash", "");
|
||||
|
|
|
@ -93,6 +93,12 @@ namespace NzbDrone.Core.Configuration
|
|||
set { SetValue("RecycleBin", value); }
|
||||
}
|
||||
|
||||
public int RecycleBinCleanupDays
|
||||
{
|
||||
get { return GetValueInt("RecycleBinCleanupDays", 7); }
|
||||
set { SetValue("RecycleBinCleanupDays", value); }
|
||||
}
|
||||
|
||||
public int RssSyncInterval
|
||||
{
|
||||
get { return GetValueInt("RssSyncInterval", 60); }
|
||||
|
@ -287,6 +293,13 @@ namespace NzbDrone.Core.Configuration
|
|||
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
|
||||
}
|
||||
|
||||
public int MinimumFreeSpaceWhenImporting
|
||||
{
|
||||
get { return GetValueInt("MinimumFreeSpaceWhenImporting", 100); }
|
||||
|
||||
set { SetValue("MinimumFreeSpaceWhenImporting", value); }
|
||||
}
|
||||
|
||||
public bool CopyUsingHardlinks
|
||||
{
|
||||
get { return GetValueBoolean("CopyUsingHardlinks", true); }
|
||||
|
|
|
@ -27,11 +27,13 @@ namespace NzbDrone.Core.Configuration
|
|||
//Media Management
|
||||
bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }
|
||||
string RecycleBin { get; set; }
|
||||
int RecycleBinCleanupDays { get; set; }
|
||||
bool AutoDownloadPropers { get; set; }
|
||||
bool CreateEmptyMovieFolders { get; set; }
|
||||
bool DeleteEmptyFolders { get; set; }
|
||||
FileDateType FileDate { get; set; }
|
||||
bool SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||
int MinimumFreeSpaceWhenImporting { get; set; }
|
||||
bool CopyUsingHardlinks { get; set; }
|
||||
bool EnableMediaInfo { get; set; }
|
||||
bool ImportExtraFiles { get; set; }
|
||||
|
|
|
@ -39,7 +39,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
|
||||
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);
|
||||
var mostRecent = _historyService.MostRecentForMovie(subject.Movie.Id);
|
||||
|
||||
|
@ -51,7 +51,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
|
||||
if (!recent && cdhEnabled)
|
||||
{
|
||||
return Decision.Accept();
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (!cutoffUnmet)
|
||||
|
|
|
@ -48,9 +48,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||
}
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
_logger.ErrorException(ex.Message, ex);
|
||||
_logger.Error(ex, ex.Message);
|
||||
return Enumerable.Empty<DownloadClientItem>();
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
_logger.ErrorException(ex.Message, ex);
|
||||
_logger.Error(ex, ex.Message);
|
||||
|
||||
return new NzbDroneValidationFailure("Password", "Authentication failed");
|
||||
}
|
||||
|
@ -188,7 +188,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Failed to map Hadouken torrent data.", ex);
|
||||
_logger.Error(ex, "Failed to map Hadouken torrent data.");
|
||||
}
|
||||
|
||||
return torrent;
|
||||
|
|
|
@ -135,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
historyItem.CanMoveFiles = true;
|
||||
historyItem.CanBeRemoved = true;
|
||||
|
||||
if (item.DeleteStatus == "MANUAL" || item.DeleteStatus == "COPY")
|
||||
if (item.DeleteStatus == "MANUAL")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -53,18 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||
|
||||
private IEnumerable<DownloadClientItem> GetQueue()
|
||||
{
|
||||
SabnzbdQueue sabQueue;
|
||||
|
||||
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 sabQueue = _proxy.GetQueue(0, 0, Settings);
|
||||
var queueItems = new List<DownloadClientItem>();
|
||||
|
||||
foreach (var sabQueueItem in sabQueue.Items)
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
torrentInfo.Title = torrent.title;
|
||||
torrentInfo.Size = torrent.size;
|
||||
torrentInfo.DownloadUrl = torrent.download;
|
||||
torrentInfo.InfoUrl = torrent.info_page;
|
||||
torrentInfo.InfoUrl = torrent.info_page + "&app_id=Radarr"; ;
|
||||
torrentInfo.PublishDate = torrent.pubdate.ToUniversalTime();
|
||||
torrentInfo.Seeders = torrent.seeders;
|
||||
torrentInfo.Peers = torrent.leechers + torrent.seeders;
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
{
|
||||
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
|
||||
|
@ -71,7 +71,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
_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())
|
||||
{
|
||||
|
@ -96,14 +96,17 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
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;
|
||||
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())
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
var files = _diskScanService.FilterFiles(baseFolder, _diskScanService.GetVideoFiles(baseFolder, false));
|
||||
var subfolders = _diskScanService.FilterFiles(baseFolder, _diskProvider.GetDirectories(baseFolder));
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -159,11 +155,11 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
|
|||
if (downloadId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.Find(downloadId);
|
||||
downloadClientItem = trackedDownload.DownloadItem;
|
||||
downloadClientItem = trackedDownload?.DownloadItem;
|
||||
|
||||
if (movie == null)
|
||||
{
|
||||
movie = trackedDownload.RemoteMovie.Movie;
|
||||
movie = trackedDownload?.RemoteMovie?.Movie;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
|
|||
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);
|
||||
return Decision.Reject("Not enough free space");
|
||||
|
|
|
@ -19,12 +19,20 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
|
|||
{
|
||||
var movieFile = localMovie.Movie.MovieFile;
|
||||
|
||||
if (localMovie.Movie.MovieFileId == 0 || movieFile == null)
|
||||
if (localMovie.Movie.MovieFileId == 0)
|
||||
{
|
||||
_logger.Debug("No existing movie file, skipping");
|
||||
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)
|
||||
{
|
||||
_logger.Debug("'{0}' Has the same filesize as existing file", localMovie.Path);
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
public interface IRecycleBinProvider
|
||||
{
|
||||
void DeleteFolder(string path);
|
||||
void DeleteFile(string path);
|
||||
void DeleteFile(string path, string subfolder = "");
|
||||
void Empty();
|
||||
void Cleanup();
|
||||
}
|
||||
|
@ -62,18 +62,14 @@ namespace NzbDrone.Core.MediaFiles
|
|||
_diskProvider.FolderSetLastWriteTime(destination, DateTime.UtcNow);
|
||||
foreach (var file in _diskProvider.GetFiles(destination, SearchOption.AllDirectories))
|
||||
{
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
//TODO: Better fix than this for non-Windows?
|
||||
_diskProvider.FileSetLastWriteTime(file, DateTime.UtcNow);
|
||||
}
|
||||
SetLastWriteTime(file, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
_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);
|
||||
var recyclingBin = _configService.RecycleBin;
|
||||
|
@ -94,7 +90,10 @@ namespace NzbDrone.Core.MediaFiles
|
|||
else
|
||||
{
|
||||
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;
|
||||
while (_diskProvider.FileExists(destination))
|
||||
|
@ -102,11 +101,11 @@ namespace NzbDrone.Core.MediaFiles
|
|||
index++;
|
||||
if (fileInfo.Extension.IsNullOrWhiteSpace())
|
||||
{
|
||||
destination = Path.Combine(recyclingBin, fileInfo.Name + "_" + index);
|
||||
destination = Path.Combine(destinationFolder, fileInfo.Name + "_" + index);
|
||||
}
|
||||
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)
|
||||
{
|
||||
var message = string.Format("Unable to move '{0}' to the recycling bin: '{1}'", path, destination);
|
||||
_logger.Error(e, message);
|
||||
_logger.Error(e, "Unable to move '{0}' to the recycling bin: '{1}'", path, destination);
|
||||
throw;
|
||||
}
|
||||
|
||||
//TODO: Better fix than this for non-Windows?
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
_diskProvider.FileSetLastWriteTime(destination, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
SetLastWriteTime(destination, DateTime.UtcNow);
|
||||
|
||||
_logger.Debug("File has been moved to the recycling bin: {0}", destination);
|
||||
}
|
||||
|
@ -163,22 +157,19 @@ namespace NzbDrone.Core.MediaFiles
|
|||
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.Debug("Folder hasn't expired yet, skipping: {0}", folder);
|
||||
continue;
|
||||
}
|
||||
|
||||
_diskProvider.DeleteFolder(folder, true);
|
||||
_logger.Info("Automatic cleanup of Recycle Bin is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
continue;
|
||||
|
@ -187,9 +178,26 @@ namespace NzbDrone.Core.MediaFiles
|
|||
_diskProvider.DeleteFile(file);
|
||||
}
|
||||
|
||||
_diskProvider.RemoveEmptySubfolders(_configService.RecycleBin);
|
||||
|
||||
_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)
|
||||
{
|
||||
if (message.DeleteFiles)
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace NzbDrone.Core.Messaging.Commands
|
|||
_logger.Error(ex, "Thread aborted: " + ex.Message);
|
||||
Thread.ResetAbort();
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Trace("Stopped one command execution pipeline");
|
||||
}
|
||||
|
|
|
@ -385,7 +385,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
{
|
||||
return new List<Movie> { GetMovieInfo(parserResult.ImdbId) };
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
return new List<Movie>();
|
||||
}
|
||||
|
@ -538,7 +538,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster);
|
||||
imdbMovie.Images.Add(imdbPoster);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Debug(result);
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ namespace NzbDrone.Core.Movies
|
|||
movie.SecondaryYearSourceId = 0;
|
||||
}
|
||||
}
|
||||
catch (RadarrAPIException ex)
|
||||
catch (RadarrAPIException)
|
||||
{
|
||||
//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");
|
||||
}
|
||||
catch (WebException ex)
|
||||
catch (WebException)
|
||||
{
|
||||
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace NzbDrone.Core.Parser.Augmenters
|
|||
try {
|
||||
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);
|
||||
} // 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);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
#if !LIBRARY
|
||||
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]))(?=[_.-])",
|
||||
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);
|
||||
|
||||
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 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);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace NzbDrone.Core.Parser
|
|||
|
||||
private static readonly Regex SourceRegex = new Regex(@"\b(?:
|
||||
(?<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)|
|
||||
(?<bdrip>BDRip)|(?<brrip>BRRip)|
|
||||
(?<dvdr>DVD-R|DVDR)|
|
||||
|
|
|
@ -2,6 +2,8 @@ using System.Linq;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Disk;
|
||||
|
@ -71,11 +73,9 @@ namespace NzbDrone.Core.RootFolders
|
|||
{
|
||||
try
|
||||
{
|
||||
if (folder.Path.IsPathValid() && _diskProvider.FolderExists(folder.Path))
|
||||
if (folder.Path.IsPathValid())
|
||||
{
|
||||
folder.FreeSpace = _diskProvider.GetAvailableSpace(folder.Path);
|
||||
folder.TotalSpace = _diskProvider.GetTotalSize(folder.Path);
|
||||
folder.UnmappedFolders = GetUnmappedFolders(folder.Path);
|
||||
GetDetails(folder);
|
||||
}
|
||||
}
|
||||
//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);
|
||||
|
||||
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path);
|
||||
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
|
||||
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
|
||||
GetDetails(rootFolder);
|
||||
|
||||
return rootFolder;
|
||||
}
|
||||
|
@ -168,9 +166,8 @@ namespace NzbDrone.Core.RootFolders
|
|||
public RootFolder Get(int id)
|
||||
{
|
||||
var rootFolder = _rootFolderRepository.Get(id);
|
||||
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path);
|
||||
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
|
||||
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
|
||||
GetDetails(rootFolder);
|
||||
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
|
@ -187,5 +184,19 @@ namespace NzbDrone.Core.RootFolders
|
|||
|
||||
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);
|
||||
await Next.Invoke(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Debug("Unable to set version header");
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ namespace Radarr.Api.V2.Config
|
|||
public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator)
|
||||
: base(configService)
|
||||
{
|
||||
SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0);
|
||||
SharedValidator.RuleFor(c => c.FileChmod).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.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
|
||||
}
|
||||
|
||||
protected override MediaManagementConfigResource ToResource(IConfigService model)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue