mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
83 Commits
v3.0.7.1477
..
v3
| Author | SHA1 | Date | |
|---|---|---|---|
| 2639c069bc | |||
| d185783987 | |||
| e4c0e80e3e | |||
| 46bc711558 | |||
| e35e24a4c2 | |||
| 7eb61fafa4 | |||
| ab578566be | |||
| 35edef91c6 | |||
| 78dc2a7e13 | |||
| 5d976ac657 | |||
| d5fff15f32 | |||
| 7c98c2397a | |||
| ad6081aec6 | |||
| 1558929484 | |||
| 4a2f120bc1 | |||
| 6c0f22a11e | |||
| 41a821352e | |||
| 0991cfe27e | |||
| 97925feed9 | |||
| 93fc9abae9 | |||
| 2ea7b477cb | |||
| c9df12e6bc | |||
| b542dd0ddd | |||
| c1e5b7f642 | |||
| ccb88919b9 | |||
| f9b2c2d843 | |||
| d48950ec3c | |||
| 6a7d84f134 | |||
| a81a80a00f | |||
| c00cbb9a5a | |||
| 0d739cd26d | |||
| d01e6d32de | |||
| 704cf7aebe | |||
| 52d95fa632 | |||
| a71cc1081e | |||
| edf1167a37 | |||
| 8f2c4fe4d1 | |||
| cc9fc1e3c3 | |||
| 9fb29f42c4 | |||
| 9a1a320110 | |||
| f6664b8b42 | |||
| 82646db70d | |||
| 97e40dc00a | |||
| ae0e23fc8e | |||
| 893a6744ac | |||
| fa4b80b86f | |||
| 18f7bcd212 | |||
| 458c5cd0b3 | |||
| c93f63cd20 | |||
| bc5a43bd92 | |||
| a6a68b4cae | |||
| 9183c6b846 | |||
| d73ad3e27a | |||
| bd70fa5410 | |||
| 481345226a | |||
| 365c6a7741 | |||
| 8fa6e5ec6d | |||
| d376ae2f9f | |||
| 103d2751ee | |||
| bdd5865876 | |||
| f678775e5c | |||
| 5a08d5dc24 | |||
| bba4a5636e | |||
| 8d83b1d8d6 | |||
| 3be5d6c258 | |||
| 40ecdbc12d | |||
| 6e271e9272 | |||
| 5c5b012ded | |||
| be1acfc2f9 | |||
| ebb48a19cc | |||
| fa9136c4d1 | |||
| e7ca98489e | |||
| a3fd3c5e67 | |||
| cc09f85212 | |||
| 581fb2cb3d | |||
| d899225509 | |||
| 06464d720c | |||
| d21e9753bc | |||
| 07f0db477a | |||
| e280897bc7 | |||
| bb02fc4668 | |||
| e3aa92d09a | |||
| d02d1bbdfe |
@@ -24,6 +24,7 @@ function HistoryDetails(props) {
|
|||||||
indexer,
|
indexer,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
preferredWordScore,
|
preferredWordScore,
|
||||||
|
seriesMatchType,
|
||||||
nzbInfoUrl,
|
nzbInfoUrl,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
downloadClientName,
|
downloadClientName,
|
||||||
@@ -72,6 +73,16 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
seriesMatchType ?
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Series Match Type"
|
||||||
|
data={seriesMatchType}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
nzbInfoUrl ?
|
nzbInfoUrl ?
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -217,6 +217,16 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'details') {
|
if (name === 'details') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
+18
-1
@@ -1,4 +1,5 @@
|
|||||||
.series {
|
.container {
|
||||||
|
display: flex;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -6,3 +7,19 @@
|
|||||||
background-color: $menuItemHoverBackgroundColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.series {
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvdbLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvdbLinkIcon {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|||||||
+29
-23
@@ -1,33 +1,28 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||||
import styles from './ImportSeriesSearchResult.css';
|
import styles from './ImportSeriesSearchResult.css';
|
||||||
|
|
||||||
class ImportSeriesSearchResult extends Component {
|
function ImportSeriesSearchResult(props) {
|
||||||
|
const {
|
||||||
|
tvdbId,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
network,
|
||||||
|
isExistingSeries,
|
||||||
|
onPress
|
||||||
|
} = props;
|
||||||
|
|
||||||
//
|
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
return (
|
||||||
this.props.onPress(this.props.tvdbId);
|
<div className={styles.container}>
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
year,
|
|
||||||
network,
|
|
||||||
isExistingSeries
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
<Link
|
||||||
className={styles.series}
|
className={styles.series}
|
||||||
onPress={this.onPress}
|
onPress={onPressCallback}
|
||||||
>
|
>
|
||||||
<ImportSeriesTitle
|
<ImportSeriesTitle
|
||||||
title={title}
|
title={title}
|
||||||
@@ -36,8 +31,19 @@ class ImportSeriesSearchResult extends Component {
|
|||||||
isExistingSeries={isExistingSeries}
|
isExistingSeries={isExistingSeries}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
|
||||||
}
|
<Link
|
||||||
|
className={styles.tvdbLink}
|
||||||
|
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.tvdbLinkIcon}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportSeriesSearchResult.propTypes = {
|
ImportSeriesSearchResult.propTypes = {
|
||||||
|
|||||||
@@ -20,24 +20,27 @@ function ImportSeriesTitle(props) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!title.contains(year) &&
|
!title.contains(year) &&
|
||||||
year > 0 &&
|
year > 0 ?
|
||||||
<span className={styles.year}>
|
<span className={styles.year}>
|
||||||
({year})
|
({year})
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!network &&
|
network ?
|
||||||
<Label>{network}</Label>
|
<Label>{network}</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isExistingSeries &&
|
isExistingSeries ?
|
||||||
<Label
|
<Label
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
Existing
|
Existing
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
|||||||
|
|
||||||
function App({ store, history }) {
|
function App({ store, history }) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title="Sonarr">
|
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
|||||||
import Settings from 'Settings/Settings';
|
import Settings from 'Settings/Settings';
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
import Quality from 'Settings/Quality/Quality';
|
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
@@ -158,7 +158,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/quality"
|
path="/settings/quality"
|
||||||
component={Quality}
|
component={QualityConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
|
Sonarr has lost its connection to the backend and will need to be reloaded to restore functionality.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const REFRESH_SERIES = 'RefreshSeries';
|
|||||||
export const RENAME_FILES = 'RenameFiles';
|
export const RENAME_FILES = 'RenameFiles';
|
||||||
export const RENAME_SERIES = 'RenameSeries';
|
export const RENAME_SERIES = 'RenameSeries';
|
||||||
export const RESET_API_KEY = 'ResetApiKey';
|
export const RESET_API_KEY = 'ResetApiKey';
|
||||||
|
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
|
||||||
export const RSS_SYNC = 'RssSync';
|
export const RSS_SYNC = 'RssSync';
|
||||||
export const SEASON_SEARCH = 'SeasonSearch';
|
export const SEASON_SEARCH = 'SeasonSearch';
|
||||||
export const SERIES_SEARCH = 'SeriesSearch';
|
export const SERIES_SEARCH = 'SeriesSearch';
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
|
|||||||
<TextInput
|
<TextInput
|
||||||
name={NAME}
|
name={NAME}
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
|
type="date"
|
||||||
placeholder="yyyy-mm-dd"
|
placeholder="yyyy-mm-dd"
|
||||||
onChange={this.onValueChange}
|
onChange={this.onValueChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
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';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import FocusLock from 'react-focus-lock';
|
|||||||
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 { isIOS } from 'Utilities/browser';
|
||||||
import { setScrollLock } from 'Utilities/scrollLock';
|
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';
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary errorComponent={PageContentError}>
|
<ErrorBoundary errorComponent={PageContentError}>
|
||||||
<DocumentTitle title={title ? `${title} - Sonarr` : 'Sonarr'}>
|
<DocumentTitle title={title ? `${title} - ${window.Sonarr.instanceName}` : window.Sonarr.instanceName}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile, isFirefox } from 'Utilities/browser';
|
||||||
import { isLocked } from 'Utilities/scrollLock';
|
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';
|
||||||
@@ -15,7 +15,8 @@ class PageContentBody extends Component {
|
|||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._isMobile = isMobileUtil();
|
this._isMobile = isMobile();
|
||||||
|
this._isSmallScreenFirefox = isFirefox && window.innerWidth < 768;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -41,7 +42,9 @@ class PageContentBody extends Component {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
const ScrollerComponent = this._isMobile || this._isSmallScreenFirefox ?
|
||||||
|
Scroller :
|
||||||
|
OverlayScroller;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollerComponent
|
<ScrollerComponent
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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';
|
||||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||||
|
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||||
|
|
||||||
function getState(status) {
|
function getState(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -70,6 +71,7 @@ const mapDispatchToProps = {
|
|||||||
dispatchUpdateItem: updateItem,
|
dispatchUpdateItem: updateItem,
|
||||||
dispatchRemoveItem: removeItem,
|
dispatchRemoveItem: removeItem,
|
||||||
dispatchFetchHealth: fetchHealth,
|
dispatchFetchHealth: fetchHealth,
|
||||||
|
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
||||||
dispatchFetchQueue: fetchQueue,
|
dispatchFetchQueue: fetchQueue,
|
||||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||||
dispatchFetchRootFolders: fetchRootFolders,
|
dispatchFetchRootFolders: fetchRootFolders,
|
||||||
@@ -221,6 +223,10 @@ class SignalRConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleQualitydefinition = () => {
|
||||||
|
this.props.dispatchFetchQualityDefinitions();
|
||||||
|
}
|
||||||
|
|
||||||
handleQueue = () => {
|
handleQueue = () => {
|
||||||
if (this.props.isQueuePopulated) {
|
if (this.props.isQueuePopulated) {
|
||||||
this.props.dispatchFetchQueue();
|
this.props.dispatchFetchQueue();
|
||||||
@@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
|
|||||||
dispatchUpdateItem: PropTypes.func.isRequired,
|
dispatchUpdateItem: PropTypes.func.isRequired,
|
||||||
dispatchRemoveItem: PropTypes.func.isRequired,
|
dispatchRemoveItem: PropTypes.func.isRequired,
|
||||||
dispatchFetchHealth: PropTypes.func.isRequired,
|
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
||||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class VirtualTable extends Component {
|
|||||||
scroller,
|
scroller,
|
||||||
header,
|
header,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
|
rowHeight,
|
||||||
rowRenderer,
|
rowRenderer,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -153,7 +154,7 @@ class VirtualTable extends Component {
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
headerHeight={height - headerHeight}
|
headerHeight={height - headerHeight}
|
||||||
rowHeight={ROW_HEIGHT}
|
rowHeight={rowHeight}
|
||||||
rowCount={items.length}
|
rowCount={items.length}
|
||||||
columnCount={1}
|
columnCount={1}
|
||||||
columnWidth={width}
|
columnWidth={width}
|
||||||
@@ -194,7 +195,8 @@ VirtualTable.propTypes = {
|
|||||||
|
|
||||||
VirtualTable.defaultProps = {
|
VirtualTable.defaultProps = {
|
||||||
className: styles.tableContainer,
|
className: styles.tableContainer,
|
||||||
headerHeight: 38
|
headerHeight: 38,
|
||||||
|
rowHeight: ROW_HEIGHT
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VirtualTable;
|
export default VirtualTable;
|
||||||
|
|||||||
@@ -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 { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ function MediaInfo(props) {
|
|||||||
audioCodec,
|
audioCodec,
|
||||||
audioLanguages,
|
audioLanguages,
|
||||||
subtitles,
|
subtitles,
|
||||||
videoCodec
|
videoCodec,
|
||||||
|
videoDynamicRangeType
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO) {
|
if (type === mediaInfoTypes.AUDIO) {
|
||||||
@@ -72,6 +73,14 @@ function MediaInfo(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{videoDynamicRangeType}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +90,8 @@ MediaInfo.propTypes = {
|
|||||||
audioCodec: PropTypes.string,
|
audioCodec: PropTypes.string,
|
||||||
audioLanguages: PropTypes.string,
|
audioLanguages: PropTypes.string,
|
||||||
subtitles: PropTypes.string,
|
subtitles: PropTypes.string,
|
||||||
videoCodec: PropTypes.string
|
videoCodec: PropTypes.string,
|
||||||
|
videoDynamicRangeType: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MediaInfo;
|
export default MediaInfo;
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export const AUDIO = 'audio';
|
|||||||
export const AUDIO_LANGUAGES = 'audioLanguages';
|
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||||
export const SUBTITLES = 'subtitles';
|
export const SUBTITLES = 'subtitles';
|
||||||
export const VIDEO = 'video';
|
export const VIDEO = 'video';
|
||||||
|
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class SelectEpisodeModalContent extends Component {
|
|||||||
isAnime,
|
isAnime,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
|
modalTitle,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -121,7 +122,7 @@ class SelectEpisodeModalContent extends Component {
|
|||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
Manual Import - Select Episode(s)
|
{modalTitle} - Select Episode(s)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
@@ -235,6 +236,7 @@ SelectEpisodeModalContent.propTypes = {
|
|||||||
isAnime: PropTypes.bool.isRequired,
|
isAnime: PropTypes.bool.isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.string,
|
sortDirection: PropTypes.string,
|
||||||
|
modalTitle: PropTypes.string,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onEpisodesSelect: PropTypes.func.isRequired,
|
onEpisodesSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
recentFolders,
|
recentFolders,
|
||||||
onRemoveRecentFolderPress,
|
onRemoveRecentFolderPress,
|
||||||
|
modalTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Folder
|
{modalTitle} - Select Folder
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -159,6 +160,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
|||||||
|
|
||||||
InteractiveImportSelectFolderModalContent.propTypes = {
|
InteractiveImportSelectFolderModalContent.propTypes = {
|
||||||
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onQuickImportPress: PropTypes.func.isRequired,
|
onQuickImportPress: PropTypes.func.isRequired,
|
||||||
onInteractiveImportPress: PropTypes.func.isRequired,
|
onInteractiveImportPress: PropTypes.func.isRequired,
|
||||||
onRemoveRecentFolderPress: PropTypes.func.isRequired,
|
onRemoveRecentFolderPress: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const filterExistingFilesOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importModeOptions = [
|
const importModeOptions = [
|
||||||
|
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||||
{ key: 'move', value: 'Move Files' },
|
{ key: 'move', value: 'Move Files' },
|
||||||
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
||||||
];
|
];
|
||||||
@@ -250,6 +251,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
importMode,
|
importMode,
|
||||||
interactiveImportErrorMessage,
|
interactiveImportErrorMessage,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
|
modalTitle,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -299,7 +301,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - {title || folder}
|
{modalTitle} - {title || folder}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
@@ -375,6 +377,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
allowSeriesChange={allowSeriesChange}
|
allowSeriesChange={allowSeriesChange}
|
||||||
autoSelectRow={autoSelectRow}
|
autoSelectRow={autoSelectRow}
|
||||||
columns={this.state.columns}
|
columns={this.state.columns}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onValidRowChange={this.onValidRowChange}
|
onValidRowChange={this.onValidRowChange}
|
||||||
/>
|
/>
|
||||||
@@ -452,6 +455,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
<SelectSeriesModal
|
<SelectSeriesModal
|
||||||
isOpen={selectModalOpen === SERIES}
|
isOpen={selectModalOpen === SERIES}
|
||||||
ids={selectedIds}
|
ids={selectedIds}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -459,6 +463,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
isOpen={selectModalOpen === SEASON}
|
isOpen={selectModalOpen === SEASON}
|
||||||
ids={selectedIds}
|
ids={selectedIds}
|
||||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -467,6 +472,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
ids={orderedSelectedIds}
|
ids={orderedSelectedIds}
|
||||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||||
seasonNumber={selectedItem && selectedItem.seasonNumber}
|
seasonNumber={selectedItem && selectedItem.seasonNumber}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -474,6 +480,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
isOpen={selectModalOpen === RELEASE_GROUP}
|
isOpen={selectModalOpen === RELEASE_GROUP}
|
||||||
ids={selectedIds}
|
ids={selectedIds}
|
||||||
releaseGroup=""
|
releaseGroup=""
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -481,6 +488,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
isOpen={selectModalOpen === LANGUAGE}
|
isOpen={selectModalOpen === LANGUAGE}
|
||||||
ids={selectedIds}
|
ids={selectedIds}
|
||||||
languageId={0}
|
languageId={0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -490,6 +498,7 @@ class InteractiveImportModalContent extends Component {
|
|||||||
qualityId={0}
|
qualityId={0}
|
||||||
proper={false}
|
proper={false}
|
||||||
real={false}
|
real={false}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectModalClose}
|
onModalClose={this.onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -528,6 +537,7 @@ InteractiveImportModalContent.propTypes = {
|
|||||||
interactiveImportErrorMessage: PropTypes.string,
|
interactiveImportErrorMessage: PropTypes.string,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
deleteError: PropTypes.object,
|
deleteError: PropTypes.object,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||||
onImportModeChange: PropTypes.func.isRequired,
|
onImportModeChange: PropTypes.func.isRequired,
|
||||||
|
|||||||
+7
-6
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
@@ -29,12 +30,7 @@ function isSameEpisodeFile(file, originalFile) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const episodeIds = episodes.map((e) => e.id);
|
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||||
const originalEpisodeIds = originalFile.episodes ? originalFile.episodes.map((e) => e.id) : [];
|
|
||||||
|
|
||||||
return episodeIds.every((episodeId) => {
|
|
||||||
return originalEpisodeIds.indexOf(episodeId) >= 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -182,6 +178,11 @@ class InteractiveImportModalContentConnector extends Component {
|
|||||||
const existingFiles = [];
|
const existingFiles = [];
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
|
if (importMode === 'chooseImportMode') {
|
||||||
|
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const isSelected = selected.indexOf(item.id) > -1;
|
const isSelected = selected.indexOf(item.id) > -1;
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ class InteractiveImportRow extends Component {
|
|||||||
size,
|
size,
|
||||||
rejections,
|
rejections,
|
||||||
isReprocessing,
|
isReprocessing,
|
||||||
isSelected
|
isSelected,
|
||||||
|
modalTitle
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -396,6 +397,7 @@ class InteractiveImportRow extends Component {
|
|||||||
<SelectSeriesModal
|
<SelectSeriesModal
|
||||||
isOpen={isSelectSeriesModalOpen}
|
isOpen={isSelectSeriesModalOpen}
|
||||||
ids={[id]}
|
ids={[id]}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectSeriesModalClose}
|
onModalClose={this.onSelectSeriesModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -403,6 +405,7 @@ class InteractiveImportRow extends Component {
|
|||||||
isOpen={isSelectSeasonModalOpen}
|
isOpen={isSelectSeasonModalOpen}
|
||||||
ids={[id]}
|
ids={[id]}
|
||||||
seriesId={series && series.id}
|
seriesId={series && series.id}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectSeasonModalClose}
|
onModalClose={this.onSelectSeasonModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -413,6 +416,7 @@ class InteractiveImportRow extends Component {
|
|||||||
isAnime={isAnime}
|
isAnime={isAnime}
|
||||||
seasonNumber={seasonNumber}
|
seasonNumber={seasonNumber}
|
||||||
relativePath={relativePath}
|
relativePath={relativePath}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectEpisodeModalClose}
|
onModalClose={this.onSelectEpisodeModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -420,6 +424,7 @@ class InteractiveImportRow extends Component {
|
|||||||
isOpen={isSelectReleaseGroupModalOpen}
|
isOpen={isSelectReleaseGroupModalOpen}
|
||||||
ids={[id]}
|
ids={[id]}
|
||||||
releaseGroup={releaseGroup ?? ''}
|
releaseGroup={releaseGroup ?? ''}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectReleaseGroupModalClose}
|
onModalClose={this.onSelectReleaseGroupModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -429,6 +434,7 @@ class InteractiveImportRow extends Component {
|
|||||||
qualityId={quality ? quality.quality.id : 0}
|
qualityId={quality ? quality.quality.id : 0}
|
||||||
proper={quality ? quality.revision.version > 1 : false}
|
proper={quality ? quality.revision.version > 1 : false}
|
||||||
real={quality ? quality.revision.real > 0 : false}
|
real={quality ? quality.revision.real > 0 : false}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectQualityModalClose}
|
onModalClose={this.onSelectQualityModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -436,6 +442,7 @@ class InteractiveImportRow extends Component {
|
|||||||
isOpen={isSelectLanguageModalOpen}
|
isOpen={isSelectLanguageModalOpen}
|
||||||
ids={[id]}
|
ids={[id]}
|
||||||
languageId={language ? language.id : 0}
|
languageId={language ? language.id : 0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
onModalClose={this.onSelectLanguageModalClose}
|
onModalClose={this.onSelectLanguageModalClose}
|
||||||
/>
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -460,6 +467,7 @@ InteractiveImportRow.propTypes = {
|
|||||||
episodeFileId: PropTypes.number,
|
episodeFileId: PropTypes.number,
|
||||||
isReprocessing: PropTypes.bool,
|
isReprocessing: PropTypes.bool,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onValidRowChange: PropTypes.func.isRequired
|
onValidRowChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,12 @@ InteractiveImportModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
folder: PropTypes.string,
|
folder: PropTypes.string,
|
||||||
downloadId: PropTypes.string,
|
downloadId: PropTypes.string,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
InteractiveImportModal.defaultProps = {
|
||||||
|
modalTitle: 'Manual Import'
|
||||||
|
};
|
||||||
|
|
||||||
export default InteractiveImportModal;
|
export default InteractiveImportModal;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function SelectLanguageModalContent(props) {
|
|||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
modalTitle,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onLanguageSelect
|
onLanguageSelect
|
||||||
} = props;
|
} = props;
|
||||||
@@ -33,7 +34,7 @@ function SelectLanguageModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Language
|
{modalTitle} - Select Language
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -80,6 +81,7 @@ SelectLanguageModalContent.propTypes = {
|
|||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onLanguageSelect: PropTypes.func.isRequired,
|
onLanguageSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class SelectQualityModalContent extends Component {
|
|||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
|
modalTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ class SelectQualityModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Quality
|
{modalTitle} - Select Quality
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -159,6 +160,7 @@ SelectQualityModalContent.propTypes = {
|
|||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onQualitySelect: PropTypes.func.isRequired,
|
onQualitySelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class SelectReleaseGroupModalContent extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
modalTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ class SelectReleaseGroupModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Set Release Group
|
{modalTitle} - Set Release Group
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody
|
<ModalBody
|
||||||
@@ -96,6 +97,7 @@ class SelectReleaseGroupModalContent extends Component {
|
|||||||
|
|
||||||
SelectReleaseGroupModalContent.propTypes = {
|
SelectReleaseGroupModalContent.propTypes = {
|
||||||
releaseGroup: PropTypes.string.isRequired,
|
releaseGroup: PropTypes.string.isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onReleaseGroupSelect: PropTypes.func.isRequired,
|
onReleaseGroupSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class SelectSeasonModalContent extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
|
modalTitle,
|
||||||
onSeasonSelect,
|
onSeasonSelect,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -22,7 +23,7 @@ class SelectSeasonModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Season
|
{modalTitle} - Select Season
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -51,6 +52,7 @@ class SelectSeasonModalContent extends Component {
|
|||||||
|
|
||||||
SelectSeasonModalContent.propTypes = {
|
SelectSeasonModalContent.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onSeasonSelect: PropTypes.func.isRequired,
|
onSeasonSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class SelectSeriesModalContent extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
|
modalTitle,
|
||||||
onSeriesSelect,
|
onSeriesSelect,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -47,7 +48,7 @@ class SelectSeriesModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manual Import - Select Series
|
{modalTitle} - Select Series
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody
|
<ModalBody
|
||||||
@@ -96,6 +97,7 @@ class SelectSeriesModalContent extends Component {
|
|||||||
|
|
||||||
SelectSeriesModalContent.propTypes = {
|
SelectSeriesModalContent.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
onSeriesSelect: PropTypes.func.isRequired,
|
onSeriesSelect: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ function InteractiveSearch(props) {
|
|||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<InteractiveSearchRow
|
<InteractiveSearchRow
|
||||||
key={item.guid}
|
key={`${item.indexerId}-${item.guid}`}
|
||||||
{...item}
|
{...item}
|
||||||
searchPayload={searchPayload}
|
searchPayload={searchPayload}
|
||||||
longDateFormat={longDateFormat}
|
longDateFormat={longDateFormat}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.audioLanguages,
|
.audioLanguages,
|
||||||
|
.videoDynamicRangeType,
|
||||||
.subtitles {
|
.subtitles {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,20 @@ class EpisodeRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'videoDynamicRangeType') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.videoDynamicRangeType}
|
||||||
|
>
|
||||||
|
<MediaInfoConnector
|
||||||
|
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||||
|
episodeFileId={episodeFileId}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'size') {
|
if (name === 'size') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ class SeriesDetails extends Component {
|
|||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Manage Episodes"
|
label="Manage Episodes"
|
||||||
iconName={icons.EPISODE_FILE}
|
iconName={icons.EPISODE_FILE}
|
||||||
isDisabled={!hasEpisodeFiles}
|
|
||||||
onPress={this.onManageEpisodesPress}
|
onPress={this.onManageEpisodesPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -651,6 +650,7 @@ class SeriesDetails extends Component {
|
|||||||
autoSelectRow={false}
|
autoSelectRow={false}
|
||||||
showDelete={true}
|
showDelete={true}
|
||||||
showImportMode={false}
|
showImportMode={false}
|
||||||
|
modalTitle={'Manage Episodes'}
|
||||||
onModalClose={this.onManageEpisodesModalClose}
|
onModalClose={this.onManageEpisodesModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const requiresRestartKeys = [
|
|||||||
'bindAddress',
|
'bindAddress',
|
||||||
'port',
|
'port',
|
||||||
'urlBase',
|
'urlBase',
|
||||||
|
'instanceName',
|
||||||
'enableSsl',
|
'enableSsl',
|
||||||
'sslPort',
|
'sslPort',
|
||||||
'sslCertHash',
|
'sslCertHash',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function HostSettings(props) {
|
|||||||
bindAddress,
|
bindAddress,
|
||||||
port,
|
port,
|
||||||
urlBase,
|
urlBase,
|
||||||
|
instanceName,
|
||||||
enableSsl,
|
enableSsl,
|
||||||
sslPort,
|
sslPort,
|
||||||
sslCertHash,
|
sslCertHash,
|
||||||
@@ -71,6 +72,22 @@ function HostSettings(props) {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
>
|
||||||
|
<FormLabel>Instance Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="instanceName"
|
||||||
|
helpText="Instance name in tab and for Syslog app name"
|
||||||
|
helpTextWarning="Requires restart to take effect"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...instanceName}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class AddIndexerModalContent extends Component {
|
|||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.</div>
|
<div>Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.</div>
|
||||||
<div>For more information on the individual indexers, clink on the info buttons.</div>
|
<div>For more information on the individual indexers, click on the info buttons.</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<FieldSet legend="Usenet">
|
<FieldSet legend="Usenet">
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class MediaManagement extends Component {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="episodeTitleRequired"
|
name="episodeTitleRequired"
|
||||||
helpText="Prevent importing for up to 24 hours if the episode title is in the naming format and the episode title is TBA"
|
helpText="Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA"
|
||||||
values={episodeTitleRequiredOptions}
|
values={episodeTitleRequiredOptions}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.episodeTitleRequired}
|
{...settings.episodeTitleRequired}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function NotificationEventItems(props) {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="onDownload"
|
name="onDownload"
|
||||||
helpText="On Download"
|
helpText="On Import"
|
||||||
isDisabled={!supportsOnDownload.value}
|
isDisabled={!supportsOnDownload.value}
|
||||||
{...onDownload}
|
{...onDownload}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class DelayProfile extends Component {
|
|||||||
connectDragSource
|
connectDragSource
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let preferred = titleCase(preferredProtocol);
|
let preferred = `Prefer ${titleCase(preferredProtocol)}`;
|
||||||
|
|
||||||
if (!enableUsenet) {
|
if (!enableUsenet) {
|
||||||
preferred = 'Only Torrent';
|
preferred = 'Only Torrent';
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class DelayProfiles extends Component {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.delayProfilesHeader}>
|
<div className={styles.delayProfilesHeader}>
|
||||||
<div className={styles.column}>Protocol</div>
|
<div className={styles.column}>Preferred Protocol</div>
|
||||||
<div className={styles.column}>Usenet Delay</div>
|
<div className={styles.column}>Usenet Delay</div>
|
||||||
<div className={styles.column}>Torrent Delay</div>
|
<div className={styles.column}>Torrent Delay</div>
|
||||||
<div className={styles.tags}>Tags</div>
|
<div className={styles.tags}>Tags</div>
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import FormLabel from 'Components/Form/FormLabel';
|
|||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import styles from './EditDelayProfileModalContent.css';
|
import styles from './EditDelayProfileModalContent.css';
|
||||||
|
|
||||||
|
const protocolOptions = [
|
||||||
|
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
||||||
|
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
||||||
|
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
||||||
|
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
||||||
|
];
|
||||||
|
|
||||||
function EditDelayProfileModalContent(props) {
|
function EditDelayProfileModalContent(props) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -25,7 +32,6 @@ function EditDelayProfileModalContent(props) {
|
|||||||
saveError,
|
saveError,
|
||||||
item,
|
item,
|
||||||
protocol,
|
protocol,
|
||||||
protocolOptions,
|
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onProtocolChange,
|
onProtocolChange,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
@@ -51,20 +57,22 @@ function EditDelayProfileModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching ?
|
||||||
<LoadingIndicator />
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error ?
|
||||||
<div>Unable to add a new quality profile, please try again.</div>
|
<div>Unable to add a new quality profile, please try again.</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !error &&
|
!isFetching && !error ?
|
||||||
<Form {...otherProps}>
|
<Form {...otherProps}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Protocol</FormLabel>
|
<FormLabel>Preferred Protocol</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
@@ -140,19 +148,21 @@ function EditDelayProfileModalContent(props) {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
</Form>
|
</Form> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
{
|
||||||
id && id > 1 &&
|
id && id > 1 ?
|
||||||
<Button
|
<Button
|
||||||
className={styles.deleteButton}
|
className={styles.deleteButton}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={onDeleteDelayProfilePress}
|
onPress={onDeleteDelayProfilePress}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -190,7 +200,6 @@ EditDelayProfileModalContent.propTypes = {
|
|||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onProtocolChange: PropTypes.func.isRequired,
|
onProtocolChange: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ const newDelayProfile = {
|
|||||||
tags: []
|
tags: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const protocolOptions = [
|
|
||||||
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
|
||||||
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
|
||||||
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
|
||||||
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function createDelayProfileSelector() {
|
function createDelayProfileSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { id }) => id,
|
(state, { id }) => id,
|
||||||
@@ -78,7 +71,6 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
protocol,
|
protocol,
|
||||||
protocolOptions,
|
|
||||||
...delayProfile
|
...delayProfile
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||||
|
|
||||||
class Quality extends Component {
|
class Quality extends Component {
|
||||||
|
|
||||||
@@ -16,7 +21,8 @@ class Quality extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
hasPendingChanges: false
|
hasPendingChanges: false,
|
||||||
|
isConfirmQualityDefinitionResetModalOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +37,14 @@ class Quality extends Component {
|
|||||||
this.setState(payload);
|
this.setState(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResetQualityDefinitionsPress = () => {
|
||||||
|
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseResetQualityDefinitionsModal = () => {
|
||||||
|
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
onSavePress = () => {
|
onSavePress = () => {
|
||||||
if (this._saveCallback) {
|
if (this._saveCallback) {
|
||||||
this._saveCallback();
|
this._saveCallback();
|
||||||
@@ -43,6 +57,7 @@ class Quality extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isResettingQualityDefinitions,
|
||||||
hasPendingChanges
|
hasPendingChanges
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@@ -51,6 +66,18 @@ class Quality extends Component {
|
|||||||
<SettingsToolbarConnector
|
<SettingsToolbarConnector
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
hasPendingChanges={hasPendingChanges}
|
hasPendingChanges={hasPendingChanges}
|
||||||
|
additionalButtons={
|
||||||
|
<Fragment>
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Reset Definitions"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isResettingQualityDefinitions}
|
||||||
|
onPress={this.onResetQualityDefinitionsPress}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -60,9 +87,18 @@ class Quality extends Component {
|
|||||||
onChildStateChange={this.onChildStateChange}
|
onChildStateChange={this.onChildStateChange}
|
||||||
/>
|
/>
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
|
<ResetQualityDefinitionsModal
|
||||||
|
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
||||||
|
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Quality.propTypes = {
|
||||||
|
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default Quality;
|
export default Quality;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Quality from './Quality';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
|
||||||
|
(isResettingQualityDefinitions) => {
|
||||||
|
return {
|
||||||
|
isResettingQualityDefinitions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class QualityConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Quality
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityConnector.propTypes = {
|
||||||
|
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(QualityConnector);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ResetQualityDefinitionsModalContentConnector from './ResetQualityDefinitionsModalContentConnector';
|
||||||
|
|
||||||
|
function ResetQualityDefinitionsModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ResetQualityDefinitionsModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetQualityDefinitionsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetQualityDefinitionsModal;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.messageContainer {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import styles from './ResetQualityDefinitionsModalContent.css';
|
||||||
|
|
||||||
|
class ResetQualityDefinitionsModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
resetDefinitionTitles: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onResetDefinitionTitlesChange = ({ value }) => {
|
||||||
|
this.setState({ resetDefinitionTitles: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onResetQualityDefinitionsConfirmed = () => {
|
||||||
|
const resetDefinitionTitles = this.state.resetDefinitionTitles;
|
||||||
|
|
||||||
|
this.setState({ resetDefinitionTitles: false });
|
||||||
|
this.props.onResetQualityDefinitions(resetDefinitionTitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onModalClose,
|
||||||
|
isResettingQualityDefinitions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const resetDefinitionTitles = this.state.resetDefinitionTitles;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
Reset Quality Definitions
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.messageContainer}>
|
||||||
|
Are you sure you want to reset quality definitions?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Reset Titles</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="resetDefinitionTitles"
|
||||||
|
value={resetDefinitionTitles}
|
||||||
|
helpText="Reset definition titles as well as values"
|
||||||
|
onChange={this.onResetDefinitionTitlesChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onResetQualityDefinitionsConfirmed}
|
||||||
|
isDisabled={isResettingQualityDefinitions}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetQualityDefinitionsModalContent.propTypes = {
|
||||||
|
onResetQualityDefinitions: PropTypes.func.isRequired,
|
||||||
|
isResettingQualityDefinitions: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetQualityDefinitionsModalContent;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
|
||||||
|
(isResettingQualityDefinitions) => {
|
||||||
|
return {
|
||||||
|
isResettingQualityDefinitions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResetQualityDefinitionsModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onResetQualityDefinitions = (resetTitles) => {
|
||||||
|
this.props.executeCommand({ name: commandNames.RESET_QUALITY_DEFINITIONS, resetTitles });
|
||||||
|
this.props.onModalClose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ResetQualityDefinitionsModalContent
|
||||||
|
{...this.props}
|
||||||
|
onResetQualityDefinitions={this.onResetQualityDefinitions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetQualityDefinitionsModalContentConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
isResettingQualityDefinitions: PropTypes.bool.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ResetQualityDefinitionsModalContentConnector);
|
||||||
@@ -74,6 +74,11 @@ export const defaultState = {
|
|||||||
label: 'Video Codec',
|
label: 'Video Codec',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'videoDynamicRangeType',
|
||||||
|
label: 'Video Dynamic Range',
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'audioLanguages',
|
name: 'audioLanguages',
|
||||||
label: 'Audio Languages',
|
label: 'Audio Languages',
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export const defaultState = {
|
|||||||
label: 'Release Group',
|
label: 'Release Group',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceTitle',
|
||||||
|
label: 'Source Title',
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'preferredWordScore',
|
name: 'preferredWordScore',
|
||||||
columnLabel: 'Preferred Word Score',
|
columnLabel: 'Preferred Word Score',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
|
|||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
@@ -33,12 +34,12 @@ export const defaultState = {
|
|||||||
sortKey: 'quality',
|
sortKey: 'quality',
|
||||||
sortDirection: sortDirections.DESCENDING,
|
sortDirection: sortDirections.DESCENDING,
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'move',
|
importMode: 'chooseImportMode',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
relativePath: function(item, direction) {
|
relativePath: function(item, direction) {
|
||||||
const relativePath = item.relativePath;
|
const relativePath = item.relativePath;
|
||||||
|
|
||||||
return relativePath.toLowerCase();
|
return naturalExpansion(relativePath.toLowerCase());
|
||||||
},
|
},
|
||||||
|
|
||||||
series: function(item, direction) {
|
series: function(item, direction) {
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: 'Size',
|
label: 'Size',
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER,
|
||||||
|
valueType: filterBuilderValueTypes.BYTES
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'seeders',
|
name: 'seeders',
|
||||||
|
|||||||
@@ -44,7 +44,14 @@ function filter(items, state) {
|
|||||||
const predicate = filterPredicates[key];
|
const predicate = filterPredicates[key];
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
accepted = value.some((v) => predicate(item, v, type));
|
if (
|
||||||
|
type === filterTypes.NOT_CONTAINS ||
|
||||||
|
type === filterTypes.NOT_EQUAL
|
||||||
|
) {
|
||||||
|
accepted = value.every((v) => predicate(item, v, type));
|
||||||
|
} else {
|
||||||
|
accepted = value.some((v) => predicate(item, v, type));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
accepted = predicate(item, value, type);
|
accepted = predicate(item, value, type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class MoreInfo extends Component {
|
|||||||
|
|
||||||
<DescriptionListItemTitle>Twitter</DescriptionListItemTitle>
|
<DescriptionListItemTitle>Twitter</DescriptionListItemTitle>
|
||||||
<DescriptionListItemDescription>
|
<DescriptionListItemDescription>
|
||||||
<Link to="https://sonarr.tv/">@sonarrtv</Link>
|
<Link to="https://twitter.com/sonarrtv">@sonarrtv</Link>
|
||||||
</DescriptionListItemDescription>
|
</DescriptionListItemDescription>
|
||||||
|
|
||||||
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
|
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const regex = /\d+/g;
|
||||||
|
|
||||||
|
function naturalExpansion(input) {
|
||||||
|
if (!input) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.replace(regex, (n) => n.padStart(8, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default naturalExpansion;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
const regex = /\b\w+/g;
|
||||||
|
|
||||||
function titleCase(input) {
|
function titleCase(input) {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return input.replace(/\b\w+/g, (match) => {
|
return input.replace(regex, (match) => {
|
||||||
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ export function isMobile() {
|
|||||||
export function isIOS() {
|
export function isIOS() {
|
||||||
return mobileDetect.is('iOS');
|
return mobileDetect.is('iOS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFirefox() {
|
||||||
|
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
|
||||||
|
}
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://wiki.servarr.com/Sonarr_FAQ#Help_I_have_forgotten_my_password"
|
href="https://wiki.servarr.com/sonarr/faq#help-i-have-locked-myself-out"
|
||||||
class="forgot-password"
|
class="forgot-password"
|
||||||
>Forgot your password?</a
|
>Forgot your password?</a
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Ical.Net;
|
using Ical.Net;
|
||||||
|
using Ical.Net.CalendarComponents;
|
||||||
using Ical.Net.DataTypes;
|
using Ical.Net.DataTypes;
|
||||||
using Ical.Net.General;
|
|
||||||
using Ical.Net.Interfaces.Serialization;
|
|
||||||
using Ical.Net.Serialization;
|
using Ical.Net.Serialization;
|
||||||
using Ical.Net.Serialization.iCalendar.Factory;
|
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using Nancy.Responses;
|
using Nancy.Responses;
|
||||||
using NzbDrone.Core.Tags;
|
using NzbDrone.Core.Tags;
|
||||||
@@ -116,7 +114,7 @@ namespace NzbDrone.Api.Calendar
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var occurrence = calendar.Create<Event>();
|
var occurrence = calendar.Create<CalendarEvent>();
|
||||||
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
||||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||||
occurrence.Description = episode.Overview;
|
occurrence.Description = episode.Overview;
|
||||||
|
|||||||
@@ -30,11 +30,5 @@ namespace NzbDrone.Api.DownloadClient
|
|||||||
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
|
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
|
||||||
definition.RemoveFailedDownloads = resource.RemoveFailedDownloads;
|
definition.RemoveFailedDownloads = resource.RemoveFailedDownloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)
|
|
||||||
{
|
|
||||||
if (!definition.Enable) return;
|
|
||||||
base.Validate(definition, includeWarnings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,11 +30,5 @@ namespace NzbDrone.Api.Indexers
|
|||||||
definition.EnableInteractiveSearch = resource.EnableSearch;
|
definition.EnableInteractiveSearch = resource.EnableSearch;
|
||||||
definition.Priority = resource.Priority;
|
definition.Priority = resource.Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
|
|
||||||
{
|
|
||||||
if (!definition.Enable) return;
|
|
||||||
base.Validate(definition, includeWarnings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,5 @@ namespace NzbDrone.Api.Metadata
|
|||||||
|
|
||||||
definition.Enable = resource.Enable;
|
definition.Enable = resource.Enable;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Validate(MetadataDefinition definition, bool includeWarnings)
|
|
||||||
{
|
|
||||||
if (!definition.Enable) return;
|
|
||||||
base.Validate(definition, includeWarnings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,11 +38,5 @@ namespace NzbDrone.Api.Notifications
|
|||||||
definition.SupportsOnRename = resource.SupportsOnRename;
|
definition.SupportsOnRename = resource.SupportsOnRename;
|
||||||
definition.Tags = resource.Tags;
|
definition.Tags = resource.Tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Validate(NotificationDefinition definition, bool includeWarnings)
|
|
||||||
{
|
|
||||||
if (!definition.OnGrab && !definition.OnDownload) return;
|
|
||||||
base.Validate(definition, includeWarnings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ namespace NzbDrone.Api
|
|||||||
|
|
||||||
private int CreateProvider(TProviderResource providerResource)
|
private int CreateProvider(TProviderResource providerResource)
|
||||||
{
|
{
|
||||||
var providerDefinition = GetDefinition(providerResource, false);
|
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||||
|
|
||||||
if (providerDefinition.Enable)
|
if (providerDefinition.Enable)
|
||||||
{
|
{
|
||||||
@@ -88,18 +88,18 @@ namespace NzbDrone.Api
|
|||||||
|
|
||||||
private void UpdateProvider(TProviderResource providerResource)
|
private void UpdateProvider(TProviderResource providerResource)
|
||||||
{
|
{
|
||||||
var providerDefinition = GetDefinition(providerResource, false);
|
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||||
|
|
||||||
_providerFactory.Update(providerDefinition);
|
_providerFactory.Update(providerDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true)
|
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate)
|
||||||
{
|
{
|
||||||
var definition = new TProviderDefinition();
|
var definition = new TProviderDefinition();
|
||||||
|
|
||||||
MapToModel(definition, providerResource);
|
MapToModel(definition, providerResource);
|
||||||
|
|
||||||
if (validate)
|
if (validate && (definition.Enable || forceValidate))
|
||||||
{
|
{
|
||||||
Validate(definition, includeWarnings);
|
Validate(definition, includeWarnings);
|
||||||
}
|
}
|
||||||
@@ -170,19 +170,16 @@ namespace NzbDrone.Api
|
|||||||
|
|
||||||
private object Test(TProviderResource providerResource)
|
private object Test(TProviderResource providerResource)
|
||||||
{
|
{
|
||||||
// Don't validate when getting the definition so we can validate afterwards (avoids validation being skipped because the provider is disabled)
|
var providerDefinition = GetDefinition(providerResource, true, true, true);
|
||||||
var providerDefinition = GetDefinition(providerResource, true, false);
|
|
||||||
|
|
||||||
Validate(providerDefinition, true);
|
|
||||||
Test(providerDefinition, true);
|
Test(providerDefinition, true);
|
||||||
|
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private object RequestAction(string action, TProviderResource providerResource)
|
private object RequestAction(string action, TProviderResource providerResource)
|
||||||
{
|
{
|
||||||
var providerDefinition = GetDefinition(providerResource, true, false);
|
var providerDefinition = GetDefinition(providerResource, false, false, false);
|
||||||
|
|
||||||
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
||||||
|
|
||||||
@@ -192,7 +189,7 @@ namespace NzbDrone.Api
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
private void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||||
{
|
{
|
||||||
var validationResult = definition.Settings.Validate();
|
var validationResult = definition.Settings.Validate();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="8.4.0" />
|
<PackageReference Include="FluentValidation" Version="8.4.0" />
|
||||||
<PackageReference Include="Ical.Net" Version="2.2.32" />
|
<PackageReference Include="Ical.Net" Version="4.1.11" />
|
||||||
<PackageReference Include="Nancy" Version="2.0.0" />
|
<PackageReference Include="Nancy" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
|||||||
"/boot",
|
"/boot",
|
||||||
"/lib",
|
"/lib",
|
||||||
"/sbin",
|
"/sbin",
|
||||||
"/proc"
|
"/proc",
|
||||||
|
"/usr/bin"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var result = string.Format("Res: [{0}] {1}: {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode);
|
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
|
||||||
|
|
||||||
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
|
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
<PackageReference Include="NLog" Version="4.6.6" />
|
<PackageReference Include="NLog" Version="4.6.6" />
|
||||||
|
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||||
<PackageReference Include="Sentry" Version="1.2.0" />
|
<PackageReference Include="Sentry" Version="1.2.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
|
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -20,38 +20,50 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
private RemoteEpisode parseResultMulti;
|
private RemoteEpisode parseResultMulti;
|
||||||
private RemoteEpisode parseResultSingle;
|
private RemoteEpisode parseResultSingle;
|
||||||
private Series series;
|
private Series series;
|
||||||
|
private List<Episode> episodes;
|
||||||
private QualityDefinition qualityType;
|
private QualityDefinition qualityType;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
series = Builder<Series>.CreateNew()
|
series = Builder<Series>.CreateNew()
|
||||||
.Build();
|
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(2).Build().ToList())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
episodes = Builder<Episode>.CreateListOfSize(10)
|
||||||
|
.All()
|
||||||
|
.With(s => s.SeasonNumber = 1)
|
||||||
|
.BuildList();
|
||||||
|
|
||||||
parseResultMultiSet = new RemoteEpisode
|
parseResultMultiSet = new RemoteEpisode
|
||||||
{
|
{
|
||||||
Series = series,
|
Series = series,
|
||||||
Release = new ReleaseInfo(),
|
Release = new ReleaseInfo(),
|
||||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||||
Episodes = new List<Episode> { new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), new Episode() }
|
Episodes = Builder<Episode>.CreateListOfSize(6).All().With(s => s.SeasonNumber = 1).BuildList()
|
||||||
};
|
};
|
||||||
|
|
||||||
parseResultMulti = new RemoteEpisode
|
parseResultMulti = new RemoteEpisode
|
||||||
{
|
{
|
||||||
Series = series,
|
Series = series,
|
||||||
Release = new ReleaseInfo(),
|
Release = new ReleaseInfo(),
|
||||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||||
Episodes = new List<Episode> { new Episode(), new Episode() }
|
Episodes = Builder<Episode>.CreateListOfSize(2).All().With(s => s.SeasonNumber = 1).BuildList()
|
||||||
};
|
};
|
||||||
|
|
||||||
parseResultSingle = new RemoteEpisode
|
parseResultSingle = new RemoteEpisode
|
||||||
{
|
{
|
||||||
Series = series,
|
Series = series,
|
||||||
Release = new ReleaseInfo(),
|
Release = new ReleaseInfo(),
|
||||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||||
Episodes = new List<Episode> { new Episode() { Id = 2 } }
|
Episodes = new List<Episode> {
|
||||||
|
Builder<Episode>.CreateNew()
|
||||||
|
.With(s => s.SeasonNumber = 1)
|
||||||
|
.With(s => s.EpisodeNumber = 1)
|
||||||
|
.Build()
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IQualityDefinitionService>()
|
Mocker.GetMock<IQualityDefinitionService>()
|
||||||
.Setup(v => v.Get(It.IsAny<Quality>()))
|
.Setup(v => v.Get(It.IsAny<Quality>()))
|
||||||
@@ -67,18 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>().Setup(
|
Mocker.GetMock<IEpisodeService>().Setup(
|
||||||
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
||||||
.Returns(new List<Episode>() {
|
.Returns(episodes);
|
||||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
|
|
||||||
new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 }, new Episode() });
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GivenLastEpisode()
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IEpisodeService>().Setup(
|
|
||||||
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
|
||||||
.Returns(new List<Episode>() {
|
|
||||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
|
|
||||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(30, 50, false)]
|
[TestCase(30, 50, false)]
|
||||||
@@ -92,6 +93,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
series.Runtime = runtime;
|
series.Runtime = runtime;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().Id = 5;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||||
}
|
}
|
||||||
@@ -100,13 +102,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[TestCase(30, 1000, false)]
|
[TestCase(30, 1000, false)]
|
||||||
[TestCase(60, 1000, true)]
|
[TestCase(60, 1000, true)]
|
||||||
[TestCase(60, 2000, false)]
|
[TestCase(60, 2000, false)]
|
||||||
public void single_episode_first_or_last(int runtime, int sizeInMegaBytes, bool expectedResult)
|
public void should_return_expected_result_for_first_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
|
||||||
{
|
{
|
||||||
GivenLastEpisode();
|
|
||||||
|
|
||||||
series.Runtime = runtime;
|
series.Runtime = runtime;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().Id = episodes.First().Id;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(30, 500, true)]
|
||||||
|
[TestCase(30, 1000, false)]
|
||||||
|
[TestCase(60, 1000, true)]
|
||||||
|
[TestCase(60, 2000, false)]
|
||||||
|
public void should_return_expected_result_for_last_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
|
||||||
|
{
|
||||||
|
series.Runtime = runtime;
|
||||||
|
parseResultSingle.Series = series;
|
||||||
|
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().Id = episodes.Last().Id;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||||
}
|
}
|
||||||
@@ -144,8 +159,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_size_is_zero()
|
public void should_return_true_if_size_is_zero()
|
||||||
{
|
{
|
||||||
GivenLastEpisode();
|
|
||||||
|
|
||||||
series.Runtime = 30;
|
series.Runtime = 30;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Release.Size = 0;
|
parseResultSingle.Release.Size = 0;
|
||||||
@@ -158,8 +171,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_unlimited_30_minute()
|
public void should_return_true_if_unlimited_30_minute()
|
||||||
{
|
{
|
||||||
GivenLastEpisode();
|
|
||||||
|
|
||||||
series.Runtime = 30;
|
series.Runtime = 30;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Release.Size = 18457280000;
|
parseResultSingle.Release.Size = 18457280000;
|
||||||
@@ -171,8 +182,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_unlimited_60_minute()
|
public void should_return_true_if_unlimited_60_minute()
|
||||||
{
|
{
|
||||||
GivenLastEpisode();
|
|
||||||
|
|
||||||
series.Runtime = 60;
|
series.Runtime = 60;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Release.Size = 36857280000;
|
parseResultSingle.Release.Size = 36857280000;
|
||||||
@@ -184,8 +193,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_treat_daily_series_as_single_episode()
|
public void should_treat_daily_series_as_single_episode()
|
||||||
{
|
{
|
||||||
GivenLastEpisode();
|
|
||||||
|
|
||||||
series.Runtime = 60;
|
series.Runtime = 60;
|
||||||
parseResultSingle.Series = series;
|
parseResultSingle.Series = series;
|
||||||
parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
|
parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
|
||||||
@@ -216,5 +223,94 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
|
|
||||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_series_runtime_is_zero_and_single_episode_is_not_from_first_season()
|
||||||
|
{
|
||||||
|
series.Runtime = 0;
|
||||||
|
parseResultSingle.Series = series;
|
||||||
|
parseResultSingle.Episodes.First().Id = 5;
|
||||||
|
parseResultSingle.Release.Size = 200.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().SeasonNumber = 2;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_series_runtime_is_zero_and_single_episode_aired_more_than_24_hours_after_first_aired_episode()
|
||||||
|
{
|
||||||
|
series.Runtime = 0;
|
||||||
|
|
||||||
|
parseResultSingle.Series = series;
|
||||||
|
parseResultSingle.Release.Size = 200.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().Id = 5;
|
||||||
|
parseResultSingle.Episodes.First().SeasonNumber = 1;
|
||||||
|
parseResultSingle.Episodes.First().EpisodeNumber = 2;
|
||||||
|
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_series_runtime_is_zero_and_single_episode_aired_less_than_24_hours_after_first_aired_episode()
|
||||||
|
{
|
||||||
|
series.Runtime = 0;
|
||||||
|
|
||||||
|
parseResultSingle.Series = series;
|
||||||
|
parseResultSingle.Release.Size = 200.Megabytes();
|
||||||
|
parseResultSingle.Episodes.First().Id = 5;
|
||||||
|
parseResultSingle.Episodes.First().SeasonNumber = 1;
|
||||||
|
parseResultSingle.Episodes.First().EpisodeNumber = 2;
|
||||||
|
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_is_not_from_first_season()
|
||||||
|
{
|
||||||
|
series.Runtime = 0;
|
||||||
|
parseResultMulti.Series = series;
|
||||||
|
parseResultMulti.Release.Size = 200.Megabytes();
|
||||||
|
parseResultMulti.Episodes.ForEach(e => e.SeasonNumber = 2);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_aired_more_than_24_hours_after_first_aired_episode()
|
||||||
|
{
|
||||||
|
var airDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
|
||||||
|
|
||||||
|
series.Runtime = 0;
|
||||||
|
|
||||||
|
parseResultMulti.Series = series;
|
||||||
|
parseResultMulti.Release.Size = 200.Megabytes();
|
||||||
|
parseResultMulti.Episodes.ForEach(e =>
|
||||||
|
{
|
||||||
|
e.SeasonNumber = 1;
|
||||||
|
e.AirDateUtc = airDateUtc;
|
||||||
|
});
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_series_runtime_is_zero_and_multi_episode_aired_less_than_24_hours_after_first_aired_episode()
|
||||||
|
{
|
||||||
|
var airDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
|
||||||
|
|
||||||
|
series.Runtime = 0;
|
||||||
|
|
||||||
|
parseResultMulti.Series = series;
|
||||||
|
parseResultMulti.Release.Size = 200.Megabytes();
|
||||||
|
parseResultMulti.Episodes.ForEach(e =>
|
||||||
|
{
|
||||||
|
e.SeasonNumber = 1;
|
||||||
|
e.AirDateUtc = airDateUtc;
|
||||||
|
});
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Extras
|
|||||||
WithExistingFile(file);
|
WithExistingFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(_episodeFolder, SearchOption.AllDirectories))
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(_episodeFolder, It.IsAny<SearchOption>()))
|
||||||
.Returns(files.ToArray());
|
.Returns(files.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,5 +202,45 @@ namespace NzbDrone.Core.Test.Extras
|
|||||||
_subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Never());
|
_subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Never());
|
||||||
_otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Once());
|
_otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_search_subtitles_when_importing_from_job_folder()
|
||||||
|
{
|
||||||
|
_localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo();
|
||||||
|
|
||||||
|
var subtitleFile = Path.Combine(_episodeFolder, "Series.Title.S01E01.en.srt").AsOsAgnostic();
|
||||||
|
|
||||||
|
var files = new List<string> {
|
||||||
|
_localEpisode.Path,
|
||||||
|
subtitleFile
|
||||||
|
};
|
||||||
|
|
||||||
|
WithExistingFiles(files);
|
||||||
|
|
||||||
|
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_episodeFolder, SearchOption.AllDirectories), Times.Once);
|
||||||
|
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_episodeFolder, SearchOption.TopDirectoryOnly), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_search_subtitles_when_not_importing_from_job_folder()
|
||||||
|
{
|
||||||
|
_localEpisode.FolderEpisodeInfo = null;
|
||||||
|
|
||||||
|
var subtitleFile = Path.Combine(_episodeFolder, "Series.Title.S01E01.en.srt").AsOsAgnostic();
|
||||||
|
|
||||||
|
var files = new List<string> {
|
||||||
|
_localEpisode.Path,
|
||||||
|
subtitleFile
|
||||||
|
};
|
||||||
|
|
||||||
|
WithExistingFiles(files);
|
||||||
|
|
||||||
|
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_episodeFolder, SearchOption.AllDirectories), Times.Never);
|
||||||
|
Mocker.GetMock<IDiskProvider>().Verify(v => v.GetFiles(_episodeFolder, SearchOption.TopDirectoryOnly), Times.Once);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:nyaa="https://nyaa.si/xmlns/nyaa" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Nyaa - Home - Torrent File RSS</title>
|
||||||
|
<description>RSS Feed for Home</description>
|
||||||
|
<link>https://nyaa.si/</link>
|
||||||
|
<atom:link href="https://nyaa.si/?page=rss" rel="self" type="application/rss+xml"/>
|
||||||
|
<item>
|
||||||
|
<title>[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv</title>
|
||||||
|
<link>https://nyaa.si/download/1424896.torrent</link>
|
||||||
|
<guid isPermaLink="true">https://nyaa.si/view/1424896</guid>
|
||||||
|
<pubDate>Tue, 24 Aug 2021 22:18:46 -0000</pubDate>
|
||||||
|
<nyaa:seeders>4</nyaa:seeders>
|
||||||
|
<nyaa:leechers>3</nyaa:leechers>
|
||||||
|
<nyaa:downloads>2</nyaa:downloads>
|
||||||
|
<nyaa:infoHash>e8ca5e20eca876339f41c3d9e95ea66c1d7caaee</nyaa:infoHash>
|
||||||
|
<nyaa:categoryId>1_3</nyaa:categoryId>
|
||||||
|
<nyaa:category>Anime - Non-English-translated</nyaa:category>
|
||||||
|
<nyaa:size>609.6 MiB</nyaa:size>
|
||||||
|
<nyaa:comments>0</nyaa:comments>
|
||||||
|
<nyaa:trusted>No</nyaa:trusted>
|
||||||
|
<nyaa:remake>No</nyaa:remake>
|
||||||
|
<description>
|
||||||
|
<![CDATA[ <a href="https://nyaa.si/view/1424896">#1424896 | [Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv</a> | 609.6 MiB | Anime - Non-English-translated | E8CA5E20ECA876339F41C3D9E95EA66C1D7CAAEE ]]>
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Macross Zero (BDRip 1920x1080p x265 HEVC TrueHD, FLAC 5.1+2.0)[sxales]</title>
|
||||||
|
<link>https://nyaa.si/download/1424895.torrent</link>
|
||||||
|
<guid isPermaLink="true">https://nyaa.si/view/1424895</guid>
|
||||||
|
<pubDate>Tue, 24 Aug 2021 22:03:11 -0000</pubDate>
|
||||||
|
<nyaa:seeders>23</nyaa:seeders>
|
||||||
|
<nyaa:leechers>32</nyaa:leechers>
|
||||||
|
<nyaa:downloads>17</nyaa:downloads>
|
||||||
|
<nyaa:infoHash>26f37f26d5b3475b41a98dc575fabfa6f8d32a76</nyaa:infoHash>
|
||||||
|
<nyaa:categoryId>1_2</nyaa:categoryId>
|
||||||
|
<nyaa:category>Anime - English-translated</nyaa:category>
|
||||||
|
<nyaa:size>5.7 GiB</nyaa:size>
|
||||||
|
<nyaa:comments>2</nyaa:comments>
|
||||||
|
<nyaa:trusted>No</nyaa:trusted>
|
||||||
|
<nyaa:remake>No</nyaa:remake>
|
||||||
|
<description>
|
||||||
|
<![CDATA[ <a href="https://nyaa.si/view/1424895">#1424895 | Macross Zero (BDRip 1920x1080p x265 HEVC TrueHD, FLAC 5.1+2.0)[sxales]</a> | 5.7 GiB | Anime - English-translated | 26F37F26D5B3475B41A98DC575FABFA6F8D32A76 ]]>
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Fumetsu no Anata e - 19 [WEBDL 1080p] Ukr DVO</title>
|
||||||
|
<link>https://nyaa.si/download/1424887.torrent</link>
|
||||||
|
<guid isPermaLink="true">https://nyaa.si/view/1424887</guid>
|
||||||
|
<pubDate>Tue, 24 Aug 2021 21:23:06 -0000</pubDate>
|
||||||
|
<nyaa:seeders>5</nyaa:seeders>
|
||||||
|
<nyaa:leechers>4</nyaa:leechers>
|
||||||
|
<nyaa:downloads>4</nyaa:downloads>
|
||||||
|
<nyaa:infoHash>3e4300e24b39983802162877755aab4380bd137a</nyaa:infoHash>
|
||||||
|
<nyaa:categoryId>1_3</nyaa:categoryId>
|
||||||
|
<nyaa:category>Anime - Non-English-translated</nyaa:category>
|
||||||
|
<nyaa:size>1.4 GiB</nyaa:size>
|
||||||
|
<nyaa:comments>0</nyaa:comments>
|
||||||
|
<nyaa:trusted>No</nyaa:trusted>
|
||||||
|
<nyaa:remake>No</nyaa:remake>
|
||||||
|
<description>
|
||||||
|
<![CDATA[ <a href="https://nyaa.si/view/1424887">#1424887 | Fumetsu no Anata e - 19 [WEBDL 1080p] Ukr DVO</a> | 1.4 GiB | Anime - Non-English-translated | 3E4300E24B39983802162877755AAB4380BD137A ]]>
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -147,7 +147,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
var pages = results.GetTier(0).Take(2).Select(t => t.First()).ToList();
|
var pages = results.GetTier(0).Take(2).Select(t => t.First()).ToList();
|
||||||
|
|
||||||
pages[0].Url.FullUri.Should().Contain("q=Monkey%20Island+100");
|
pages[0].Url.FullUri.Should().Contain("q=Monkey%20Island+100");
|
||||||
pages[1].Url.FullUri.Should().Contain("q=Monkey%20Island+s05e04");
|
pages[1].Url.FullUri.Should().Contain("q=Monkey%20Island&season=5&ep=4");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -339,5 +339,40 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
pageTier2.Url.Query.Should().NotContain("rid=10");
|
pageTier2.Url.Query.Should().NotContain("rid=10");
|
||||||
pageTier2.Url.Query.Should().Contain("q=");
|
pageTier2.Url.Query.Should().Contain("q=");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_encode_raw_title()
|
||||||
|
{
|
||||||
|
_capabilities.SupportedTvSearchParameters = new[] { "q", "season", "ep" };
|
||||||
|
_capabilities.TvTextSearchEngine = "raw";
|
||||||
|
_singleEpisodeSearchCriteria.SceneTitles[0] = "Edith & Little";
|
||||||
|
|
||||||
|
var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria);
|
||||||
|
results.Tiers.Should().Be(1);
|
||||||
|
|
||||||
|
var pageTier = results.GetTier(0).First().First();
|
||||||
|
|
||||||
|
pageTier.Url.Query.Should().Contain("q=Edith%20%26%20Little");
|
||||||
|
pageTier.Url.Query.Should().NotContain(" & ");
|
||||||
|
pageTier.Url.Query.Should().Contain("%26");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_clean_title_and_encode()
|
||||||
|
{
|
||||||
|
_capabilities.SupportedTvSearchParameters = new[] { "q", "season", "ep" };
|
||||||
|
_capabilities.TvTextSearchEngine = "sphinx";
|
||||||
|
_singleEpisodeSearchCriteria.SceneTitles[0] = "Edith & Little";
|
||||||
|
|
||||||
|
var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria);
|
||||||
|
results.Tiers.Should().Be(1);
|
||||||
|
|
||||||
|
var pageTier = results.GetTier(0).First().First();
|
||||||
|
|
||||||
|
pageTier.Url.Query.Should().Contain("q=Edith%20and%20Little");
|
||||||
|
pageTier.Url.Query.Should().Contain("and");
|
||||||
|
pageTier.Url.Query.Should().NotContain(" & ");
|
||||||
|
pageTier.Url.Query.Should().NotContain("%26");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
|||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
Subject.Definition = new IndexerDefinition()
|
Subject.Definition = new IndexerDefinition()
|
||||||
{
|
{
|
||||||
Name = "Nyaa",
|
Name = "Nyaa",
|
||||||
Settings = new NyaaSettings()
|
Settings = new NyaaSettings()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
/* [Test]
|
||||||
|
// Legacy Nyaa feed test
|
||||||
|
|
||||||
public void should_parse_recent_feed_from_Nyaa()
|
public void should_parse_recent_feed_from_Nyaa()
|
||||||
{
|
{
|
||||||
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
|
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
|
||||||
@@ -50,8 +52,37 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
|
|||||||
torrentInfo.Size.Should().Be(2523293286); //2.35 GiB
|
torrentInfo.Size.Should().Be(2523293286); //2.35 GiB
|
||||||
torrentInfo.InfoHash.Should().Be(null);
|
torrentInfo.InfoHash.Should().Be(null);
|
||||||
torrentInfo.MagnetUrl.Should().Be(null);
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
torrentInfo.Peers.Should().Be(2+1);
|
torrentInfo.Peers.Should().Be(2 + 1);
|
||||||
torrentInfo.Seeders.Should().Be(1);
|
torrentInfo.Seeders.Should().Be(1);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_parse_2021_recent_feed_from_Nyaa()
|
||||||
|
{
|
||||||
|
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa2021.xml");
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
|
||||||
|
|
||||||
|
var releases = Subject.FetchRecent();
|
||||||
|
|
||||||
|
releases.Should().HaveCount(3);
|
||||||
|
releases.First().Should().BeOfType<TorrentInfo>();
|
||||||
|
|
||||||
|
var torrentInfo = releases.First() as TorrentInfo;
|
||||||
|
|
||||||
|
torrentInfo.Title.Should().Be("[Foxy-Subs] Mahouka Koukou no Yuutousei - 08 [720p] [3194D881].mkv");
|
||||||
|
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||||
|
torrentInfo.DownloadUrl.Should().Be("https://nyaa.si/download/1424896.torrent");
|
||||||
|
torrentInfo.InfoUrl.Should().Be("https://nyaa.si/view/1424896");
|
||||||
|
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||||
|
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||||
|
torrentInfo.PublishDate.Should().Be(DateTime.Parse("Tue, 24 Aug 2021 22:18:46"));
|
||||||
|
torrentInfo.Size.Should().Be(639211930); //609.6 MiB
|
||||||
|
torrentInfo.MagnetUrl.Should().Be(null);
|
||||||
|
torrentInfo.Seeders.Should().Be(4);
|
||||||
|
torrentInfo.Peers.Should().Be(3+4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_reject_if_episode_title_is_required_for_bulk_season_releases_and_it_is_mising()
|
public void should_reject_if_episode_title_is_required_for_bulk_season_releases_and_it_is_missing()
|
||||||
{
|
{
|
||||||
_localEpisode.Episodes.First().Title = "TBA";
|
_localEpisode.Episodes.First().Title = "TBA";
|
||||||
|
|
||||||
@@ -154,5 +154,28 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_reject_if_episode_title_is_required_for_bulk_season_releases_and_some_episodes_do_not_have_air_date()
|
||||||
|
{
|
||||||
|
_localEpisode.Episodes.First().Title = "TBA";
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(s => s.EpisodeTitleRequired)
|
||||||
|
.Returns(EpisodeTitleRequiredType.BulkSeasonReleases);
|
||||||
|
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
||||||
|
.Returns(Builder<Episode>.CreateListOfSize(5)
|
||||||
|
.All()
|
||||||
|
.With(e => e.Title = "TBA")
|
||||||
|
.With(e => e.AirDateUtc = null)
|
||||||
|
.TheFirst(1)
|
||||||
|
.With(e => e.AirDateUtc = _localEpisode.Episodes.First().AirDateUtc)
|
||||||
|
.BuildList());
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -754,19 +754,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
.Should().Be("[SonarrTest]South.Park.100");
|
.Should().Be("[SonarrTest]South.Park.100");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_able_to_use_original_filename()
|
|
||||||
{
|
|
||||||
_series.Title = "30 Rock";
|
|
||||||
_namingConfig.StandardEpisodeFormat = "{Series Title} - {Original Filename}";
|
|
||||||
|
|
||||||
_episodeFile.SceneName = "30.Rock.S01E01.xvid-LOL";
|
|
||||||
_episodeFile.RelativePath = "30 Rock - S01E01 - Test";
|
|
||||||
|
|
||||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
|
||||||
.Should().Be("30 Rock - 30 Rock - S01E01 - Test");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_be_able_to_use_original_filename_only()
|
public void should_be_able_to_use_original_filename_only()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Organizer;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class OriginalTitleFixture : CoreTest<FileNameBuilder>
|
||||||
|
{
|
||||||
|
private Series _series;
|
||||||
|
private Episode _episode;
|
||||||
|
private EpisodeFile _episodeFile;
|
||||||
|
private NamingConfig _namingConfig;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_series = Builder<Series>
|
||||||
|
.CreateNew()
|
||||||
|
.With(s => s.Title = "My Series")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_episode = Builder<Episode>.CreateNew()
|
||||||
|
.With(e => e.Title = "City Sushi")
|
||||||
|
.With(e => e.SeasonNumber = 15)
|
||||||
|
.With(e => e.EpisodeNumber = 6)
|
||||||
|
.With(e => e.AbsoluteEpisodeNumber = 100)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_episodeFile = new EpisodeFile { Id = 5, Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
|
||||||
|
|
||||||
|
_namingConfig = NamingConfig.Default;
|
||||||
|
_namingConfig.RenameEpisodes = true;
|
||||||
|
|
||||||
|
Mocker.GetMock<INamingConfigService>()
|
||||||
|
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||||
|
|
||||||
|
Mocker.GetMock<IQualityDefinitionService>()
|
||||||
|
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||||
|
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_original_title_if_not_current_file_name()
|
||||||
|
{
|
||||||
|
_episodeFile.SceneName = "my.series.s15e06";
|
||||||
|
_episodeFile.RelativePath = "My Series - S15E06 - City Sushi";
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - S15E06 - City Sushi [my.series.s15e06]");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_if_not_renaming_files()
|
||||||
|
{
|
||||||
|
_episodeFile.SceneName = "my.series.s15e06";
|
||||||
|
_namingConfig.RenameEpisodes = false;
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("my.series.s15e06");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_if_not_including_season_and_episode_tokens_for_standard_series()
|
||||||
|
{
|
||||||
|
_episodeFile.RelativePath = "My Series - S15E06 - City Sushi";
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Original Title} {Quality Title}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - S15E06 - City Sushi HDTV-720p");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_if_not_including_air_date_token_for_daily_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Daily;
|
||||||
|
_episode.AirDate = "2022-04-28";
|
||||||
|
_episodeFile.RelativePath = "My Series - 2022-04-28 - City Sushi";
|
||||||
|
_namingConfig.DailyEpisodeFormat = "{Original Title} {Quality Title}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 2022-04-28 - City Sushi HDTV-720p");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_if_not_including_absolute_episode_number_token_for_anime_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Anime;
|
||||||
|
_episode.AbsoluteEpisodeNumber = 123;
|
||||||
|
_episodeFile.RelativePath = "My Series - 123 - City Sushi";
|
||||||
|
_namingConfig.AnimeEpisodeFormat = "{Original Title} {Quality Title}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 123 - City Sushi HDTV-720p");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_include_current_filename_if_including_season_and_episode_tokens_for_standard_series()
|
||||||
|
{
|
||||||
|
_episodeFile.RelativePath = "My Series - S15E06 - City Sushi";
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - S15E06");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_include_current_filename_if_including_air_date_token_for_daily_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Daily;
|
||||||
|
_episode.AirDate = "2022-04-28";
|
||||||
|
_episodeFile.RelativePath = "My Series - 2022-04-28 - City Sushi";
|
||||||
|
_namingConfig.DailyEpisodeFormat = "{Series Title} - {Air-Date} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 2022-04-28");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_include_current_filename_if_including_absolute_episode_number_token_for_anime_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Anime;
|
||||||
|
_episode.AbsoluteEpisodeNumber = 123;
|
||||||
|
_episodeFile.RelativePath = "My Series - 123 - City Sushi";
|
||||||
|
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:00} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_for_new_file_if_including_season_and_episode_tokens_for_standard_series()
|
||||||
|
{
|
||||||
|
_episodeFile.Id = 0;
|
||||||
|
_episodeFile.RelativePath = "My Series - S15E06 - City Sushi";
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - S15E06 [My Series - S15E06 - City Sushi]");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_for_new_file_if_including_air_date_token_for_daily_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Daily;
|
||||||
|
_episode.AirDate = "2022-04-28";
|
||||||
|
_episodeFile.Id = 0;
|
||||||
|
_episodeFile.RelativePath = "My Series - 2022-04-28 - City Sushi";
|
||||||
|
_namingConfig.DailyEpisodeFormat = "{Series Title} - {Air-Date} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 2022-04-28 [My Series - 2022-04-28 - City Sushi]");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_current_filename_for_new_file_if_including_absolute_episode_number_token_for_anime_series()
|
||||||
|
{
|
||||||
|
_series.SeriesType = SeriesTypes.Anime;
|
||||||
|
_episode.AbsoluteEpisodeNumber = 123;
|
||||||
|
_episodeFile.Id = 0;
|
||||||
|
_episodeFile.RelativePath = "My Series - 123 - City Sushi";
|
||||||
|
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:00} {[Original Title]}";
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
|
||||||
|
.Should().Be("My Series - 123 [My Series - 123 - City Sushi]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,5 +99,11 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
{
|
{
|
||||||
Parser.Parser.ParseTitle(fileName).Should().BeNull();
|
Parser.Parser.ParseTitle(fileName).Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("Specials/Series - Episode Title (part 1)")]
|
||||||
|
public void should_not_parse_special_with_part_number(string fileName)
|
||||||
|
{
|
||||||
|
Parser.Parser.ParseTitle(fileName).Should().BeNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,17 +53,24 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.French.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.French.HDTV.XviD-LOL")]
|
||||||
[TestCase("Title.the.Series.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD")]
|
[TestCase("Title.the.Series.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD")]
|
||||||
|
[TestCase("Title.S01.720p.VF.WEB-DL.AAC2.0.H.264-BTN")]
|
||||||
|
[TestCase("Title.S01.720p.VF2.WEB-DL.AAC2.0.H.264-BTN")]
|
||||||
|
[TestCase("Title.S01.720p.VFF.WEB-DL.AAC2.0.H.264-BTN")]
|
||||||
|
[TestCase("Title.S01.720p.VFQ.WEB-DL.AAC2.0.H.264-BTN")]
|
||||||
|
[TestCase("Title.S01.720p.TRUEFRENCH.WEB-DL.AAC2.0.H.264-BTN")]
|
||||||
public void should_parse_language_french(string postTitle)
|
public void should_parse_language_french(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.French.Id);
|
result.Id.Should().Be(Language.French.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Spanish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Spanish.HDTV.XviD-LOL")]
|
||||||
|
[TestCase("Series Title - Temporada 1 [HDTV 720p][Cap.101][AC3 5.1 Castellano][www.pctnew.ORG]")]
|
||||||
|
[TestCase("Series Title - Temporada 2 [HDTV 720p][Cap.206][AC3 5.1 Español Castellano]")]
|
||||||
public void should_parse_language_spanish(string postTitle)
|
public void should_parse_language_spanish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Spanish.Id);
|
result.Id.Should().Be(Language.Spanish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.German.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.German.HDTV.XviD-LOL")]
|
||||||
@@ -72,45 +79,45 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Series.Title.S01E03.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")]
|
[TestCase("Series.Title.S01E03.Ger.Dub.AAC.1080p.WebDL.x264-TKP21")]
|
||||||
public void should_parse_language_german(string postTitle)
|
public void should_parse_language_german(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.German.Id);
|
result.Id.Should().Be(Language.German.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Italian.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Italian.HDTV.XviD-LOL")]
|
||||||
[TestCase("Title.the.Series.1x19.ita.720p.bdmux.x264-novarip")]
|
[TestCase("Title.the.Series.1x19.ita.720p.bdmux.x264-novarip")]
|
||||||
public void should_parse_language_italian(string postTitle)
|
public void should_parse_language_italian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Italian.Id);
|
result.Id.Should().Be(Language.Italian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Danish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Danish.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_danish(string postTitle)
|
public void should_parse_language_danish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Danish.Id);
|
result.Id.Should().Be(Language.Danish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Dutch.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Dutch.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_dutch(string postTitle)
|
public void should_parse_language_dutch(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Dutch.Id);
|
result.Id.Should().Be(Language.Dutch.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Japanese.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Japanese.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_japanese(string postTitle)
|
public void should_parse_language_japanese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Japanese.Id);
|
result.Id.Should().Be(Language.Japanese.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Icelandic.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Icelandic.HDTV.XviD-LOL")]
|
||||||
[TestCase("Title.the.Series.S01E03.1080p.WEB-DL.DD5.1.H.264-SbR Icelandic")]
|
[TestCase("Title.the.Series.S01E03.1080p.WEB-DL.DD5.1.H.264-SbR Icelandic")]
|
||||||
public void should_parse_language_icelandic(string postTitle)
|
public void should_parse_language_icelandic(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Icelandic.Id);
|
result.Id.Should().Be(Language.Icelandic.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Chinese.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Chinese.HDTV.XviD-LOL")]
|
||||||
@@ -128,23 +135,23 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[喵萌奶茶屋&LoliHouse] 拳愿阿修罗 / Kengan Ashura - 17 [WebRip 1080p HEVC-10bit AAC][中日双语字幕]")]
|
[TestCase("[喵萌奶茶屋&LoliHouse] 拳愿阿修罗 / Kengan Ashura - 17 [WebRip 1080p HEVC-10bit AAC][中日双语字幕]")]
|
||||||
public void should_parse_language_chinese(string postTitle)
|
public void should_parse_language_chinese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Chinese.Id);
|
result.Id.Should().Be(Language.Chinese.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Korean.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Korean.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_korean(string postTitle)
|
public void should_parse_language_korean(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Korean.Id);
|
result.Id.Should().Be(Language.Korean.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Russian.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Russian.HDTV.XviD-LOL")]
|
||||||
[TestCase("Title.the.Series.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike")]
|
[TestCase("Title.the.Series.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike")]
|
||||||
public void should_parse_language_russian(string postTitle)
|
public void should_parse_language_russian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Russian.Id);
|
result.Id.Should().Be(Language.Russian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Polish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Polish.HDTV.XviD-LOL")]
|
||||||
@@ -159,64 +166,64 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Title.the.Series.2009.S01E14.DUB-PL.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.DUB-PL.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_polish(string postTitle)
|
public void should_parse_language_polish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Polish.Id);
|
result.Id.Should().Be(Language.Polish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Vietnamese.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Vietnamese.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_vietnamese(string postTitle)
|
public void should_parse_language_vietnamese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Vietnamese.Id);
|
result.Id.Should().Be(Language.Vietnamese.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Swedish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Swedish.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_swedish(string postTitle)
|
public void should_parse_language_swedish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Swedish.Id);
|
result.Id.Should().Be(Language.Swedish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Norwegian.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Norwegian.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_norwegian(string postTitle)
|
public void should_parse_language_norwegian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Norwegian.Id);
|
result.Id.Should().Be(Language.Norwegian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Finnish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Finnish.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_finnish(string postTitle)
|
public void should_parse_language_finnish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Finnish.Id);
|
result.Id.Should().Be(Language.Finnish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Turkish.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Turkish.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_turkish(string postTitle)
|
public void should_parse_language_turkish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Turkish.Id);
|
result.Id.Should().Be(Language.Turkish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Portuguese.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Portuguese.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_portuguese(string postTitle)
|
public void should_parse_language_portuguese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Portuguese.Id);
|
result.Id.Should().Be(Language.Portuguese.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.S01E01.FLEMISH.HDTV.x264-BRiGAND")]
|
[TestCase("Title.the.Series.S01E01.FLEMISH.HDTV.x264-BRiGAND")]
|
||||||
public void should_parse_language_flemish(string postTitle)
|
public void should_parse_language_flemish(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Flemish.Id);
|
result.Id.Should().Be(Language.Flemish.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.S03E13.Greek.PDTV.XviD-Ouzo")]
|
[TestCase("Title.the.Series.S03E13.Greek.PDTV.XviD-Ouzo")]
|
||||||
public void should_parse_language_greek(string postTitle)
|
public void should_parse_language_greek(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Greek.Id);
|
result.Id.Should().Be(Language.Greek.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.HDTV.XviD.HUNDUB-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.HDTV.XviD.HUNDUB-LOL")]
|
||||||
@@ -224,44 +231,44 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Title.the.Series.2009.S01E14.HDTV.XviD.HUN-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.HDTV.XviD.HUN-LOL")]
|
||||||
public void should_parse_language_hungarian(string postTitle)
|
public void should_parse_language_hungarian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Hungarian.Id);
|
result.Id.Should().Be(Language.Hungarian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.S01-03.DVDRip.HebDub")]
|
[TestCase("Title.the.Series.S01-03.DVDRip.HebDub")]
|
||||||
public void should_parse_language_hebrew(string postTitle)
|
public void should_parse_language_hebrew(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Hebrew.Id);
|
result.Id.Should().Be(Language.Hebrew.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.S05E01.WEBRip.x264.AC3.LT.EN-CNN")]
|
[TestCase("Title.the.Series.S05E01.WEBRip.x264.AC3.LT.EN-CNN")]
|
||||||
public void should_parse_language_lithuanian(string postTitle)
|
public void should_parse_language_lithuanian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Lithuanian.Id);
|
result.Id.Should().Be(Language.Lithuanian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.S07E11.WEB Rip.XviD.Louige-CZ.EN.5.1")]
|
[TestCase("Title.the.Series.S07E11.WEB Rip.XviD.Louige-CZ.EN.5.1")]
|
||||||
public void should_parse_language_czech(string postTitle)
|
public void should_parse_language_czech(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Czech.Id);
|
result.Id.Should().Be(Language.Czech.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Series Title.S01.ARABIC.COMPLETE.720p.NF.WEBRip.x264-PTV")]
|
[TestCase("Series Title.S01.ARABIC.COMPLETE.720p.NF.WEBRip.x264-PTV")]
|
||||||
public void should_parse_language_arabic(string postTitle)
|
public void should_parse_language_arabic(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Arabic.Id);
|
result.Id.Should().Be(Language.Arabic.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("The Shadow Series S01 E01-08 WebRip Dual Audio [Hindi 5.1 + English 5.1] 720p x264 AAC ESub")]
|
[TestCase("The Shadow Series S01 E01-08 WebRip Dual Audio [Hindi 5.1 + English 5.1] 720p x264 AAC ESub")]
|
||||||
[TestCase("The Final Sonarr (2020) S04 Complete 720p NF WEBRip [Hindi+English] Dual audio")]
|
[TestCase("The Final Sonarr (2020) S04 Complete 720p NF WEBRip [Hindi+English] Dual audio")]
|
||||||
public void should_parse_language_hindi(string postTitle)
|
public void should_parse_language_hindi(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Hindi.Id);
|
result.Id.Should().Be(Language.Hindi.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2009.S01E14.Bulgarian.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.Bulgarian.HDTV.XviD-LOL")]
|
||||||
@@ -269,8 +276,26 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Title.the.Series.2009.S01E14.BG.AUDIO.HDTV.XviD-LOL")]
|
[TestCase("Title.the.Series.2009.S01E14.BG.AUDIO.HDTV.XviD-LOL")]
|
||||||
public void should_parse_language_bulgarian(string postTitle)
|
public void should_parse_language_bulgarian(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Id.Should().Be(Language.Bulgarian.Id);
|
result.Id.Should().Be(Language.Bulgarian.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("Series Title S01E01 Malayalam.1080p.WebRip.AVC.5.1-Rjaa")]
|
||||||
|
[TestCase("Series Title S01E01 Malayalam DVDRip XviD 5.1 ESub MTR")]
|
||||||
|
[TestCase("Series.Title.S01E01.DVDRip.1CD.Malayalam.Xvid.MP3 @Mastitorrents")]
|
||||||
|
public void should_parse_language_malayalam(string postTitle)
|
||||||
|
{
|
||||||
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
|
result.Id.Should().Be(Language.Malayalam.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("Гало(Сезон 1, серії 1-5) / SeriesTitle(Season 1, episodes 1-5) (2022) WEBRip-AVC Ukr/Eng")]
|
||||||
|
[TestCase("Архів 81 (Сезон 1) / Series 81 (Season 1) (2022) WEB-DLRip-AVC Ukr/Eng | Sub Ukr/Eng")]
|
||||||
|
[TestCase("Книга Боби Фетта(Сезон 1) / Series Title(Season 1) (2021) WEB-DLRip Ukr/Eng")]
|
||||||
|
public void should_parse_language_ukrainian(string postTitle)
|
||||||
|
{
|
||||||
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
|
result.Id.Should().Be(Language.Ukrainian.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Russian.Series.S01E07.Cold.Action.HDTV.XviD-Droned")]
|
[TestCase("Title.the.Russian.Series.S01E07.Cold.Action.HDTV.XviD-Droned")]
|
||||||
@@ -280,8 +305,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Title The Spanish S02E02 Flodden 720p AMZN WEB-DL DDP5 1 H 264-NTb")]
|
[TestCase("Title The Spanish S02E02 Flodden 720p AMZN WEB-DL DDP5 1 H 264-NTb")]
|
||||||
public void should_not_parse_series_or_episode_title(string postTitle)
|
public void should_not_parse_series_or_episode_title(string postTitle)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = LanguageParser.ParseLanguage(postTitle);
|
||||||
result.Language.Name.Should().Be(Language.English.Name);
|
result.Name.Should().Be(Language.English.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,5 +518,46 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
|||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
.Verify(v => v.GetEpisodesBySceneSeason(It.IsAny<int>(), It.IsAny<int>()), Times.Once);
|
.Verify(v => v.GetEpisodesBySceneSeason(It.IsAny<int>(), It.IsAny<int>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_season_zero_when_looking_up_is_partial_special_episode_found_by_title()
|
||||||
|
{
|
||||||
|
_series.UseSceneNumbering = false;
|
||||||
|
_parsedEpisodeInfo.SeasonNumber = 1;
|
||||||
|
_parsedEpisodeInfo.EpisodeNumbers = new int[] { 0 };
|
||||||
|
_parsedEpisodeInfo.ReleaseTitle = "Series.Title.S01E00.My.Special.Episode.1080p.AMZN.WEB-DL.DDP5.1.H264-TEPES";
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||||
|
.Returns(
|
||||||
|
Builder<Episode>.CreateNew()
|
||||||
|
.With(e => e.SeasonNumber = 0)
|
||||||
|
.With(e => e.EpisodeNumber = 1)
|
||||||
|
.Build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_original_parse_result_when_special_episode_lookup_by_title_fails()
|
||||||
|
{
|
||||||
|
_series.UseSceneNumbering = false;
|
||||||
|
_parsedEpisodeInfo.SeasonNumber = 1;
|
||||||
|
_parsedEpisodeInfo.EpisodeNumbers = new int[] { 0 };
|
||||||
|
_parsedEpisodeInfo.ReleaseTitle = "Series.Title.S01E00.My.Special.Episode.1080p.AMZN.WEB-DL.DDP5.1.H264-TEPES";
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||||
|
.Returns((Episode)null);
|
||||||
|
|
||||||
|
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][1080p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][1080p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||||
[TestCase("[LostYears] Series Title - 01-17 (WEB 1080p x264 10-bit AAC) [Dual-Audio]", false)]
|
[TestCase("[LostYears] Series Title - 01-17 (WEB 1080p x264 10-bit AAC) [Dual-Audio]", false)]
|
||||||
[TestCase("Series.and.Titles.S01.1080p.NF.WEB.DD2.0.x264-SNEAkY", false)]
|
[TestCase("Series.and.Titles.S01.1080p.NF.WEB.DD2.0.x264-SNEAkY", false)]
|
||||||
|
[TestCase("Series.Title.S02E02.This.Year.Will.Be.Different.1080p.WEB.H 265", false)]
|
||||||
public void should_parse_webdl1080p_quality(string title, bool proper)
|
public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
||||||
@@ -267,6 +268,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("House.of.Sonarr.AK.s05e13.UHD.4K.WEB.DL", false)]
|
[TestCase("House.of.Sonarr.AK.s05e13.UHD.4K.WEB.DL", false)]
|
||||||
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][2160p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][2160p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||||
[TestCase("Series Title S02 2013 WEB-DL 4k H265 AAC 2Audio-HDSWEB", false)]
|
[TestCase("Series Title S02 2013 WEB-DL 4k H265 AAC 2Audio-HDSWEB", false)]
|
||||||
|
[TestCase("Series.Title.S02E02.This.Year.Will.Be.Different.2160p.WEB.H.265", false)]
|
||||||
public void should_parse_webdl2160p_quality(string title, bool proper)
|
public void should_parse_webdl2160p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.WEBDL2160p, proper);
|
ParseAndVerifyQuality(title, Quality.WEBDL2160p, proper);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Series.SAISON.1.VFQ.PDTV.H264-ACC-ROLLED", "Series", 1)]
|
[TestCase("Series.SAISON.1.VFQ.PDTV.H264-ACC-ROLLED", "Series", 1)]
|
||||||
[TestCase("Series Title - Series 1 (1970) DivX", "Series Title", 1)]
|
[TestCase("Series Title - Series 1 (1970) DivX", "Series Title", 1)]
|
||||||
[TestCase("SeriesTitle.S03.540p.AMZN.WEB-DL.DD+2.0.x264-RTN", "SeriesTitle", 3)]
|
[TestCase("SeriesTitle.S03.540p.AMZN.WEB-DL.DD+2.0.x264-RTN", "SeriesTitle", 3)]
|
||||||
|
[TestCase("Series.Title.S01.576p.BluRay.DD5.1.x264-HiSD", "Series Title", 1)]
|
||||||
public void should_parse_full_season_release(string postTitle, string title, int season)
|
public void should_parse_full_season_release(string postTitle, string title, int season)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Series Title - S02E01 1920x910", "Series Title", 2, 1)]
|
[TestCase("Series Title - S02E01 1920x910", "Series Title", 2, 1)]
|
||||||
[TestCase("Anime Title - S2020E1527 [1527] [2020-10-11] - Episode Title", "Anime Title", 2020, 1527)]
|
[TestCase("Anime Title - S2020E1527 [1527] [2020-10-11] - Episode Title", "Anime Title", 2020, 1527)]
|
||||||
[TestCase("Anime Title - S2010E994 [0994] [2010-02-28] - Episode Title [x264 720p][AAC 2ch][HS][Shion+GakiDave]", "Anime Title", 2010, 994)]
|
[TestCase("Anime Title - S2010E994 [0994] [2010-02-28] - Episode Title [x264 720p][AAC 2ch][HS][Shion+GakiDave]", "Anime Title", 2010, 994)]
|
||||||
|
[TestCase("Series Title - Temporada 2 [HDTV 720p][Cap.201][AC3 5.1 Castellano][www.pctnew.com]", "Series Title", 2, 1)]
|
||||||
|
[TestCase("Series Title - Temporada 2 [HDTV 720p][Cap.1901][AC3 5.1 Castellano][www.pctnew.com]", "Series Title", 19, 1)]
|
||||||
|
[TestCase("Series Title 1x1", "Series Title", 1, 1)]
|
||||||
|
[TestCase("1x1", "", 1, 1)]
|
||||||
//[TestCase("", "", 0, 0)]
|
//[TestCase("", "", 0, 0)]
|
||||||
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[星空字幕组] 剃须。然后捡到女高中生。 / Anime Series Title [05][1080p][简日内嵌]", "Anime Series Title", "星空字幕组", 5)]
|
[TestCase("[星空字幕组] 剃须。然后捡到女高中生。 / Anime Series Title [05][1080p][简日内嵌]", "Anime Series Title", "星空字幕组", 5)]
|
||||||
[TestCase("【DHR动研字幕组】[多田君不恋爱_Anime Series Title][13完][繁体][720P][MP4]", "Anime Series Title", "DHR动研字幕组", 13)]
|
[TestCase("【DHR动研字幕组】[多田君不恋爱_Anime Series Title][13完][繁体][720P][MP4]", "Anime Series Title", "DHR动研字幕组", 13)]
|
||||||
[TestCase("【动漫国字幕组】★01月新番[Anime Series Title~!][01][1080P][简体][MP4]", "Anime Series Title~!", "动漫国字幕组", 1)]
|
[TestCase("【动漫国字幕组】★01月新番[Anime Series Title~!][01][1080P][简体][MP4]", "Anime Series Title~!", "动漫国字幕组", 1)]
|
||||||
|
[TestCase("[风车字幕组][名侦探柯南][857][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||||
|
[TestCase("[风车字幕组][名侦探柯南][857集][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||||
public void should_parse_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
public void should_parse_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
||||||
{
|
{
|
||||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
||||||
@@ -44,6 +46,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[Lilith-Raws] 艾梅洛閣下 II 世事件簿 -魔眼蒐集列車 Grace note- / Anime-Series Title - 04 [BiliBili][WEB-DL][1080p][AVC AAC][CHT][MKV]", "Anime-Series Title", "Lilith-Raws", 4)]
|
[TestCase("[Lilith-Raws] 艾梅洛閣下 II 世事件簿 -魔眼蒐集列車 Grace note- / Anime-Series Title - 04 [BiliBili][WEB-DL][1080p][AVC AAC][CHT][MKV]", "Anime-Series Title", "Lilith-Raws", 4)]
|
||||||
[TestCase("[NC-Raws] 影宅 / Anime-Series Title - 07 [B-Global][WEB-DL][1080p][AVC AAC][CHS_CHT_ENG_TH_SRT][MKV]", "Anime-Series Title", "NC-Raws", 7)]
|
[TestCase("[NC-Raws] 影宅 / Anime-Series Title - 07 [B-Global][WEB-DL][1080p][AVC AAC][CHS_CHT_ENG_TH_SRT][MKV]", "Anime-Series Title", "NC-Raws", 7)]
|
||||||
[TestCase("[NC-Raws] ANIME-SERIES TITLE-影宅- / Anime-Series Title - 07 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]", "Anime-Series Title", "NC-Raws", 7)]
|
[TestCase("[NC-Raws] ANIME-SERIES TITLE-影宅- / Anime-Series Title - 07 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]", "Anime-Series Title", "NC-Raws", 7)]
|
||||||
|
[TestCase("[OPFans楓雪動漫][ANIME SERIES 海賊王][第1008話][典藏版][1080P][MKV][簡繁]", "ANIME SERIES", "OPFans", 1008)]
|
||||||
|
[TestCase("[Skymoon-Raws][Anime Series 海賊王][1008][ViuTV][WEB-RIP][CHT][SRTx2][1080p][MKV]", "Anime Series", "Skymoon-Raws", 1008)]
|
||||||
public void should_parse_unbracketed_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
public void should_parse_unbracketed_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
||||||
{
|
{
|
||||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
||||||
|
|||||||
@@ -39,9 +39,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string SslCertHash { get; }
|
string SslCertHash { get; }
|
||||||
string UrlBase { get; }
|
string UrlBase { get; }
|
||||||
string UiFolder { get; }
|
string UiFolder { get; }
|
||||||
|
string InstanceName { get; }
|
||||||
bool UpdateAutomatically { get; }
|
bool UpdateAutomatically { get; }
|
||||||
UpdateMechanism UpdateMechanism { get; }
|
UpdateMechanism UpdateMechanism { get; }
|
||||||
string UpdateScriptPath { get; }
|
string UpdateScriptPath { get; }
|
||||||
|
string SyslogServer { get; }
|
||||||
|
int SyslogPort { get; }
|
||||||
|
string SyslogLevel { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConfigFileProvider : IConfigFileProvider
|
public class ConfigFileProvider : IConfigFileProvider
|
||||||
@@ -202,6 +206,19 @@ namespace NzbDrone.Core.Configuration
|
|||||||
// public string UiFolder => GetValue("UiFolder", "UI", false);GetValue("UiFolder", "UI", false);
|
// public string UiFolder => GetValue("UiFolder", "UI", false);GetValue("UiFolder", "UI", false);
|
||||||
public string UiFolder => "UI";
|
public string UiFolder => "UI";
|
||||||
|
|
||||||
|
public string InstanceName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var instanceName = GetValue("InstanceName", BuildInfo.AppName);
|
||||||
|
|
||||||
|
if (instanceName.StartsWith(BuildInfo.AppName) || instanceName.EndsWith(BuildInfo.AppName) )
|
||||||
|
{
|
||||||
|
return instanceName;
|
||||||
|
}
|
||||||
|
return BuildInfo.AppName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
||||||
|
|
||||||
@@ -209,7 +226,13 @@ namespace NzbDrone.Core.Configuration
|
|||||||
|
|
||||||
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
||||||
|
|
||||||
public int GetValueInt(string key, int defaultValue)
|
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
|
||||||
|
|
||||||
|
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
|
||||||
|
|
||||||
|
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
|
||||||
|
|
||||||
|
public int GetValueInt(string key, int defaultValue, bool persist = true)
|
||||||
{
|
{
|
||||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
|||||||
removeFailedDownloads = false;
|
removeFailedDownloads = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN (\"RTorrent\", \"Flood\") THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
|
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN ('RTorrent', 'Flood') THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
|
||||||
{
|
{
|
||||||
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
|
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
|
||||||
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
|
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(168)]
|
||||||
|
public class add_additional_info_to_pending_releases : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("PendingReleases").AddColumn("AdditionalInfo").AsString().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(169)]
|
||||||
|
public class add_malayalam_and_ukrainian_languages : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Execute.WithConnection(ConvertProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConvertProfile(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updater = new LanguageProfileUpdater169(conn, tran);
|
||||||
|
|
||||||
|
updater.AppendMissing();
|
||||||
|
|
||||||
|
updater.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LanguageProfile169 : ModelBase
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public List<LanguageProfileItem169> Languages { get; set; }
|
||||||
|
public bool UpgradeAllowed { get; set; }
|
||||||
|
public Language Cutoff { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LanguageProfileItem169
|
||||||
|
{
|
||||||
|
public int Language { get; set; }
|
||||||
|
public bool Allowed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class LanguageProfileUpdater169
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _connection;
|
||||||
|
private readonly IDbTransaction _transaction;
|
||||||
|
|
||||||
|
private List<LanguageProfile169> _profiles;
|
||||||
|
private HashSet<LanguageProfile169> _changedProfiles = new HashSet<LanguageProfile169>();
|
||||||
|
|
||||||
|
public LanguageProfileUpdater169(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
_connection = conn;
|
||||||
|
_transaction = tran;
|
||||||
|
|
||||||
|
_profiles = GetProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
foreach (var profile in _changedProfiles)
|
||||||
|
{
|
||||||
|
using (var updateProfileCmd = _connection.CreateCommand())
|
||||||
|
{
|
||||||
|
updateProfileCmd.Transaction = _transaction;
|
||||||
|
updateProfileCmd.CommandText = "UPDATE LanguageProfiles SET Languages = ? WHERE Id = ?";
|
||||||
|
updateProfileCmd.AddParameter(profile.Languages.ToJson());
|
||||||
|
updateProfileCmd.AddParameter(profile.Id);
|
||||||
|
|
||||||
|
updateProfileCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_changedProfiles.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendMissing()
|
||||||
|
{
|
||||||
|
foreach (var profile in _profiles)
|
||||||
|
{
|
||||||
|
var hash = new HashSet<int>(profile.Languages.Select(v => v.Language));
|
||||||
|
|
||||||
|
var missing = Language.All.Where(l => !hash.Contains(l.Id))
|
||||||
|
.OrderByDescending(l => l.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missing.Any())
|
||||||
|
{
|
||||||
|
profile.Languages.InsertRange(0, missing.Select(l => new LanguageProfileItem169 { Language = l.Id, Allowed = false }));
|
||||||
|
|
||||||
|
_changedProfiles.Add(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LanguageProfile169> GetProfiles()
|
||||||
|
{
|
||||||
|
var profiles = new List<LanguageProfile169>();
|
||||||
|
|
||||||
|
using (var getProfilesCmd = _connection.CreateCommand())
|
||||||
|
{
|
||||||
|
getProfilesCmd.Transaction = _transaction;
|
||||||
|
getProfilesCmd.CommandText = @"SELECT Id, Name, Languages, UpgradeAllowed, Cutoff FROM LanguageProfiles";
|
||||||
|
|
||||||
|
using (var profileReader = getProfilesCmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (profileReader.Read())
|
||||||
|
{
|
||||||
|
profiles.Add(new LanguageProfile169
|
||||||
|
{
|
||||||
|
Id = profileReader.GetInt32(0),
|
||||||
|
Name = profileReader.GetString(1),
|
||||||
|
Languages = Json.Deserialize<List<LanguageProfileItem169>>(profileReader.GetString(2)),
|
||||||
|
UpgradeAllowed = profileReader.GetBoolean(3),
|
||||||
|
Cutoff = Language.FindById(profileReader.GetInt32(4))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
public override bool SupportsTransactions => true;
|
public override bool SupportsTransactions => true;
|
||||||
|
|
||||||
|
public override bool TableExists(string schemaName, string tableName)
|
||||||
|
{
|
||||||
|
return Exists("select count(*) from sqlite_master where name='{0}' and type='table'", tableName);
|
||||||
|
}
|
||||||
|
|
||||||
public override void Process(AlterColumnExpression expression)
|
public override void Process(AlterColumnExpression expression)
|
||||||
{
|
{
|
||||||
var tableDefinition = GetTableSchema(expression.TableName);
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
MapRepository.Instance.RegisterTypeConverter(typeof(List<LanguageProfileItem>), new EmbeddedDocumentConverter(new LanguageIntConverter()));
|
MapRepository.Instance.RegisterTypeConverter(typeof(List<LanguageProfileItem>), new EmbeddedDocumentConverter(new LanguageIntConverter()));
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
|
||||||
|
MapRepository.Instance.RegisterTypeConverter(typeof(PendingReleaseAdditionalInfo), new EmbeddedDocumentConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<int>), new EmbeddedDocumentConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<int>), new EmbeddedDocumentConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -43,18 +44,47 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runtime = subject.Series.Runtime;
|
||||||
|
|
||||||
|
if (runtime == 0)
|
||||||
|
{
|
||||||
|
var firstSeasonNumber = subject.Series.Seasons.Where(s => s.SeasonNumber > 0).Min(s => s.SeasonNumber);
|
||||||
|
var pilotEpisode = _episodeService.GetEpisodesBySeason(subject.Series.Id, firstSeasonNumber).First();
|
||||||
|
|
||||||
|
if (subject.Episodes.First().SeasonNumber == pilotEpisode.SeasonNumber)
|
||||||
|
{
|
||||||
|
// If the first episode has an air date use it, otherwise use the release's publish date because like runtime it may not have updated yet.
|
||||||
|
var gracePeriodEnd = (pilotEpisode.AirDateUtc ?? subject.Release.PublishDate).AddHours(24);
|
||||||
|
|
||||||
|
// If episodes don't have an air date that is okay, otherwise make sure it's within 24 hours of the first episode airing.
|
||||||
|
if (subject.Episodes.All(e => !e.AirDateUtc.HasValue || e.AirDateUtc.Value.Before(gracePeriodEnd)))
|
||||||
|
{
|
||||||
|
_logger.Debug("Series runtime is 0, but all episodes in release aired within 24 hours of first episode in season, defaulting runtime to 45 minutes");
|
||||||
|
runtime = 45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject if the run time is still 0
|
||||||
|
if (runtime == 0)
|
||||||
|
{
|
||||||
|
_logger.Debug("Series runtime is 0, unable to validate size until it is available, rejecting");
|
||||||
|
return Decision.Reject("Series runtime is 0, unable to validate size until it is available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var qualityDefinition = _qualityDefinitionService.Get(quality);
|
var qualityDefinition = _qualityDefinitionService.Get(quality);
|
||||||
|
|
||||||
if (qualityDefinition.MinSize.HasValue)
|
if (qualityDefinition.MinSize.HasValue)
|
||||||
{
|
{
|
||||||
var minSize = qualityDefinition.MinSize.Value.Megabytes();
|
var minSize = qualityDefinition.MinSize.Value.Megabytes();
|
||||||
|
|
||||||
//Multiply maxSize by Series.Runtime
|
// Multiply maxSize by Series.Runtime
|
||||||
minSize = minSize * subject.Series.Runtime * subject.Episodes.Count;
|
minSize = minSize * runtime * subject.Episodes.Count;
|
||||||
|
|
||||||
//If the parsed size is smaller than minSize we don't want it
|
// If the parsed size is smaller than minSize we don't want it
|
||||||
if (subject.Release.Size < minSize)
|
if (subject.Release.Size < minSize)
|
||||||
{
|
{
|
||||||
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min";
|
var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min";
|
||||||
|
|
||||||
_logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage);
|
_logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage);
|
||||||
return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage);
|
return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage);
|
||||||
@@ -64,46 +94,31 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
{
|
{
|
||||||
_logger.Debug("Max size is unlimited, skipping size check");
|
_logger.Debug("Max size is unlimited, skipping size check");
|
||||||
}
|
}
|
||||||
else if (subject.Series.Runtime == 0)
|
|
||||||
{
|
|
||||||
_logger.Debug("Series runtime is 0, unable to validate size until it is available, rejecting");
|
|
||||||
return Decision.Reject("Series runtime is 0, unable to validate size until it is available");
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var maxSize = qualityDefinition.MaxSize.Value.Megabytes();
|
var maxSize = qualityDefinition.MaxSize.Value.Megabytes();
|
||||||
|
|
||||||
//Multiply maxSize by Series.Runtime
|
// Multiply maxSize by Series.Runtime
|
||||||
maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count;
|
maxSize = maxSize * runtime * subject.Episodes.Count;
|
||||||
|
|
||||||
if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard)
|
if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard)
|
||||||
{
|
{
|
||||||
Episode episode = subject.Episodes.First();
|
var firstEpisode = subject.Episodes.First();
|
||||||
List<Episode> seasonEpisodes;
|
var seasonEpisodes = GetSeasonEpisodes(subject, searchCriteria);
|
||||||
|
|
||||||
var seasonSearchCriteria = searchCriteria as SeasonSearchCriteria;
|
// Ensure that this is either the first episode
|
||||||
if (seasonSearchCriteria != null && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == episode.Id))
|
// or is the last episode in a season that has 10 or more episodes
|
||||||
{
|
if (seasonEpisodes.First().Id == firstEpisode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == firstEpisode.Id))
|
||||||
seasonEpisodes = seasonSearchCriteria.Episodes;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
seasonEpisodes = _episodeService.GetEpisodesBySeason(episode.SeriesId, episode.SeasonNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Ensure that this is either the first episode
|
|
||||||
//or is the last episode in a season that has 10 or more episodes
|
|
||||||
if (seasonEpisodes.First().Id == episode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == episode.Id))
|
|
||||||
{
|
{
|
||||||
_logger.Debug("Possible double episode, doubling allowed size.");
|
_logger.Debug("Possible double episode, doubling allowed size.");
|
||||||
maxSize = maxSize * 2;
|
maxSize = maxSize * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//If the parsed size is greater than maxSize we don't want it
|
// If the parsed size is greater than maxSize we don't want it
|
||||||
if (subject.Release.Size > maxSize)
|
if (subject.Release.Size > maxSize)
|
||||||
{
|
{
|
||||||
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min";
|
var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min";
|
||||||
|
|
||||||
_logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting", subject, subject.Release.Size, maxSize, runtimeMessage);
|
_logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting", subject, subject.Release.Size, maxSize, runtimeMessage);
|
||||||
return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage);
|
return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage);
|
||||||
@@ -113,5 +128,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||||||
_logger.Debug("Item: {0}, meets size constraints", subject);
|
_logger.Debug("Item: {0}, meets size constraints", subject);
|
||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Episode> GetSeasonEpisodes(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
var firstEpisode = subject.Episodes.First();
|
||||||
|
|
||||||
|
if (searchCriteria is SeasonSearchCriteria seasonSearchCriteria && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == firstEpisode.Id))
|
||||||
|
{
|
||||||
|
return seasonSearchCriteria.Episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _episodeService.GetEpisodesBySeason(firstEpisode.SeriesId, firstEpisode.SeasonNumber);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
|||||||
|
|
||||||
public int Code { get; set; }
|
public int Code { get; set; }
|
||||||
|
|
||||||
public bool SessionError => Code == 105 || Code == 106 || Code == 107;
|
public bool SessionError => Code == 105 || Code == 106 || Code == 107 || Code == 119;
|
||||||
|
|
||||||
public string GetMessage(DiskStationApi api)
|
public string GetMessage(DiskStationApi api)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -286,7 +286,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "forcedDL": //torrent is being downloaded, and was forced started
|
case "forcedDL": // torrent is being downloaded, and was forced started
|
||||||
|
case "forcedMetaDL": // torrent metadata is being forcibly downloaded
|
||||||
case "moving": // torrent is being moved from a folder
|
case "moving": // torrent is being moved from a folder
|
||||||
case "downloading": // torrent is being downloaded and data is being transferred
|
case "downloading": // torrent is being downloaded and data is being transferred
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
@@ -308,7 +309,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
else if (item.Status == DownloadItemStatus.Completed)
|
else if (item.Status == DownloadItemStatus.Completed)
|
||||||
{
|
{
|
||||||
item.Status = DownloadItemStatus.Warning;
|
item.Status = DownloadItemStatus.Warning;
|
||||||
item.Message = "Unable to import since content path is equal to root download directory, perhaps Keep top-level folder was disabled for this torrent?";
|
item.Message = "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using NzbDrone.Core.MediaFiles;
|
|||||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
@@ -97,9 +98,13 @@ namespace NzbDrone.Core.Download
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackedDownload.Warn("Found matching series via grab history, but release title doesn't match series title. Automatic import is not possible.");
|
Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.SERIES_MATCH_TYPE, SeriesMatchType.Unknown.ToString()), out SeriesMatchType seriesMatchType);
|
||||||
|
|
||||||
return;
|
if (seriesMatchType == SeriesMatchType.Id)
|
||||||
|
{
|
||||||
|
trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackedDownload.State = TrackedDownloadState.ImportPending;
|
trackedDownload.State = TrackedDownloadState.ImportPending;
|
||||||
@@ -124,7 +129,7 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
var outputPath = trackedDownload.ImportItem.OutputPath.FullPath;
|
var outputPath = trackedDownload.ImportItem.OutputPath.FullPath;
|
||||||
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto,
|
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto,
|
||||||
trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
|
trackedDownload.RemoteEpisode.Series, trackedDownload.ImportItem);
|
||||||
|
|
||||||
if (VerifyImport(trackedDownload, importResults))
|
if (VerifyImport(trackedDownload, importResults))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
||||||
public ReleaseInfo Release { get; set; }
|
public ReleaseInfo Release { get; set; }
|
||||||
public PendingReleaseReason Reason { get; set; }
|
public PendingReleaseReason Reason { get; set; }
|
||||||
|
public PendingReleaseAdditionalInfo AdditionalInfo { get; set; }
|
||||||
|
|
||||||
//Not persisted
|
//Not persisted
|
||||||
public RemoteEpisode RemoteEpisode { get; set; }
|
public RemoteEpisode RemoteEpisode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PendingReleaseAdditionalInfo
|
||||||
|
{
|
||||||
|
public SeriesMatchType SeriesMatchType { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,16 +291,15 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
// Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up)
|
// Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up)
|
||||||
if (series == null) return null;
|
if (series == null) return null;
|
||||||
|
|
||||||
|
|
||||||
release.RemoteEpisode = new RemoteEpisode
|
release.RemoteEpisode = new RemoteEpisode
|
||||||
{
|
{
|
||||||
Series = series,
|
Series = series,
|
||||||
|
SeriesMatchType = release.AdditionalInfo?.SeriesMatchType ?? SeriesMatchType.Unknown,
|
||||||
ParsedEpisodeInfo = release.ParsedEpisodeInfo,
|
ParsedEpisodeInfo = release.ParsedEpisodeInfo,
|
||||||
Release = release.Release
|
Release = release.Release
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteEpisode knownRemoteEpisode;
|
if (knownRemoteEpisodes != null && knownRemoteEpisodes.TryGetValue(release.Release.Title, out var knownRemoteEpisode))
|
||||||
if (knownRemoteEpisodes != null && knownRemoteEpisodes.TryGetValue(release.Release.Title, out knownRemoteEpisode))
|
|
||||||
{
|
{
|
||||||
release.RemoteEpisode.MappedSeasonNumber = knownRemoteEpisode.MappedSeasonNumber;
|
release.RemoteEpisode.MappedSeasonNumber = knownRemoteEpisode.MappedSeasonNumber;
|
||||||
release.RemoteEpisode.Episodes = knownRemoteEpisode.Episodes;
|
release.RemoteEpisode.Episodes = knownRemoteEpisode.Episodes;
|
||||||
@@ -333,7 +332,11 @@ namespace NzbDrone.Core.Download.Pending
|
|||||||
Release = decision.RemoteEpisode.Release,
|
Release = decision.RemoteEpisode.Release,
|
||||||
Title = decision.RemoteEpisode.Release.Title,
|
Title = decision.RemoteEpisode.Release.Title,
|
||||||
Added = DateTime.UtcNow,
|
Added = DateTime.UtcNow,
|
||||||
Reason = reason
|
Reason = reason,
|
||||||
|
AdditionalInfo = new PendingReleaseAdditionalInfo
|
||||||
|
{
|
||||||
|
SeriesMatchType = decision.RemoteEpisode.SeriesMatchType
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());
|
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ namespace NzbDrone.Core.Extras
|
|||||||
public void Handle(SeriesScannedEvent message)
|
public void Handle(SeriesScannedEvent message)
|
||||||
{
|
{
|
||||||
var series = message.Series;
|
var series = message.Series;
|
||||||
var extraFiles = new List<ExtraFile>();
|
|
||||||
|
|
||||||
if (!_diskProvider.FolderExists(series.Path))
|
if (!_diskProvider.FolderExists(series.Path))
|
||||||
{
|
{
|
||||||
@@ -41,19 +40,18 @@ namespace NzbDrone.Core.Extras
|
|||||||
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
||||||
|
|
||||||
var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path);
|
var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path);
|
||||||
var possibleExtraFiles = _diskScanService.FilterPaths(series.Path, filesOnDisk, false);
|
var possibleExtraFiles = _diskScanService.FilterPaths(series.Path, filesOnDisk);
|
||||||
|
|
||||||
var filteredFiles = possibleExtraFiles;
|
|
||||||
var importedFiles = new List<string>();
|
var importedFiles = new List<string>();
|
||||||
|
|
||||||
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
|
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
|
||||||
{
|
{
|
||||||
var imported = existingExtraFileImporter.ProcessFiles(series, filteredFiles, importedFiles);
|
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles);
|
||||||
|
|
||||||
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
|
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Info("Found {0} extra files", extraFiles.Count);
|
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,17 @@ namespace NzbDrone.Core.Extras
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var folderSearchOption = localEpisode.FolderEpisodeInfo == null
|
||||||
|
? SearchOption.TopDirectoryOnly
|
||||||
|
: SearchOption.AllDirectories;
|
||||||
|
|
||||||
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
.Select(e => e.Trim(' ', '.')
|
.Select(e => e.Trim(' ', '.')
|
||||||
.Insert(0, "."))
|
.Insert(0, "."))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var sourceFolder = _diskProvider.GetParentFolder(localEpisode.Path);
|
var sourceFolder = _diskProvider.GetParentFolder(localEpisode.Path);
|
||||||
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories);
|
var files = _diskProvider.GetFiles(sourceFolder, folderSearchOption);
|
||||||
var managedFiles = _extraFileManagers.Select((i) => new List<string>()).ToArray();
|
var managedFiles = _extraFileManagers.Select((i) => new List<string>()).ToArray();
|
||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Extras.Files
|
|||||||
List<TExtraFile> GetFilesBySeries(int seriesId);
|
List<TExtraFile> GetFilesBySeries(int seriesId);
|
||||||
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
|
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
|
||||||
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
||||||
TExtraFile FindByPath(string path);
|
TExtraFile FindByPath(int seriesId, string path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExtraFileRepository<TExtraFile> : BasicRepository<TExtraFile>, IExtraFileRepository<TExtraFile>
|
public class ExtraFileRepository<TExtraFile> : BasicRepository<TExtraFile>, IExtraFileRepository<TExtraFile>
|
||||||
@@ -54,9 +54,9 @@ namespace NzbDrone.Core.Extras.Files
|
|||||||
return Query.Where(c => c.EpisodeFileId == episodeFileId);
|
return Query.Where(c => c.EpisodeFileId == episodeFileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TExtraFile FindByPath(string path)
|
public TExtraFile FindByPath(int seriesId, string path)
|
||||||
{
|
{
|
||||||
return Query.Where(c => c.RelativePath == path).SingleOrDefault();
|
return Query.Where(c => c.SeriesId == seriesId && c.RelativePath == path).SingleOrDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras.Files
|
|||||||
{
|
{
|
||||||
List<TExtraFile> GetFilesBySeries(int seriesId);
|
List<TExtraFile> GetFilesBySeries(int seriesId);
|
||||||
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
||||||
TExtraFile FindByPath(string path);
|
TExtraFile FindByPath(int seriesId, string path);
|
||||||
void Upsert(TExtraFile extraFile);
|
void Upsert(TExtraFile extraFile);
|
||||||
void Upsert(List<TExtraFile> extraFiles);
|
void Upsert(List<TExtraFile> extraFiles);
|
||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
@@ -59,9 +59,9 @@ namespace NzbDrone.Core.Extras.Files
|
|||||||
return _repository.GetFilesByEpisodeFile(episodeFileId);
|
return _repository.GetFilesByEpisodeFile(episodeFileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TExtraFile FindByPath(string path)
|
public TExtraFile FindByPath(int seriesId, string path)
|
||||||
{
|
{
|
||||||
return _repository.FindByPath(path);
|
return _repository.FindByPath(seriesId, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Upsert(TExtraFile extraFile)
|
public void Upsert(TExtraFile extraFile)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user