1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

89 Commits

Author SHA1 Message Date
Mark McDowall
67e2fe551a Bumped package version to 3.0.10 2023-03-06 10:28:17 -08:00
Mark McDowall
5db5b1dace Fixed: Migrations on SQLite 3.41.0 2023-03-06 07:27:50 -08:00
bakerboy448
c7919f80de Fixed: Improve WebDL and WebRip Parsing 2022-08-07 13:43:04 -07:00
David Newhall
1a1d427c42 Fixed: Logging when series folder is moved successfully 2022-08-07 12:03:27 -07:00
bakerboy448
9263fc1564 New: Improve messaging for rejected quality upgrades 2022-08-07 11:54:15 -07:00
Kevin Richter
8ab040f612 Fixed: Improve moving file to location where another one exists 2022-08-07 11:52:54 -07:00
Dominik Krivohlavek
d6dff451e0 New: Preserve language tags when importing subtitle files
Closes #2570
Closes #3278
2022-08-07 11:47:14 -07:00
C.J. Manca
ac7afc351c New: Add maximum single episode age option (per indexer) 2022-08-07 11:43:18 -07:00
Mark McDowall
e4c0e80e3e Bumped package version to 3.0.9 2022-08-06 15:36:38 -07:00
Qstick
46bc711558 Fixed: Releases Size filter has incorrect value type
Closes #5085
2022-07-29 18:37:21 -08:00
Mark McDowall
e35e24a4c2 Fixed broken unicode tests 2022-07-06 08:10:25 -07:00
Mark McDowall
7eb61fafa4 Fixed: Parsing of Chinese anime releases with character after episode number
Closes #5054
2022-06-25 23:24:23 -07:00
Mark McDowall
ab578566be Log replacement regex 2022-06-25 23:23:13 -07:00
Mark McDowall
35edef91c6 Fixed: Standard episode searches for anime
Closes #5066
2022-06-25 23:05:18 -07:00
Mark McDowall
78dc2a7e13 New: Include Series Match Type in grab event details 2022-06-20 23:23:48 -07:00
Mark McDowall
5d976ac657 LanguageProfileId for Language profile filtering 2022-06-12 15:17:47 -07:00
Qstick
d5fff15f32 New: Reset Quality Definitions to default 2022-06-12 09:49:07 -07:00
Qstick
7c98c2397a New: Instance name in System/Status API endpoint 2022-06-12 09:36:43 -07:00
Qstick
ad6081aec6 New: Instance name for Page Title 2022-06-12 09:36:43 -07:00
Robin Dadswell
1558929484 New: Instance Name used for Syslog 2022-06-12 09:36:43 -07:00
Robin Dadswell
4a2f120bc1 New: Set Instance Name 2022-06-12 09:36:43 -07:00
Robin Dadswell
6c0f22a11e New: Added UDP syslog support
(cherry picked from commit 8d856b2edb8bf46a2b516d5f7644ae3fa1151323)
2022-06-12 09:36:43 -07:00
Qstick
41a821352e New: Sonarr Sync on Language Profile 2022-06-12 09:34:50 -07:00
Qstick
0991cfe27e Fixed: Validate if equals or child for startup folder 2022-06-12 09:33:59 -07:00
bakerboy448
97925feed9 Fixed: Improved parsing WebDL Releases 2022-06-12 09:33:11 -07:00
Mark McDowall
93fc9abae9 Parse another additional Chinese anime release format 2022-05-29 10:16:14 -07:00
Mark McDowall
2ea7b477cb Fixed: Forgot password wiki link 2022-05-29 10:16:14 -07:00
Kathrin De Cecco
c9df12e6bc Fixed: Typo in add indexer modal 2022-05-28 12:59:12 -07:00
Mark McDowall
b542dd0ddd Fixed: Manual Import without selecting Import Mode
Closes #5036
2022-05-27 08:52:06 -07:00
Mark McDowall
c1e5b7f642 Fixed cutoff unmet integration tests 2022-05-23 20:52:27 -07:00
Mark McDowall
ccb88919b9 New: Increase TBA episode title delay to 48 hours (from 24) to deal with TheTVDB's API caching
Closes #4307
2022-05-22 17:20:24 -07:00
Mark McDowall
f9b2c2d843 Fixed: PRevent import if potential bulk season release doesn't have air date for all episodes
Closes #5021
2022-05-22 17:18:19 -07:00
Mark McDowall
d48950ec3c Fixed: Cutoff Unmet showing items above lowest accepted quality when upgrades are disabled 2022-05-22 16:44:12 -07:00
Mark McDowall
6a7d84f134 New: Parse some additional Chinese anime releases
Closes #5024
2022-05-15 15:13:56 -07:00
Mark McDowall
a81a80a00f Fixed: Parsing of single episode numbers
Closes #5022
2022-05-15 14:47:23 -07:00
Mark McDowall
c00cbb9a5a New: Add FAQ to error adding series with duplicate slug
Closes #5020
2022-05-15 14:41:25 -07:00
Mark McDowall
0d739cd26d New: Don't default manual import to move
Closes #5005
2022-05-09 22:23:30 -07:00
Mark McDowall
d01e6d32de Fixed: Original Filename/Title not being included when importing a new file
Closes #5010
2022-05-09 21:04:42 -07:00
Mark McDowall
704cf7aebe New: Rename Protocol to Preferred Protocol in Delay Profiles
Closes #4951
2022-05-09 21:00:58 -07:00
Jure Merhar
52d95fa632 Fixed: Bluray 576p parsing
Closes #5006
2022-04-30 14:32:57 -08:00
Mark McDowall
a71cc1081e Downgrade Ical.Net from 4.2 to 4.1.11 2022-04-30 15:10:47 -07:00
Mark McDowall
edf1167a37 Fixed: Mapping episode 0 to the parsed season instead of specials 2022-04-29 17:27:35 -07:00
Mark McDowall
8f2c4fe4d1 Fixed: Don't parse part # in brackets as mini series style naming
Closes #1265
2022-04-29 00:10:42 -07:00
Mark McDowall
cc9fc1e3c3 New: Use 45 minutes for runtime when episode aired within 24 hours of pilot episode 2022-04-28 23:55:07 -07:00
installemployee
9fb29f42c4 Fixed: iCal status values 2022-04-28 17:27:23 -07:00
Mark McDowall
9a1a320110 Fixed: Not including Original Title/Filename during rename when episode identifiers are missing
Closes #5003
2022-04-28 17:24:21 -07:00
Mark McDowall
f6664b8b42 New: Parse Spanish language using Castellano and Español
Closes #3579
2022-04-24 17:18:50 -07:00
Mark McDowall
82646db70d New: Added Malayalam and Ukrainian languages
Closes #4662
Closes #5000
2022-04-24 17:18:50 -07:00
Mark McDowall
97e40dc00a Fixed: Rename On Download to On Import when editing connections 2022-04-24 13:20:31 -07:00
Mark McDowall
ae0e23fc8e New: Added Mediainfo Video Dynamic Range column for episodes
Closes #4963
2022-04-24 13:18:48 -07:00
Mark McDowall
893a6744ac New: Add The TVDB link to library import search results
Closes #4996
2022-04-24 12:30:20 -07:00
Mark McDowall
fa4b80b86f Bumped package version to 3.0.8 2022-04-23 21:18:23 -07:00
Mark McDowall
18f7bcd212 Fixed: QBittorrent unknown download state: forcedMetaDL
Closes #4997
2022-04-23 21:14:06 -07:00
Mark McDowall
458c5cd0b3 Fixed: UI hiding search results with duplicate GUIDs
Closes #4994
2022-04-23 21:08:35 -07:00
Mark McDowall
c93f63cd20 Fixed: Interactive Search Filter not filtering multiple qualities in the same filter row 2022-04-18 18:46:54 -07:00
zodac
bc5a43bd92 Fixed: Typo in Connection Lost modal 2022-04-18 18:12:42 -07:00
Mark McDowall
a6a68b4cae Fixed: A potential issue when extra files for multiple series have the same relative path 2022-04-12 17:46:10 -07:00
Mark McDowall
9183c6b846 Fixed: Importing file from UNC shared folder without job folder
Closes #4943
2022-04-12 17:08:53 -07:00
Mark McDowall
d73ad3e27a Remove copy/pasta VOSTFR parsing as French 2022-04-11 20:25:52 -07:00
Mark McDowall
bd70fa5410 Fixed: Plex Library Updates
Closes #4914
2022-04-11 19:34:02 -07:00
Mark McDowall
481345226a Fixed: Skip extras in 'Extras' subfolder
Closes #4980
2022-04-11 18:18:02 -07:00
Mark McDowall
365c6a7741 New: Added additional terms for matching French language releases
Closes #4949
2022-04-09 22:03:59 -07:00
Mark McDowall
8fa6e5ec6d Fixed: Use Manage Episodes instead of Manual Import for title when managing episodes for a series 2022-04-09 21:52:46 -07:00
Mark McDowall
d376ae2f9f Fixed: Manage Episodes button grayed out if there are no episodes
Closes #4938
2022-04-09 21:51:10 -07:00
issueg2k4g34j2g
103d2751ee New: Update Monotorrent 2022-04-09 21:35:40 -07:00
Mark McDowall
bdd5865876 New: Natural Sorting Manual Import Relative Paths
Closes #4956
2022-04-09 21:19:17 -07:00
bakerboy448
f678775e5c Fixed: Escape Characters as needed for *znab queries
Closes #4788
2022-04-09 21:16:20 -07:00
Douglas R Andreani
5a08d5dc24 New: Add date picker for custom filter dates
Closes #4972
2022-04-09 21:15:02 -07:00
bakerboy448
bba4a5636e Fixed: Clarify Qbit Content Path Error 2022-04-06 17:53:52 -08:00
bakerboy448
8d83b1d8d6 Fixed: API error when sending payload without optional parameters
Co-authored-by: Qstick <qstick@gmail.com>
2022-04-05 17:23:33 -07:00
Marcelo Castagna
3be5d6c258 Fixed: Properly handle 119 error code from Synology Download Station 2022-04-05 17:22:37 -07:00
bakerboy448
40ecdbc12d New: Support for new Nyaa RSS Feed format
Closes #4614
2022-04-05 17:10:26 -07:00
Mark McDowall
6e271e9272 Fixed: Error when trying to import an empty Plex Watchlist 2022-04-05 17:08:58 -07:00
Mark McDowall
5c5b012ded Remove old, broken test 2022-04-05 17:08:28 -07:00
Mark McDowall
be1acfc2f9 Fixed: Re-assigning file that was mapped to multiple episodes to only one of those episodes
Closes #4946
2022-03-28 17:50:11 -07:00
Mark McDowall
ebb48a19cc Improve usage of Original Title renaming token
Fixed: Don't recursively add the current file name to new file name when '{Original Title}' is used in addition to other naming tokens
2022-03-28 17:34:19 -07:00
Mark McDowall
fa9136c4d1 Fixed: Validation when testing indexers, import lists, connections and download clients 2022-03-28 17:34:19 -07:00
Mark McDowall
e7ca98489e Fixed: Default sort key for wanted/missing API endpoint
Closes #4950
2022-03-28 17:34:19 -07:00
Mark McDowall
a3fd3c5e67 Fixed: Clean API request path before authentication 2022-03-28 17:34:19 -07:00
Nial McCallister
cc09f85212 Fixed: Twitter link 2022-03-24 13:50:07 -07:00
Stevie Robinson
581fb2cb3d New: Add optional Source Title column to history 2022-03-24 13:49:37 -07:00
Taloth Saldono
d899225509 Add response size to http responses 2022-03-20 22:19:06 +01:00
Mark McDowall
06464d720c Don't return early after re-running checks after startup grace period 2022-03-20 00:21:07 -07:00
Mark McDowall
d21e9753bc New: Support for parsing some releases from Spanish trackers 2022-03-19 13:04:40 -07:00
Mark McDowall
07f0db477a Fixed: Delay health check notifications on startup
Closes #4381
2022-03-19 13:00:15 -07:00
Mark McDowall
e280897bc7 Fixed: Automatic import of releases when file is not matched to series
Closes #4935
2022-03-19 12:57:00 -07:00
Mark McDowall
bb02fc4668 Fixed: Newznab requests for anime using season/episode numbers 2022-03-19 12:55:45 -07:00
Mark McDowall
e3aa92d09a Fixed: Scrolling in Firefox in small window (requires refresh)
Closes #4403
2022-03-19 12:55:06 -07:00
Mark McDowall
d02d1bbdfe Fixed: Releases without a job folder importing extra files from another release 2022-03-06 22:05:17 -08:00
175 changed files with 2320 additions and 537 deletions

