mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
89 Commits
v3.0.7.147
...
3.0.10.156
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67e2fe551a | ||
|
|
5db5b1dace | ||
|
|
c7919f80de | ||
|
|
1a1d427c42 | ||
|
|
9263fc1564 | ||
|
|
8ab040f612 | ||
|
|
d6dff451e0 | ||
|
|
ac7afc351c | ||
|
|
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,
|
||||
releaseGroup,
|
||||
preferredWordScore,
|
||||
seriesMatchType,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
@@ -72,6 +73,16 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
seriesMatchType ?
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Series Match Type"
|
||||
data={seriesMatchType}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
nzbInfoUrl ?
|
||||
<span>
|
||||
|
||||
@@ -217,6 +217,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.series {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
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 Icon from 'Components/Icon';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSearchResult.css';
|
||||
|
||||
class ImportSeriesSearchResult extends Component {
|
||||
function ImportSeriesSearchResult(props) {
|
||||
const {
|
||||
tvdbId,
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries,
|
||||
onPress
|
||||
} = props;
|
||||
|
||||
//
|
||||
// Listeners
|
||||
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tvdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.series}
|
||||
onPress={this.onPress}
|
||||
onPress={onPressCallback}
|
||||
>
|
||||
<ImportSeriesTitle
|
||||
title={title}
|
||||
@@ -36,8 +31,19 @@ class ImportSeriesSearchResult extends Component {
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
</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 = {
|
||||
|
||||
@@ -20,24 +20,27 @@ function ImportSeriesTitle(props) {
|
||||
|
||||
{
|
||||
!title.contains(year) &&
|
||||
year > 0 &&
|
||||
year > 0 ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label>{network}</Label>
|
||||
network ?
|
||||
<Label>{network}</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
isExistingSeries ?
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Sonarr">
|
||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -19,7 +19,7 @@ import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import Settings from 'Settings/Settings';
|
||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||
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 ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
@@ -158,7 +158,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/quality"
|
||||
component={Quality}
|
||||
component={QualityConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<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 className={styles.automatic}>
|
||||
|
||||
@@ -15,6 +15,7 @@ export const REFRESH_SERIES = 'RefreshSeries';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_SERIES = 'RenameSeries';
|
||||
export const RESET_API_KEY = 'ResetApiKey';
|
||||
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
|
||||
export const RSS_SYNC = 'RssSync';
|
||||
export const SEASON_SEARCH = 'SeasonSearch';
|
||||
export const SERIES_SEARCH = 'SeriesSearch';
|
||||
|
||||
@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
type="date"
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
|
||||
@@ -5,7 +5,7 @@ import FocusLock from 'react-focus-lock';
|
||||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isIOS } from 'Utilities/mobile';
|
||||
import { isIOS } from 'Utilities/browser';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
|
||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={PageContentError}>
|
||||
<DocumentTitle title={title ? `${title} - Sonarr` : 'Sonarr'}>
|
||||
<DocumentTitle title={title ? `${title} - ${window.Sonarr.instanceName}` : window.Sonarr.instanceName}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile, isFirefox } from 'Utilities/browser';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
@@ -15,7 +15,8 @@ class PageContentBody extends Component {
|
||||
constructor(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
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
const ScrollerComponent = this._isMobile || this._isSmallScreenFirefox ?
|
||||
Scroller :
|
||||
OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||
|
||||
function getState(status) {
|
||||
switch (status) {
|
||||
@@ -70,6 +71,7 @@ const mapDispatchToProps = {
|
||||
dispatchUpdateItem: updateItem,
|
||||
dispatchRemoveItem: removeItem,
|
||||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
@@ -221,6 +223,10 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleQualitydefinition = () => {
|
||||
this.props.dispatchFetchQualityDefinitions();
|
||||
}
|
||||
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.dispatchFetchQueue();
|
||||
@@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
|
||||
dispatchUpdateItem: PropTypes.func.isRequired,
|
||||
dispatchRemoveItem: PropTypes.func.isRequired,
|
||||
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
|
||||
@@ -103,6 +103,7 @@ class VirtualTable extends Component {
|
||||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowHeight,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -153,7 +154,7 @@ class VirtualTable extends Component {
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
@@ -194,7 +195,8 @@ VirtualTable.propTypes = {
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38
|
||||
headerHeight: 38,
|
||||
rowHeight: ROW_HEIGHT
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Portal from 'Components/Portal';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
|
||||
@@ -32,7 +32,8 @@ function MediaInfo(props) {
|
||||
audioCodec,
|
||||
audioLanguages,
|
||||
subtitles,
|
||||
videoCodec
|
||||
videoCodec,
|
||||
videoDynamicRangeType
|
||||
} = props;
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO) {
|
||||
@@ -72,6 +73,14 @@ function MediaInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||
return (
|
||||
<span>
|
||||
{videoDynamicRangeType}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,7 +90,8 @@ MediaInfo.propTypes = {
|
||||
audioCodec: PropTypes.string,
|
||||
audioLanguages: PropTypes.string,
|
||||
subtitles: PropTypes.string,
|
||||
videoCodec: PropTypes.string
|
||||
videoCodec: PropTypes.string,
|
||||
videoDynamicRangeType: PropTypes.string
|
||||
};
|
||||
|
||||
export default MediaInfo;
|
||||
|
||||
@@ -2,3 +2,4 @@ export const AUDIO = 'audio';
|
||||
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||
export const SUBTITLES = 'subtitles';
|
||||
export const VIDEO = 'video';
|
||||
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||
|
||||
@@ -96,6 +96,7 @@ class SelectEpisodeModalContent extends Component {
|
||||
isAnime,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
modalTitle,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -121,7 +122,7 @@ class SelectEpisodeModalContent extends Component {
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
<div className={styles.header}>
|
||||
Manual Import - Select Episode(s)
|
||||
{modalTitle} - Select Episode(s)
|
||||
</div>
|
||||
|
||||
</ModalHeader>
|
||||
@@ -235,6 +236,7 @@ SelectEpisodeModalContent.propTypes = {
|
||||
isAnime: PropTypes.bool.isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
modalTitle: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onEpisodesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -67,6 +67,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
const {
|
||||
recentFolders,
|
||||
onRemoveRecentFolderPress,
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -75,7 +76,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Folder
|
||||
{modalTitle} - Select Folder
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -159,6 +160,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
|
||||
InteractiveImportSelectFolderModalContent.propTypes = {
|
||||
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onQuickImportPress: PropTypes.func.isRequired,
|
||||
onInteractiveImportPress: PropTypes.func.isRequired,
|
||||
onRemoveRecentFolderPress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -94,6 +94,7 @@ const filterExistingFilesOptions = {
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
||||
];
|
||||
@@ -250,6 +251,7 @@ class InteractiveImportModalContent extends Component {
|
||||
importMode,
|
||||
interactiveImportErrorMessage,
|
||||
isDeleting,
|
||||
modalTitle,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -299,7 +301,7 @@ class InteractiveImportModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - {title || folder}
|
||||
{modalTitle} - {title || folder}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
@@ -375,6 +377,7 @@ class InteractiveImportModalContent extends Component {
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
autoSelectRow={autoSelectRow}
|
||||
columns={this.state.columns}
|
||||
modalTitle={modalTitle}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
@@ -452,6 +455,7 @@ class InteractiveImportModalContent extends Component {
|
||||
<SelectSeriesModal
|
||||
isOpen={selectModalOpen === SERIES}
|
||||
ids={selectedIds}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -459,6 +463,7 @@ class InteractiveImportModalContent extends Component {
|
||||
isOpen={selectModalOpen === SEASON}
|
||||
ids={selectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -467,6 +472,7 @@ class InteractiveImportModalContent extends Component {
|
||||
ids={orderedSelectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
seasonNumber={selectedItem && selectedItem.seasonNumber}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -474,6 +480,7 @@ class InteractiveImportModalContent extends Component {
|
||||
isOpen={selectModalOpen === RELEASE_GROUP}
|
||||
ids={selectedIds}
|
||||
releaseGroup=""
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -481,6 +488,7 @@ class InteractiveImportModalContent extends Component {
|
||||
isOpen={selectModalOpen === LANGUAGE}
|
||||
ids={selectedIds}
|
||||
languageId={0}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -490,6 +498,7 @@ class InteractiveImportModalContent extends Component {
|
||||
qualityId={0}
|
||||
proper={false}
|
||||
real={false}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -528,6 +537,7 @@ InteractiveImportModalContent.propTypes = {
|
||||
interactiveImportErrorMessage: PropTypes.string,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
@@ -29,12 +30,7 @@ function isSameEpisodeFile(file, originalFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const episodeIds = episodes.map((e) => e.id);
|
||||
const originalEpisodeIds = originalFile.episodes ? originalFile.episodes.map((e) => e.id) : [];
|
||||
|
||||
return episodeIds.every((episodeId) => {
|
||||
return originalEpisodeIds.indexOf(episodeId) >= 0;
|
||||
});
|
||||
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -182,6 +178,11 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
const existingFiles = [];
|
||||
const files = [];
|
||||
|
||||
if (importMode === 'chooseImportMode') {
|
||||
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = selected.indexOf(item.id) > -1;
|
||||
|
||||
|
||||
@@ -215,7 +215,8 @@ class InteractiveImportRow extends Component {
|
||||
size,
|
||||
rejections,
|
||||
isReprocessing,
|
||||
isSelected
|
||||
isSelected,
|
||||
modalTitle
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -396,6 +397,7 @@ class InteractiveImportRow extends Component {
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
ids={[id]}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
/>
|
||||
|
||||
@@ -403,6 +405,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectSeasonModalOpen}
|
||||
ids={[id]}
|
||||
seriesId={series && series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeasonModalClose}
|
||||
/>
|
||||
|
||||
@@ -413,6 +416,7 @@ class InteractiveImportRow extends Component {
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
relativePath={relativePath}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectEpisodeModalClose}
|
||||
/>
|
||||
|
||||
@@ -420,6 +424,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectReleaseGroupModalOpen}
|
||||
ids={[id]}
|
||||
releaseGroup={releaseGroup ?? ''}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectReleaseGroupModalClose}
|
||||
/>
|
||||
|
||||
@@ -429,6 +434,7 @@ class InteractiveImportRow extends Component {
|
||||
qualityId={quality ? quality.quality.id : 0}
|
||||
proper={quality ? quality.revision.version > 1 : false}
|
||||
real={quality ? quality.revision.real > 0 : false}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
@@ -436,6 +442,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectLanguageModalOpen}
|
||||
ids={[id]}
|
||||
languageId={language ? language.id : 0}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectLanguageModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
@@ -460,6 +467,7 @@ InteractiveImportRow.propTypes = {
|
||||
episodeFileId: PropTypes.number,
|
||||
isReprocessing: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -75,7 +75,12 @@ InteractiveImportModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
folder: PropTypes.string,
|
||||
downloadId: PropTypes.string,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModal.defaultProps = {
|
||||
modalTitle: 'Manual Import'
|
||||
};
|
||||
|
||||
export default InteractiveImportModal;
|
||||
|
||||
@@ -19,6 +19,7 @@ function SelectLanguageModalContent(props) {
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
modalTitle,
|
||||
onModalClose,
|
||||
onLanguageSelect
|
||||
} = props;
|
||||
@@ -33,7 +34,7 @@ function SelectLanguageModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Language
|
||||
{modalTitle} - Select Language
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -80,6 +81,7 @@ SelectLanguageModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onLanguageSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ class SelectQualityModalContent extends Component {
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -80,7 +81,7 @@ class SelectQualityModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Quality
|
||||
{modalTitle} - Select Quality
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -159,6 +160,7 @@ SelectQualityModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onQualitySelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ class SelectReleaseGroupModalContent extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -55,7 +56,7 @@ class SelectReleaseGroupModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Set Release Group
|
||||
{modalTitle} - Set Release Group
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -96,6 +97,7 @@ class SelectReleaseGroupModalContent extends Component {
|
||||
|
||||
SelectReleaseGroupModalContent.propTypes = {
|
||||
releaseGroup: PropTypes.string.isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onReleaseGroupSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ class SelectSeasonModalContent extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
modalTitle,
|
||||
onSeasonSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -22,7 +23,7 @@ class SelectSeasonModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Season
|
||||
{modalTitle} - Select Season
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -51,6 +52,7 @@ class SelectSeasonModalContent extends Component {
|
||||
|
||||
SelectSeasonModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSeasonSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ class SelectSeriesModalContent extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
modalTitle,
|
||||
onSeriesSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -47,7 +48,7 @@ class SelectSeriesModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Series
|
||||
{modalTitle} - Select Series
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -96,6 +97,7 @@ class SelectSeriesModalContent extends Component {
|
||||
|
||||
SelectSeriesModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSeriesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -175,7 +175,7 @@ function InteractiveSearch(props) {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={item.guid}
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
}
|
||||
|
||||
.audioLanguages,
|
||||
.videoDynamicRangeType,
|
||||
.subtitles {
|
||||
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') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -279,7 +279,6 @@ class SeriesDetails extends Component {
|
||||
<PageToolbarButton
|
||||
label="Manage Episodes"
|
||||
iconName={icons.EPISODE_FILE}
|
||||
isDisabled={!hasEpisodeFiles}
|
||||
onPress={this.onManageEpisodesPress}
|
||||
/>
|
||||
|
||||
@@ -651,6 +650,7 @@ class SeriesDetails extends Component {
|
||||
autoSelectRow={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={'Manage Episodes'}
|
||||
onModalClose={this.onManageEpisodesModalClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const requiresRestartKeys = [
|
||||
'bindAddress',
|
||||
'port',
|
||||
'urlBase',
|
||||
'instanceName',
|
||||
'enableSsl',
|
||||
'sslPort',
|
||||
'sslCertHash',
|
||||
|
||||
@@ -19,6 +19,7 @@ function HostSettings(props) {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertHash,
|
||||
@@ -71,6 +72,22 @@ function HostSettings(props) {
|
||||
/>
|
||||
</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
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -51,7 +51,7 @@ class AddIndexerModalContent extends Component {
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<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>
|
||||
|
||||
<FieldSet legend="Usenet">
|
||||
|
||||
@@ -45,6 +45,7 @@ function EditIndexerModalContent(props) {
|
||||
tags,
|
||||
fields,
|
||||
priority,
|
||||
seasonSearchMaximumSingleEpisodeAge,
|
||||
protocol,
|
||||
downloadClientId
|
||||
} = item;
|
||||
@@ -153,6 +154,23 @@ function EditIndexerModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Maximum Single Episode Age</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="seasonSearchMaximumSingleEpisodeAge"
|
||||
helpText="During a full season search only season packs will be allowed when the season's last episode is older than this setting. Standard series only. Use 0 to disable."
|
||||
min={0}
|
||||
unit="days"
|
||||
{...seasonSearchMaximumSingleEpisodeAge}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -139,7 +139,7 @@ class MediaManagement extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
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}
|
||||
onChange={onInputChange}
|
||||
{...settings.episodeTitleRequired}
|
||||
|
||||
@@ -59,7 +59,7 @@ function NotificationEventItems(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onDownload"
|
||||
helpText="On Download"
|
||||
helpText="On Import"
|
||||
isDisabled={!supportsOnDownload.value}
|
||||
{...onDownload}
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -84,7 +84,7 @@ class DelayProfile extends Component {
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
let preferred = titleCase(preferredProtocol);
|
||||
let preferred = `Prefer ${titleCase(preferredProtocol)}`;
|
||||
|
||||
if (!enableUsenet) {
|
||||
preferred = 'Only Torrent';
|
||||
|
||||
@@ -81,7 +81,7 @@ class DelayProfiles extends Component {
|
||||
>
|
||||
<div>
|
||||
<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}>Torrent Delay</div>
|
||||
<div className={styles.tags}>Tags</div>
|
||||
|
||||
@@ -16,6 +16,13 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
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) {
|
||||
const {
|
||||
id,
|
||||
@@ -25,7 +32,6 @@ function EditDelayProfileModalContent(props) {
|
||||
saveError,
|
||||
item,
|
||||
protocol,
|
||||
protocolOptions,
|
||||
onInputChange,
|
||||
onProtocolChange,
|
||||
onSavePress,
|
||||
@@ -51,20 +57,22 @@ function EditDelayProfileModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
isFetching ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to add a new quality profile, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<div>Unable to add a new quality profile, please try again.</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
!isFetching && !error ?
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Protocol</FormLabel>
|
||||
<FormLabel>Preferred Protocol</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -140,19 +148,21 @@ function EditDelayProfileModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
</Form> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id && id > 1 &&
|
||||
id && id > 1 ?
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDelayProfilePress}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
@@ -190,7 +200,6 @@ EditDelayProfileModalContent.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onProtocolChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -16,13 +16,6 @@ const newDelayProfile = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
||||
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
||||
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
||||
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
||||
];
|
||||
|
||||
function createDelayProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
@@ -78,7 +71,6 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
protocol,
|
||||
protocolOptions,
|
||||
...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 PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
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 {
|
||||
|
||||
@@ -16,7 +21,8 @@ class Quality extends Component {
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false
|
||||
hasPendingChanges: false,
|
||||
isConfirmQualityDefinitionResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +37,14 @@ class Quality extends Component {
|
||||
this.setState(payload);
|
||||
}
|
||||
|
||||
onResetQualityDefinitionsPress = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
||||
}
|
||||
|
||||
onCloseResetQualityDefinitionsModal = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
@@ -43,6 +57,7 @@ class Quality extends Component {
|
||||
render() {
|
||||
const {
|
||||
isSaving,
|
||||
isResettingQualityDefinitions,
|
||||
hasPendingChanges
|
||||
} = this.state;
|
||||
|
||||
@@ -51,6 +66,18 @@ class Quality extends Component {
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Reset Definitions"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isResettingQualityDefinitions}
|
||||
onPress={this.onResetQualityDefinitionsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
@@ -60,9 +87,18 @@ class Quality extends Component {
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
<ResetQualityDefinitionsModal
|
||||
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
||||
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Quality.propTypes = {
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Quality;
|
||||
|
||||
38
frontend/src/Settings/Quality/QualityConnector.js
Normal file
38
frontend/src/Settings/Quality/QualityConnector.js
Normal file
@@ -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',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'videoDynamicRangeType',
|
||||
label: 'Video Dynamic Range',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'audioLanguages',
|
||||
label: 'Audio Languages',
|
||||
|
||||
@@ -82,6 +82,11 @@ export const defaultState = {
|
||||
label: 'Release Group',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'preferredWordScore',
|
||||
columnLabel: 'Preferred Word Score',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
@@ -33,12 +34,12 @@ export const defaultState = {
|
||||
sortKey: 'quality',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
recentFolders: [],
|
||||
importMode: 'move',
|
||||
importMode: 'chooseImportMode',
|
||||
sortPredicates: {
|
||||
relativePath: function(item, direction) {
|
||||
const relativePath = item.relativePath;
|
||||
|
||||
return relativePath.toLowerCase();
|
||||
return naturalExpansion(relativePath.toLowerCase());
|
||||
},
|
||||
|
||||
series: function(item, direction) {
|
||||
|
||||
@@ -178,7 +178,8 @@ export const defaultState = {
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'seeders',
|
||||
|
||||
@@ -44,7 +44,14 @@ function filter(items, state) {
|
||||
const predicate = filterPredicates[key];
|
||||
|
||||
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 {
|
||||
accepted = predicate(item, value, type);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class MoreInfo extends Component {
|
||||
|
||||
<DescriptionListItemTitle>Twitter</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://sonarr.tv/">@sonarrtv</Link>
|
||||
<Link to="https://twitter.com/sonarrtv">@sonarrtv</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
|
||||
|
||||
11
frontend/src/Utilities/String/naturalExpansion.js
Normal file
11
frontend/src/Utilities/String/naturalExpansion.js
Normal file
@@ -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) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return input.replace(/\b\w+/g, (match) => {
|
||||
return input.replace(regex, (match) => {
|
||||
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,3 +10,7 @@ export function isMobile() {
|
||||
export function isIOS() {
|
||||
return mobileDetect.is('iOS');
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return window.navigator.userAgent.toLowerCase().indexOf('firefox/') >= 0;
|
||||
}
|
||||
@@ -252,7 +252,7 @@
|
||||
</span>
|
||||
|
||||
<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"
|
||||
>Forgot your password?</a
|
||||
>
|
||||
|
||||
@@ -3,11 +3,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using Ical.Net.General;
|
||||
using Ical.Net.Interfaces.Serialization;
|
||||
using Ical.Net.Serialization;
|
||||
using Ical.Net.Serialization.iCalendar.Factory;
|
||||
using NzbDrone.Core.Tv;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Core.Tags;
|
||||
@@ -116,7 +114,7 @@ namespace NzbDrone.Api.Calendar
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrence = calendar.Create<Event>();
|
||||
var occurrence = calendar.Create<CalendarEvent>();
|
||||
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||
occurrence.Description = episode.Overview;
|
||||
|
||||
@@ -30,11 +30,5 @@ namespace NzbDrone.Api.DownloadClient
|
||||
definition.RemoveCompletedDownloads = resource.RemoveCompletedDownloads;
|
||||
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.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;
|
||||
}
|
||||
|
||||
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.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)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||
|
||||
if (providerDefinition.Enable)
|
||||
{
|
||||
@@ -88,18 +88,18 @@ namespace NzbDrone.Api
|
||||
|
||||
private void UpdateProvider(TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, false);
|
||||
var providerDefinition = GetDefinition(providerResource, true, false, false);
|
||||
|
||||
_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();
|
||||
|
||||
MapToModel(definition, providerResource);
|
||||
|
||||
if (validate)
|
||||
if (validate && (definition.Enable || forceValidate))
|
||||
{
|
||||
Validate(definition, includeWarnings);
|
||||
}
|
||||
@@ -170,19 +170,16 @@ namespace NzbDrone.Api
|
||||
|
||||
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, false);
|
||||
var providerDefinition = GetDefinition(providerResource, true, true, true);
|
||||
|
||||
Validate(providerDefinition, true);
|
||||
Test(providerDefinition, true);
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
|
||||
@@ -192,7 +189,7 @@ namespace NzbDrone.Api
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||
private void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||
{
|
||||
var validationResult = definition.Settings.Validate();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<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.Authentication.Basic" Version="2.0.0" />
|
||||
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
||||
|
||||
@@ -265,6 +265,11 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
protected virtual void MoveFileInternal(string source, string destination)
|
||||
{
|
||||
if (File.Exists(destination))
|
||||
{
|
||||
throw new FileAlreadyExistsException("File already exists", destination);
|
||||
}
|
||||
|
||||
File.Move(source, destination);
|
||||
}
|
||||
|
||||
|
||||
14
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
14
src/NzbDrone.Common/Disk/FileAlreadyExistsException.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Disk
|
||||
{
|
||||
public class FileAlreadyExistsException : Exception
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
|
||||
public FileAlreadyExistsException(string message, string filename) : base(message)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
|
||||
"/boot",
|
||||
"/lib",
|
||||
"/sbin",
|
||||
"/proc"
|
||||
"/proc",
|
||||
"/usr/bin"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<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="SharpZipLib" Version="1.2.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
@@ -30,4 +31,4 @@
|
||||
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -20,38 +20,50 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
private RemoteEpisode parseResultMulti;
|
||||
private RemoteEpisode parseResultSingle;
|
||||
private Series series;
|
||||
private List<Episode> episodes;
|
||||
private QualityDefinition qualityType;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
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
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
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
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
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
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
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>()
|
||||
.Setup(v => v.Get(It.IsAny<Quality>()))
|
||||
@@ -67,18 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
||||
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() { 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 } });
|
||||
.Returns(episodes);
|
||||
}
|
||||
|
||||
[TestCase(30, 50, false)]
|
||||
@@ -92,6 +93,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
series.Runtime = runtime;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = 5;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||
}
|
||||
@@ -100,13 +102,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestCase(30, 1000, false)]
|
||||
[TestCase(60, 1000, true)]
|
||||
[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;
|
||||
parseResultSingle.Series = series;
|
||||
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);
|
||||
}
|
||||
@@ -144,8 +159,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_size_is_zero()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 30;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 0;
|
||||
@@ -158,8 +171,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_unlimited_30_minute()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 30;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 18457280000;
|
||||
@@ -171,8 +182,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_unlimited_60_minute()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 60;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 36857280000;
|
||||
@@ -184,8 +193,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_treat_daily_series_as_single_episode()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 60;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
|
||||
@@ -216,5 +223,94 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NUnit.Framework;
|
||||
using FluentAssertions;
|
||||
using FizzWare.NBuilder;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SingleEpisodeAgeDownloadDecisionFixture : CoreTest<SeasonPackOnlySpecification>
|
||||
{
|
||||
private RemoteEpisode parseResultMulti;
|
||||
private RemoteEpisode parseResultSingle;
|
||||
private Series series;
|
||||
private List<Episode> episodes;
|
||||
private SeasonSearchCriteria multiSearch;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
series = Builder<Series>.CreateNew()
|
||||
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(1).Build().ToList())
|
||||
.With(s => s.SeriesType = SeriesTypes.Standard)
|
||||
.Build();
|
||||
|
||||
episodes = new List<Episode>();
|
||||
episodes.Add(CreateEpisodeStub(1, 400));
|
||||
episodes.Add(CreateEpisodeStub(2, 370));
|
||||
episodes.Add(CreateEpisodeStub(3, 340));
|
||||
episodes.Add(CreateEpisodeStub(4, 310));
|
||||
|
||||
multiSearch = new SeasonSearchCriteria();
|
||||
multiSearch.Episodes = episodes.ToList();
|
||||
multiSearch.SeasonNumber = 1;
|
||||
|
||||
parseResultMulti = new RemoteEpisode
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)), FullSeason = true },
|
||||
Episodes = episodes.ToList()
|
||||
};
|
||||
|
||||
parseResultSingle = new RemoteEpisode
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||
Episodes = new List<Episode>()
|
||||
};
|
||||
}
|
||||
|
||||
Episode CreateEpisodeStub(int number, int age)
|
||||
{
|
||||
return new Episode() {
|
||||
SeasonNumber = 1,
|
||||
EpisodeNumber = number,
|
||||
AirDateUtc = DateTime.UtcNow.AddDays(-age)
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(1, 200, false)]
|
||||
[TestCase(4, 200, false)]
|
||||
[TestCase(1, 600, true)]
|
||||
[TestCase(1, 365, true)]
|
||||
[TestCase(4, 365, true)]
|
||||
[TestCase(1, 0, true)]
|
||||
public void single_episode_release(int episode, int SeasonSearchMaximumSingleEpisodeAge, bool expectedResult)
|
||||
{
|
||||
parseResultSingle.Release.SeasonSearchMaximumSingleEpisodeAge = SeasonSearchMaximumSingleEpisodeAge;
|
||||
parseResultSingle.Episodes.Clear();
|
||||
parseResultSingle.Episodes.Add(episodes.Find(e => e.EpisodeNumber == episode));
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, multiSearch).Accepted.Should().Be(expectedResult);
|
||||
}
|
||||
|
||||
// should always accept all season packs
|
||||
[TestCase(200, true)]
|
||||
[TestCase(600, true)]
|
||||
[TestCase(365, true)]
|
||||
[TestCase(0, true)]
|
||||
public void multi_episode_release(int SeasonSearchMaximumSingleEpisodeAge, bool expectedResult)
|
||||
{
|
||||
parseResultMulti.Release.SeasonSearchMaximumSingleEpisodeAge = SeasonSearchMaximumSingleEpisodeAge;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultMulti, multiSearch).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Extras
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -202,5 +202,45 @@ namespace NzbDrone.Core.Test.Extras
|
||||
_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());
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ namespace NzbDrone.Core.Test.Extras.Subtitles
|
||||
[TestCase("Series Title - S01E01.srt", "Series Title - S01E01.srt")]
|
||||
[TestCase("Series.Title.S01E01.en.srt", "Series Title - S01E01.en.srt")]
|
||||
[TestCase("Series.Title.S01E01.english.srt", "Series Title - S01E01.en.srt")]
|
||||
[TestCase("Series-Title-S01E01-fr-cc.srt", "Series Title - S01E01.fr.srt")]
|
||||
[TestCase("Series Title S01E01_en_sdh_forced.srt", "Series Title - S01E01.en.srt")]
|
||||
[TestCase("Series-Title-S01E01-fr-cc.srt", "Series Title - S01E01.fr.cc.srt")]
|
||||
[TestCase("Series Title S01E01_en_sdh_forced.srt", "Series Title - S01E01.en.sdh.forced.srt")]
|
||||
[TestCase("Series_Title_S01E01 en.srt", "Series Title - S01E01.en.srt")]
|
||||
[TestCase(@"Subs\S01E01.en.srt", "Series Title - S01E01.en.srt")]
|
||||
[TestCase(@"Subs\Series.Title.S01E01\2_en.srt", "Series Title - S01E01.en.srt")]
|
||||
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Extras.Subtitles
|
||||
var files = new List<string>
|
||||
{
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.en.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.english.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.eng.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Subs", "Series_Title_S01E01_en_forced.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Subs", "Series.Title.S01E01", "2_fr.srt").AsOsAgnostic()
|
||||
};
|
||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.Extras.Subtitles
|
||||
{
|
||||
"Series Title - S01E01.1.en.srt",
|
||||
"Series Title - S01E01.2.en.srt",
|
||||
"Series Title - S01E01.3.en.srt",
|
||||
"Series Title - S01E01.en.forced.srt",
|
||||
"Series Title - S01E01.fr.srt",
|
||||
};
|
||||
|
||||
@@ -126,6 +126,35 @@ namespace NzbDrone.Core.Test.Extras.Subtitles
|
||||
results[i].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputs[i]).AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_import_multiple_subtitle_files_per_language_with_tags()
|
||||
{
|
||||
var files = new List<string>
|
||||
{
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.en.forced.cc.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.other.en.forced.cc.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.en.forced.sdh.srt").AsOsAgnostic(),
|
||||
Path.Combine(_episodeFolder, "Series.Title.S01E01.en.forced.default.srt").AsOsAgnostic(),
|
||||
};
|
||||
|
||||
var expectedOutputs = new[]
|
||||
{
|
||||
"Series Title - S01E01.1.en.forced.cc.srt",
|
||||
"Series Title - S01E01.2.en.forced.cc.srt",
|
||||
"Series Title - S01E01.en.forced.sdh.srt",
|
||||
"Series Title - S01E01.en.forced.default.srt"
|
||||
};
|
||||
|
||||
var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList();
|
||||
|
||||
results.Count().Should().Be(expectedOutputs.Length);
|
||||
|
||||
for (int i = 0; i < expectedOutputs.Length; i++)
|
||||
{
|
||||
results[i].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputs[i]).AsOsAgnostic()).Should().Be(true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("sub.srt", "Series Title - S01E01.srt")]
|
||||
|
||||
66
src/NzbDrone.Core.Test/Files/Indexers/Nyaa/Nyaa2021.xml
Normal file
66
src/NzbDrone.Core.Test/Files/Indexers/Nyaa/Nyaa2021.xml
Normal file
@@ -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();
|
||||
|
||||
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]
|
||||
@@ -339,5 +339,40 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
pageTier2.Url.Query.Should().NotContain("rid=10");
|
||||
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()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "Nyaa",
|
||||
Settings = new NyaaSettings()
|
||||
};
|
||||
{
|
||||
Name = "Nyaa",
|
||||
Settings = new NyaaSettings()
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
/* [Test]
|
||||
// Legacy Nyaa feed test
|
||||
|
||||
public void should_parse_recent_feed_from_Nyaa()
|
||||
{
|
||||
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.InfoHash.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);
|
||||
}*/
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
||||
}
|
||||
|
||||
[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";
|
||||
|
||||
@@ -154,5 +154,28 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
[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]
|
||||
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();
|
||||
}
|
||||
|
||||
[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.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)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.French.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.French.Id);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Spanish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Spanish.Id);
|
||||
}
|
||||
|
||||
[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")]
|
||||
public void should_parse_language_german(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.German.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.German.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Italian.HDTV.XviD-LOL")]
|
||||
[TestCase("Title.the.Series.1x19.ita.720p.bdmux.x264-novarip")]
|
||||
public void should_parse_language_italian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Italian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Italian.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Danish.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_danish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Danish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Danish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Dutch.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_dutch(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Dutch.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Dutch.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Japanese.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_japanese(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Japanese.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Japanese.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Icelandic.HDTV.XviD-LOL")]
|
||||
[TestCase("Title.the.Series.S01E03.1080p.WEB-DL.DD5.1.H.264-SbR Icelandic")]
|
||||
public void should_parse_language_icelandic(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Icelandic.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Icelandic.Id);
|
||||
}
|
||||
|
||||
[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][中日双语字幕]")]
|
||||
public void should_parse_language_chinese(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Chinese.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Chinese.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Korean.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_korean(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Korean.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Korean.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Russian.HDTV.XviD-LOL")]
|
||||
[TestCase("Title.the.Series.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike")]
|
||||
public void should_parse_language_russian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Russian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Russian.Id);
|
||||
}
|
||||
|
||||
[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")]
|
||||
public void should_parse_language_polish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Polish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Polish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Vietnamese.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_vietnamese(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Vietnamese.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Vietnamese.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Swedish.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_swedish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Swedish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Swedish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Norwegian.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_norwegian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Norwegian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Norwegian.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Finnish.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_finnish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Finnish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Finnish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Turkish.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_turkish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Turkish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Turkish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.2009.S01E14.Portuguese.HDTV.XviD-LOL")]
|
||||
public void should_parse_language_portuguese(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Portuguese.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Portuguese.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.S01E01.FLEMISH.HDTV.x264-BRiGAND")]
|
||||
public void should_parse_language_flemish(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Flemish.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Flemish.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.S03E13.Greek.PDTV.XviD-Ouzo")]
|
||||
public void should_parse_language_greek(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Greek.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Greek.Id);
|
||||
}
|
||||
|
||||
[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")]
|
||||
public void should_parse_language_hungarian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Hungarian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Hungarian.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.S01-03.DVDRip.HebDub")]
|
||||
public void should_parse_language_hebrew(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Hebrew.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Hebrew.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.S05E01.WEBRip.x264.AC3.LT.EN-CNN")]
|
||||
public void should_parse_language_lithuanian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Lithuanian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Lithuanian.Id);
|
||||
}
|
||||
|
||||
[TestCase("Title.the.Series.S07E11.WEB Rip.XviD.Louige-CZ.EN.5.1")]
|
||||
public void should_parse_language_czech(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Czech.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Czech.Id);
|
||||
}
|
||||
|
||||
[TestCase("Series Title.S01.ARABIC.COMPLETE.720p.NF.WEBRip.x264-PTV")]
|
||||
public void should_parse_language_arabic(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Arabic.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
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 Final Sonarr (2020) S04 Complete 720p NF WEBRip [Hindi+English] Dual audio")]
|
||||
public void should_parse_language_hindi(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Hindi.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Id.Should().Be(Language.Hindi.Id);
|
||||
}
|
||||
|
||||
[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")]
|
||||
public void should_parse_language_bulgarian(string postTitle)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Id.Should().Be(Language.Bulgarian.Id);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
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")]
|
||||
@@ -280,8 +305,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[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)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Language.Name.Should().Be(Language.English.Name);
|
||||
var result = LanguageParser.ParseLanguage(postTitle);
|
||||
result.Name.Should().Be(Language.English.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,5 +518,46 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.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("[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.Title.S02E02.This.Year.Will.Be.Different.1080p.WEB.H 265", false)]
|
||||
public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
||||
@@ -255,6 +256,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("The Series 42 S09E13 1.54 GB WEB-RIP 1080p Dual-Audio 2019 MKV", false)]
|
||||
[TestCase("Series.Title.1x04.ITA.1080p.WEBMux.x264-NovaRip", false)]
|
||||
[TestCase("Series.Title.2019.S02E07.Chapter.15.The.Believer.4Kto1080p.DSNYP.Webrip.x265.10bit.EAC3.5.1.Atmos.GokiTAoE", false)]
|
||||
[TestCase("Series.Title.S01.1080p.AMZN.WEB-Rip.DDP5.1.H.264-Telly", false)]
|
||||
public void should_parse_webrip1080p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Quality.WEBRip1080p, proper);
|
||||
@@ -267,6 +269,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[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("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)
|
||||
{
|
||||
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 Title - Series 1 (1970) DivX", "Series Title", 1)]
|
||||
[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)
|
||||
{
|
||||
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("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("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)]
|
||||
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("【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("[风车字幕组][名侦探柯南][857][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||
[TestCase("[风车字幕组][名侦探柯南][857集][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||
public void should_parse_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
||||
{
|
||||
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("[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("[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)
|
||||
{
|
||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
||||
|
||||
@@ -39,9 +39,13 @@ namespace NzbDrone.Core.Configuration
|
||||
string SslCertHash { get; }
|
||||
string UrlBase { get; }
|
||||
string UiFolder { get; }
|
||||
string InstanceName { get; }
|
||||
bool UpdateAutomatically { get; }
|
||||
UpdateMechanism UpdateMechanism { get; }
|
||||
string UpdateScriptPath { get; }
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
}
|
||||
|
||||
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 => "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);
|
||||
|
||||
@@ -209,7 +226,13 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
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(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(170)]
|
||||
public class add_language_tags_to_subtitle_files : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("SubtitleFiles").AddColumn("LanguageTags").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(172)]
|
||||
public class add_SeasonSearchMaximumSingleEpisodeAge_to_indexers : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Indexers").AddColumn("SeasonSearchMaximumSingleEpisodeAge").AsInt32().NotNullable().WithDefaultValue(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
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)
|
||||
{
|
||||
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(ParsedEpisodeInfo), 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(OsPath), new OsPathConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -43,18 +44,47 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
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);
|
||||
|
||||
if (qualityDefinition.MinSize.HasValue)
|
||||
{
|
||||
var minSize = qualityDefinition.MinSize.Value.Megabytes();
|
||||
|
||||
//Multiply maxSize by Series.Runtime
|
||||
minSize = minSize * subject.Series.Runtime * subject.Episodes.Count;
|
||||
// Multiply maxSize by Series.Runtime
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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");
|
||||
}
|
||||
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
|
||||
{
|
||||
var maxSize = qualityDefinition.MaxSize.Value.Megabytes();
|
||||
|
||||
//Multiply maxSize by Series.Runtime
|
||||
maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count;
|
||||
// Multiply maxSize by Series.Runtime
|
||||
maxSize = maxSize * runtime * subject.Episodes.Count;
|
||||
|
||||
if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard)
|
||||
{
|
||||
Episode episode = subject.Episodes.First();
|
||||
List<Episode> seasonEpisodes;
|
||||
var firstEpisode = subject.Episodes.First();
|
||||
var seasonEpisodes = GetSeasonEpisodes(subject, searchCriteria);
|
||||
|
||||
var seasonSearchCriteria = searchCriteria as SeasonSearchCriteria;
|
||||
if (seasonSearchCriteria != null && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == episode.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))
|
||||
// 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 == firstEpisode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == firstEpisode.Id))
|
||||
{
|
||||
_logger.Debug("Possible double episode, doubling allowed size.");
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class SeasonPackOnlySpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeasonPackOnlySpecification(IConfigService configService, Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria == null || searchCriteria.Episodes.Count == 1)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (subject.Release.SeasonSearchMaximumSingleEpisodeAge > 0)
|
||||
{
|
||||
if (subject.Series.SeriesType == SeriesTypes.Standard && !subject.ParsedEpisodeInfo.FullSeason && subject.Episodes.Count >= 1)
|
||||
{
|
||||
// test against episodes of the same season in the current search, and make sure they have an air date
|
||||
var subset = searchCriteria.Episodes.Where(e => e.AirDateUtc.HasValue && e.SeasonNumber == subject.Episodes.First().SeasonNumber).ToList();
|
||||
|
||||
if (subset.Count() > 0 && subset.Max(e => e.AirDateUtc).Value.Before(DateTime.UtcNow - TimeSpan.FromDays(subject.Release.SeasonSearchMaximumSingleEpisodeAge)))
|
||||
{
|
||||
_logger.Debug("Release {0}: last episode in this season aired more than {1} days ago, season pack required.", subject.Release.Title, subject.Release.SeasonSearchMaximumSingleEpisodeAge);
|
||||
return Decision.Reject("Last episode in this season aired more than {0} days ago, season pack required.", subject.Release.SeasonSearchMaximumSingleEpisodeAge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user