View File

@@ -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>

View File

@@ -217,6 +217,16 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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

View File

@@ -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}>

View File

@@ -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';

View File

@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
<TextInput
name={NAME}
value={filterValue}
type="date"
placeholder="yyyy-mm-dd"
onChange={this.onValueChange}
/>

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import 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;

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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}

View File

@@ -38,6 +38,7 @@
}
.audioLanguages,
.videoDynamicRangeType,
.subtitles {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -20,6 +20,7 @@ const requiresRestartKeys = [
'bindAddress',
'port',
'urlBase',
'instanceName',
'enableSsl',
'sslPort',
'sslCertHash',

View File

@@ -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}

View File

@@ -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">

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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';

View File

@@ -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>

View File

@@ -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,

View File

@@ -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
};
}

View File

@@ -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;

View 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);

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
.messageContainer {
margin-bottom: 20px;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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',

View File

@@ -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',

View File

@@ -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) {

View File

@@ -178,7 +178,8 @@ export const defaultState = {
{
name: 'size',
label: 'Size',
type: filterBuilderTypes.NUMBER
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'seeders',

View File

@@ -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);
}

View File

@@ -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>

View 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;

View File

@@ -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();
});
}

View File

@@ -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;
}

View File

@@ -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
>

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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" />

View File

@@ -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);
}

View 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;
}
}
}

View File

@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
"/boot",
"/lib",
"/sbin",
"/proc"
"/proc",
"/usr/bin"
};
}
}

View File

@@ -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))
{

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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")]

View 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>

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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()
{

View File

@@ -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]");
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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());

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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