1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-15 15:54:35 -04:00

Compare commits

...

45 Commits

Author SHA1 Message Date
Mark McDowall
2639c069bc Bumped package version to 3.0.10 2023-03-12 23:52:39 -07:00
Mark McDowall
d185783987 Fixed: Migrations on SQLite 3.41.0 2023-03-12 23:52:39 -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
86 changed files with 1304 additions and 265 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

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

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

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

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

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

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

View File

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

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

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

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

@@ -34,7 +34,7 @@ export const defaultState = {
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
recentFolders: [],
importMode: 'move',
importMode: 'chooseImportMode',
sortPredicates: {
relativePath: function(item, direction) {
const relativePath = item.relativePath;

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

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

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

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

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

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.With(e => e.AbsoluteEpisodeNumber = 100)
.Build();
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
_episodeFile = new EpisodeFile { Id = 5, Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
@@ -47,16 +47,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
}
[Test]
public void should_not_recursively_include_current_filename()
{
_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");
}
[Test]
public void should_include_original_title_if_not_current_file_name()
{
@@ -79,13 +69,108 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
}
[Test]
public void should_include_current_filename_if_not_including_multiple_naming_tokens()
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}";
_namingConfig.StandardEpisodeFormat = "{Original Title} {Quality Title}";
Subject.BuildFileName(new List<Episode> { _episode }, _series, _episodeFile)
.Should().Be("My Series - S15E06 - City Sushi");
.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

@@ -60,15 +60,17 @@ namespace NzbDrone.Core.Test.ParserTests
[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")]
@@ -77,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")]
@@ -133,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")]
@@ -164,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")]
@@ -229,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")]
@@ -274,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")]
@@ -285,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);
@@ -267,6 +268,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

@@ -149,6 +149,8 @@ namespace NzbDrone.Core.Test.ParserTests
[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,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

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

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

@@ -14,6 +14,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr
public int Year { get; set; }
public string TitleSlug { get; set; }
public int QualityProfileId { get; set; }
public int LanguageProfileId { get; set; }
public HashSet<int> Tags { get; set; }
}

View File

@@ -39,6 +39,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr
foreach (var item in remoteSeries)
{
if ((!Settings.ProfileIds.Any() || Settings.ProfileIds.Contains(item.QualityProfileId)) &&
(!Settings.LanguageProfileIds.Any() || Settings.LanguageProfileIds.Contains(item.LanguageProfileId)) &&
(!Settings.TagIds.Any() || Settings.TagIds.Any(tagId => item.Tags.Any(itemTagId => itemTagId == tagId))))
{
series.Add(new ImportListItemInfo
@@ -74,16 +75,33 @@ namespace NzbDrone.Core.ImportLists.Sonarr
{
Settings.Validate().Filter("ApiKey").ThrowOnError();
var profiles = _sonarrV3Proxy.GetProfiles(Settings);
var profiles = _sonarrV3Proxy.GetQualityProfiles(Settings);
return new
{
options = profiles.OrderBy(d => d.Name, StringComparer.InvariantCultureIgnoreCase)
.Select(d => new
{
value = d.Id,
name = d.Name
})
.Select(d => new
{
value = d.Id,
name = d.Name
})
};
}
if (action == "getLanguageProfiles")
{
Settings.Validate().Filter("ApiKey").ThrowOnError();
var langProfiles = _sonarrV3Proxy.GetLanguageProfiles(Settings);
return new
{
options = langProfiles.OrderBy(d => d.Name, StringComparer.InvariantCultureIgnoreCase)
.Select(d => new
{
value = d.Id,
name = d.Name
})
};
}

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.ImportLists.Sonarr
BaseUrl = "";
ApiKey = "";
ProfileIds = new int[] { };
LanguageProfileIds = new int[] { };
TagIds = new int[] { };
}
@@ -32,10 +33,13 @@ namespace NzbDrone.Core.ImportLists.Sonarr
[FieldDefinition(1, Label = "API Key", HelpText = "Apikey of the Sonarr V3 instance to import from")]
public string ApiKey { get; set; }
[FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "Profiles", HelpText = "Profiles from the source instance to import from")]
[FieldDefinition(2, Type = FieldType.Select, SelectOptionsProviderAction = "getProfiles", Label = "Quality Profiles", HelpText = "Quality Profiles from the source instance to import from")]
public IEnumerable<int> ProfileIds { get; set; }
[FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")]
[FieldDefinition(3, Type = FieldType.Select, SelectOptionsProviderAction = "getLanguageProfiles", Label = "Language Profiles", HelpText = "Language Profiles from the source instance to import from")]
public IEnumerable<int> LanguageProfileIds { get; set; }
[FieldDefinition(4, Type = FieldType.Select, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Tags from the source instance to import from")]
public IEnumerable<int> TagIds { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -12,7 +12,8 @@ namespace NzbDrone.Core.ImportLists.Sonarr
public interface ISonarrV3Proxy
{
List<SonarrSeries> GetSeries(SonarrSettings settings);
List<SonarrProfile> GetProfiles(SonarrSettings settings);
List<SonarrProfile> GetQualityProfiles(SonarrSettings settings);
List<SonarrProfile> GetLanguageProfiles(SonarrSettings settings);
List<SonarrTag> GetTags(SonarrSettings settings);
ValidationFailure Test(SonarrSettings settings);
}
@@ -33,11 +34,16 @@ namespace NzbDrone.Core.ImportLists.Sonarr
return Execute<SonarrSeries>("/api/v3/series", settings);
}
public List<SonarrProfile> GetProfiles(SonarrSettings settings)
public List<SonarrProfile> GetQualityProfiles(SonarrSettings settings)
{
return Execute<SonarrProfile>("/api/v3/qualityprofile", settings);
}
public List<SonarrProfile> GetLanguageProfiles(SonarrSettings settings)
{
return Execute<SonarrProfile>("/api/v3/languageprofile", settings);
}
public List<SonarrTag> GetTags(SonarrSettings settings)
{
return Execute<SonarrTag>("/api/v3/tag", settings);

View File

@@ -324,7 +324,7 @@ namespace NzbDrone.Core.Indexers.Newznab
if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0 && searchCriteria.EpisodeNumber > 0)
{
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.AnimeCategories, "search",
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.AnimeCategories, "tvsearch",
string.Format("&q={0}&season={1}&ep={2}",
NewsnabifyTitle(queryTitle),
searchCriteria.SeasonNumber,

View File

@@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Config;
using NLog.Targets.Syslog;
using NLog.Targets.Syslog.Settings;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Sentry;
@@ -32,6 +34,12 @@ namespace NzbDrone.Core.Instrumentation
else
minimumConsoleLogLevel = LogLevel.Info;
if (_configFileProvider.SyslogServer.IsNotNullOrWhiteSpace())
{
var syslogLevel = LogLevel.FromString(_configFileProvider.SyslogLevel);
SetSyslogParameters(_configFileProvider.SyslogServer, _configFileProvider.SyslogPort, syslogLevel);
}
var rules = LogManager.Configuration.LoggingRules;
//Console
@@ -81,6 +89,24 @@ namespace NzbDrone.Core.Instrumentation
}
}
private void SetSyslogParameters(string syslogServer, int syslogPort, LogLevel minimumLogLevel)
{
var syslogTarget = new SyslogTarget();
syslogTarget.Name = "syslogTarget";
syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
syslogTarget.MessageSend.Udp.Port = syslogPort;
syslogTarget.MessageSend.Udp.Server = syslogServer;
syslogTarget.MessageSend.Udp.ReconnectInterval = 500;
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;
var loggingRule = new LoggingRule("*", minimumLogLevel, syslogTarget);
LogManager.Configuration.AddTarget("syslogTarget", syslogTarget);
LogManager.Configuration.LoggingRules.Add(loggingRule);
}
private List<LogLevel> GetLogLevels()
{
return new List<LogLevel>

View File

@@ -84,6 +84,8 @@ namespace NzbDrone.Core.Languages
public static Language Arabic { get { return new Language(26, "Arabic"); } }
public static Language Hindi { get { return new Language(27, "Hindi"); } }
public static Language Bulgarian { get { return new Language(28, "Bulgarian"); } }
public static Language Malayalam { get { return new Language(29, "Malayalam"); } }
public static Language Ukrainian { get { return new Language(30, "Ukrainian"); } }
public static List<Language> All
@@ -120,7 +122,9 @@ namespace NzbDrone.Core.Languages
Czech,
Arabic,
Hindi,
Bulgarian
Bulgarian,
Malayalam,
Ukrainian
};
}
}

View File

@@ -55,12 +55,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
var firstEpisode = episodes.First();
var episodesInSeason = _episodeService.GetEpisodesBySeason(firstEpisode.SeriesId, firstEpisode.EpisodeNumber);
var allEpisodesOnTheSameDay = firstEpisode.AirDateUtc.HasValue && episodes.All(e =>
e.AirDateUtc.HasValue &&
!e.AirDateUtc.HasValue ||
e.AirDateUtc.Value == firstEpisode.AirDateUtc.Value);
if (episodeTitleRequired == EpisodeTitleRequiredType.BulkSeasonReleases &&
allEpisodesOnTheSameDay &&
episodesInSeason.Count(e => e.AirDateUtc.HasValue &&
episodesInSeason.Count(e => !e.AirDateUtc.HasValue ||
e.AirDateUtc.Value == firstEpisode.AirDateUtc.Value
) < 4
)
@@ -74,9 +74,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
var airDateUtc = episode.AirDateUtc;
var title = episode.Title;
if (airDateUtc.HasValue && airDateUtc.Value.Before(DateTime.UtcNow.AddDays(-1)))
if (airDateUtc.HasValue && airDateUtc.Value.Before(DateTime.UtcNow.AddHours(-48)))
{
_logger.Debug("Episode aired more than 1 day ago");
_logger.Debug("Episode aired more than 48 hours ago");
continue;
}

View File

@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Organizer
private readonly ICached<AbsoluteEpisodeFormat[]> _absoluteEpisodeFormatCache;
private readonly ICached<bool> _requiresEpisodeTitleCache;
private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache;
private readonly ICached<bool> _patternHasEpisodeIdentifierCache;
private readonly Logger _logger;
private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9+-]+(?<!-)))?(?<suffix>[- ._)\]]*)\}",
@@ -97,6 +98,7 @@ namespace NzbDrone.Core.Organizer
_absoluteEpisodeFormatCache = cacheManager.GetCache<AbsoluteEpisodeFormat[]>(GetType(), "absoluteEpisodeFormat");
_requiresEpisodeTitleCache = cacheManager.GetCache<bool>(GetType(), "requiresEpisodeTitle");
_requiresAbsoluteEpisodeNumberCache = cacheManager.GetCache<bool>(GetType(), "requiresAbsoluteEpisodeNumber");
_patternHasEpisodeIdentifierCache = cacheManager.GetCache<bool>(GetType(), "patternHasEpisodeIdentifier");
_logger = logger;
}
@@ -109,7 +111,7 @@ namespace NzbDrone.Core.Organizer
if (!namingConfig.RenameEpisodes)
{
return GetOriginalTitle(episodeFile, false) + extension;
return GetOriginalTitle(episodeFile, true) + extension;
}
if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard)
@@ -148,7 +150,7 @@ namespace NzbDrone.Core.Organizer
{
var splitPattern = splitPatterns[i];
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
var multipleTokens = TitleRegex.Matches(splitPattern).Count > 1;
var patternHasEpisodeIdentifier = GetPatternHasEpisodeIdentifier(splitPattern);
splitPattern = AddSeasonEpisodeNumberingTokens(splitPattern, tokenHandlers, episodes, namingConfig);
splitPattern = AddAbsoluteNumberingTokens(splitPattern, tokenHandlers, series, episodes, namingConfig);
@@ -159,7 +161,7 @@ namespace NzbDrone.Core.Organizer
AddIdTokens(tokenHandlers, series);
AddEpisodeTokens(tokenHandlers, episodes);
AddEpisodeTitlePlaceholderTokens(tokenHandlers);
AddEpisodeFileTokens(tokenHandlers, episodeFile, multipleTokens);
AddEpisodeFileTokens(tokenHandlers, episodeFile, !patternHasEpisodeIdentifier || episodeFile.Id == 0);
AddQualityTokens(tokenHandlers, series, episodeFile);
AddMediaInfoTokens(tokenHandlers, episodeFile);
AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords);
@@ -585,10 +587,10 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Episode CleanTitle}"] = m => GetEpisodeTitle(GetEpisodeTitles(episodes).Select(CleanTitle).ToList(), "and", maxLength);
}
private void AddEpisodeFileTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile, bool multipleTokens)
private void AddEpisodeFileTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile, bool useCurrentFilenameAsFallback)
{
tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, multipleTokens);
tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, multipleTokens);
tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback);
tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback);
tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr");
}
@@ -931,7 +933,7 @@ namespace NzbDrone.Core.Organizer
private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern)
{
return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
.Select(match => new AbsoluteEpisodeFormat
{
Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-",
@@ -939,6 +941,29 @@ namespace NzbDrone.Core.Organizer
}).ToArray());
}
private bool GetPatternHasEpisodeIdentifier(string pattern)
{
return _patternHasEpisodeIdentifierCache.Get(pattern, () =>
{
if (SeasonEpisodePatternRegex.IsMatch(pattern))
{
return true;
}
if (AbsoluteEpisodePatternRegex.IsMatch(pattern))
{
return true;
}
if (AirDateRegex.IsMatch(pattern))
{
return true;
}
return false;
});
}
private List<string> GetEpisodeTitles(List<Episode> episodes)
{
if (episodes.Count == 1)
@@ -1032,19 +1057,19 @@ namespace NzbDrone.Core.Organizer
return string.Empty;
}
private string GetOriginalTitle(EpisodeFile episodeFile, bool multipleTokens)
private string GetOriginalTitle(EpisodeFile episodeFile, bool useCurrentFilenameAsFallback)
{
if (episodeFile.SceneName.IsNullOrWhiteSpace())
{
return GetOriginalFileName(episodeFile, multipleTokens);
return GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback);
}
return episodeFile.SceneName;
}
private string GetOriginalFileName(EpisodeFile episodeFile, bool multipleTokens)
private string GetOriginalFileName(EpisodeFile episodeFile, bool useCurrentFilenameAsFallback)
{
if (multipleTokens)
if (!useCurrentFilenameAsFallback)
{
return string.Empty;
}

View File

@@ -36,7 +36,9 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("cs", "ces", Language.Czech),
new IsoLanguage("ar", "ara", Language.Arabic),
new IsoLanguage("hi", "hin", Language.Hindi),
new IsoLanguage("bg", "bul", Language.Bulgarian)
new IsoLanguage("bg", "bul", Language.Bulgarian),
new IsoLanguage("ml", "mal", Language.Malayalam),
new IsoLanguage("uk", "ukr", Language.Ukrainian),
};
public static IsoLanguage Find(string isoCode)

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Parser
new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano)\b)",
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano)\b)|(?<ukrainian>\b(?:ukr)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b))(?:(?i)(?![\W|_|^]SUB))",
@@ -96,6 +96,12 @@ namespace NzbDrone.Core.Parser
if (lowerTitle.Contains("hindi"))
return Language.Hindi;
if (lowerTitle.Contains("malayalam"))
return Language.Malayalam;
if (lowerTitle.Contains("ukrainian"))
return Language.Ukrainian;
if (lowerTitle.Contains("bulgarian"))
return Language.Bulgarian;
@@ -203,6 +209,12 @@ namespace NzbDrone.Core.Parser
if (match.Groups["bulgarian"].Success)
return Language.Bulgarian;
if (match.Groups["ukrainian"].Success)
return Language.Ukrainian;
if (match.Groups["spanish"].Success)
return Language.Spanish;
return Language.Unknown;
}
}

View File

@@ -9,7 +9,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Languages;
using System.Text;
namespace NzbDrone.Core.Parser
@@ -23,11 +22,14 @@ namespace NzbDrone.Core.Parser
// Korean series without season number, replace with S01Exxx and remove airdate
new RegexReplace(@"\.E(\d{2,4})\.\d{6}\.(.*-NEXT)$", ".S01E$1.$2", RegexOptions.Compiled),
// Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?:(?<subgroup>[^\]]+?)(?:[\u4E00-\u9FCC]+)?)\]\[(?<title>[^\]]+?)(?:\s(?<chinesetitle>[\u4E00-\u9FCC][^\]]*?))\]\[(?:(?:[\u4E00-\u9FCC]+?)?(?<episode>\d{1,4})(?:[\u4E00-\u9FCC]+?)?)\]", "[${subgroup}] ${title} - ${episode} - ", RegexOptions.Compiled),
// Chinese LoliHouse/ZERO/Lilith-Raws releases don't use the expected brackets, normalize using brackets
new RegexReplace(@"^\[(?<subgroup>[^\]]*?(?:LoliHouse|ZERO|Lilith-Raws)[^\]]*?)\](?<title>[^\[\]]+?)(?: - (?<episode>[0-9-]+)\s*|\[第?(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?\])\[", "[${subgroup}][${title}][${episode}][", RegexOptions.Compiled),
// Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>[^\]]*?[\u4E00-\u9FCC][^\]]*?)(?:\]\[|\s*[_/·]\s*))?(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)?(?:END|完)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>[^\]]*?[\u4E00-\u9FCC][^\]]*?)(?:\]\[|\s*[_/·]\s*))?(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?:END|完)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>[^\]]*?[\u4E00-\u9FCC][^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled)
@@ -176,6 +178,10 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title 4-digit Absolute Episode Number [SubGroup]
new Regex(@"^(?<title>.+?)[-_. ]+(?<absoluteepisode>(?<!\d+)\d{4}(?!\d+))[-_. ]\[(?<subgroup>.+?)\]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -193,7 +199,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1
new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)",
new Regex(@"^(?<title>.+?)(?:\W+(?:(?:(?<!\()Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+|\))))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Mini-Series, treated as season 1, episodes are labelled as Part One/Two/Three/...Nine, Part.One, Part_One
@@ -254,10 +260,6 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - Absolute episode number in square brackets
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?\[(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -351,6 +353,10 @@ namespace NzbDrone.Core.Parser
//Season only releases for poorly named anime
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ])?(?<title>.+?)[-_. ]+?[\[(](?:S|Season|Saison|Series)[-_. ]?(?<season>\d{1,2}(?![-_. ]?\d+))(?:[-_. )\]]|$)+(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes without a title, Single episode numbers (S1E1, 1x1)
new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{1}(?!\d+))))",
RegexOptions.IgnoreCase | RegexOptions.Compiled)
};
@@ -424,7 +430,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?|10-bit)\s*?",
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?|10-bit)\s*?",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -450,7 +456,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$",
private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|576p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -555,6 +561,7 @@ namespace NzbDrone.Core.Parser
{
if (replace.TryReplace(ref releaseTitle))
{
Logger.Trace($"Replace regex: {replace}");
Logger.Debug("Substituted with " + releaseTitle);
}
}

View File

@@ -246,7 +246,13 @@ namespace NzbDrone.Core.Parser
if (parsedEpisodeInfo.IsPossibleSceneSeasonSpecial)
{
parsedEpisodeInfo = ParseSpecialEpisodeTitle(parsedEpisodeInfo, parsedEpisodeInfo.ReleaseTitle, series) ?? parsedEpisodeInfo;
var parsedSpecialEpisodeInfo = ParseSpecialEpisodeTitle(parsedEpisodeInfo, parsedEpisodeInfo.ReleaseTitle, series);
if (parsedSpecialEpisodeInfo != null)
{
// Use the season number and disable scene source since the season/episode numbers that were returned are not scene numbers
return GetStandardEpisodes(series, parsedSpecialEpisodeInfo, parsedSpecialEpisodeInfo.SeasonNumber, false, searchCriteria);
}
}
return GetStandardEpisodes(series, parsedEpisodeInfo, mappedSeasonNumber, sceneSource, searchCriteria);

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SourceRegex = new Regex(@"\b(?:
(?<bluray>BluRay|Blu-Ray|HD-?DVD|BDMux|BD(?!$))|
(?<webdl>WEB[-_. ]DL|WEBDL|AmazonHD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|[. ](?-i:WEB)$|\d+0p(?:[-. ]AMZN)?[-. ]WEB[-. ]|WEB-DLMux|\b\s\/\sWEB\s\/\s\b|(?:AMZN|NF|DP)[. ]WEB[. ])|
(?<webdl>WEB[-_. ]DL|WEBDL|AmazonHD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh][ .]?26[45]|DDP?5[. ]1)|[. ](?-i:WEB)$|\d+0p(?:[-. ]AMZN)?[-. ]WEB[-. ]|WEB-DLMux|\b\s\/\sWEB\s\/\s\b|(?:AMZN|NF|DP)[. ]WEB[. ])|
(?<webrip>WebRip|Web-Rip|WEBMux)|
(?<hdtv>HDTV)|
(?<bdrip>BDRip|BDLight)|

View File

@@ -42,5 +42,10 @@ namespace NzbDrone.Core.Parser
input = _regex.Replace(input, _replacementFormat);
return result;
}
public override string ToString()
{
return _regex.ToString();
}
}
}

View File

@@ -0,0 +1,14 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Qualities.Commands
{
public class ResetQualityDefinitionsCommand : Command
{
public bool ResetTitles { get; set; }
public ResetQualityDefinitionsCommand(bool resetTitles = false)
{
ResetTitles = resetTitles;
}
}
}

View File

@@ -14,7 +14,5 @@ namespace NzbDrone.Core.Qualities
: base(database, eventAggregator)
{
}
}
}

View File

@@ -5,6 +5,8 @@ using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using System;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Qualities.Commands;
namespace NzbDrone.Core.Qualities
{
@@ -17,7 +19,7 @@ namespace NzbDrone.Core.Qualities
QualityDefinition Get(Quality quality);
}
public class QualityDefinitionService : IQualityDefinitionService, IHandle<ApplicationStartedEvent>
public class QualityDefinitionService : IQualityDefinitionService, IExecute<ResetQualityDefinitionsCommand>, IHandle<ApplicationStartedEvent>
{
private readonly IQualityDefinitionRepository _repo;
private readonly ICached<Dictionary<Quality, QualityDefinition>> _cache;
@@ -106,5 +108,28 @@ namespace NzbDrone.Core.Qualities
InsertMissingDefinitions();
}
public void Execute(ResetQualityDefinitionsCommand message)
{
List<QualityDefinition> updateList = new List<QualityDefinition>();
var allDefinitions = Quality.DefaultQualityDefinitions.OrderBy(d => d.Weight).ToList();
var existingDefinitions = _repo.All().ToList();
foreach (var definition in allDefinitions)
{
var existing = existingDefinitions.SingleOrDefault(d => d.Quality == definition.Quality);
existing.MinSize = definition.MinSize;
existing.MaxSize = definition.MaxSize;
existing.Title = message.ResetTitles ? definition.Title : existing.Title;
updateList.Add(existing);
}
_repo.UpdateMany(updateList);
_cache.Clear();
}
}
}

View File

@@ -37,7 +37,8 @@ namespace NzbDrone.Core.Tv
//Get all items less than the cutoff
foreach (var profile in profiles)
{
var cutoffIndex = profile.GetIndex(profile.Cutoff);
var cutoff = profile.UpgradeAllowed ? profile.Cutoff : profile.FirststAllowedQuality().Id;
var cutoffIndex = profile.GetIndex(cutoff);
var belowCutoff = profile.Items.Take(cutoffIndex.Index).ToList();
if (belowCutoff.Any())
@@ -48,7 +49,8 @@ namespace NzbDrone.Core.Tv
foreach (var profile in languageProfiles)
{
var languageCutoffIndex = profile.Languages.FindIndex(v => v.Language == profile.Cutoff);
var languageCutoff = profile.UpgradeAllowed ? profile.Cutoff : profile.FirstAllowedLanguage();
var languageCutoffIndex = profile.Languages.FindIndex(v => v.Language == languageCutoff);
var belowLanguageCutoff = profile.Languages.Take(languageCutoffIndex).ToList();
if (belowLanguageCutoff.Any())

View File

@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Tv
private readonly ISeriesService _seriesService;
public SeriesTitleSlugValidator(ISeriesService seriesService)
: base("Title slug '{slug}' is in use by series '{seriesTitle}'")
: base("Title slug '{slug}' is in use by series '{seriesTitle}'. Check the FAQ for more information")
{
_seriesService = seriesService;
}

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Validation.Paths
public StartupFolderValidator(IAppFolderInfo appFolderInfo)
: base("Path cannot be an ancestor of the start up folder")
: base("Path cannot be {relationship} the start up folder")
{
_appFolderInfo = appFolderInfo;
}
@@ -19,7 +19,24 @@ namespace NzbDrone.Core.Validation.Paths
{
if (context.PropertyValue == null) return true;
return !_appFolderInfo.StartUpFolder.IsParentPath(context.PropertyValue.ToString());
var startupFolder = _appFolderInfo.StartUpFolder;
var folder = context.PropertyValue.ToString();
if (startupFolder.PathEquals(folder))
{
context.MessageFormatter.AppendArgument("relationship", "set to");
return false;
}
if (startupFolder.IsParentPath(folder))
{
context.MessageFormatter.AppendArgument("relationship", "child of");
return false;
}
return true;
}
}
}

View File

@@ -62,5 +62,11 @@ namespace NzbDrone.Core.Validation
{
return ruleBuilder.WithState(v => NzbDroneValidationState.Warning);
}
public static IRuleBuilderOptions<T, string> StartsOrEndsWithSonarr<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator("^Sonarr|Sonarr$")).WithMessage("Must start or end with Sonarr");
}
}
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests
[Test, Order(1)]
public void cutoff_should_have_monitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
EnsureProfileCutoff(1, Quality.HDTV720p, true);
var series = EnsureSeries(266189, "The Blacklist", true);
EnsureEpisodeFile(series, 1, 1, Quality.SDTV);
@@ -23,7 +23,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests
[Test, Order(1)]
public void cutoff_should_not_have_unmonitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
EnsureProfileCutoff(1, Quality.HDTV720p, true);
var series = EnsureSeries(266189, "The Blacklist", false);
EnsureEpisodeFile(series, 1, 1, Quality.SDTV);
@@ -35,7 +35,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests
[Test, Order(1)]
public void cutoff_should_have_series()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
EnsureProfileCutoff(1, Quality.HDTV720p, true);
var series = EnsureSeries(266189, "The Blacklist", true);
EnsureEpisodeFile(series, 1, 1, Quality.SDTV);
@@ -48,7 +48,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests
[Test, Order(2)]
public void cutoff_should_have_unmonitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
EnsureProfileCutoff(1, Quality.HDTV720p, true);
var series = EnsureSeries(266189, "The Blacklist", false);
EnsureEpisodeFile(series, 1, 1, Quality.SDTV);

View File

@@ -308,13 +308,25 @@ namespace NzbDrone.Integration.Test
return result.EpisodeFile;
}
public ProfileResource EnsureProfileCutoff(int profileId, Quality cutoff)
public ProfileResource EnsureProfileCutoff(int profileId, Quality cutoff, bool upgradeAllowed)
{
var needsUpdate = false;
var profile = Profiles.Get(profileId);
if (profile.Cutoff != cutoff)
{
profile.Cutoff = cutoff;
needsUpdate = true;
}
if (profile.UpgradeAllowed != upgradeAllowed)
{
profile.UpgradeAllowed = upgradeAllowed;
needsUpdate = true;
}
if (needsUpdate)
{
profile = Profiles.Put(profile);
}

View File

@@ -2,11 +2,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 Nancy;
using Nancy.Responses;
using NzbDrone.Common.Extensions;
@@ -86,7 +84,7 @@ namespace Sonarr.Api.V3.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

@@ -38,6 +38,7 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.Port).ValidPort();
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
SharedValidator.RuleFor(c => c.InstanceName).StartsOrEndsWithSonarr();
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);

View File

@@ -24,6 +24,7 @@ namespace Sonarr.Api.V3.Config
public string ApiKey { get; set; }
public string SslCertHash { get; set; }
public string UrlBase { get; set; }
public string InstanceName { get; set; }
public bool UpdateAutomatically { get; set; }
public UpdateMechanism UpdateMechanism { get; set; }
public string UpdateScriptPath { get; set; }
@@ -63,6 +64,7 @@ namespace Sonarr.Api.V3.Config
ApiKey = model.ApiKey,
SslCertHash = model.SslCertHash,
UrlBase = model.UrlBase,
InstanceName = model.InstanceName,
UpdateAutomatically = model.UpdateAutomatically,
UpdateMechanism = model.UpdateMechanism,
UpdateScriptPath = model.UpdateScriptPath,

View File

@@ -16,6 +16,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
public int VideoBitrate { get; set; }
public string VideoCodec { get; set; }
public decimal VideoFps { get; set; }
public string VideoDynamicRange { get; set; }
public string VideoDynamicRangeType { get; set; }
public string Resolution { get; set; }
public string RunTime { get; set; }
public string ScanType { get; set; }
@@ -42,6 +44,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
VideoBitrate = model.VideoBitrate,
VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName),
VideoFps = model.VideoFps,
VideoDynamicRange = MediaInfoFormatter.FormatVideoDynamicRange(model),
VideoDynamicRangeType = MediaInfoFormatter.FormatVideoDynamicRangeType(model),
Resolution = $"{model.Width}x{model.Height}",
RunTime = FormatRuntime(model.RunTime),
ScanType = model.ScanType,

View File

@@ -1,17 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
using NzbDrone.SignalR;
using Sonarr.Http;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Qualities
{
public class QualityDefinitionModule : SonarrRestModule<QualityDefinitionResource>
public class QualityDefinitionModule : SonarrRestModuleWithSignalR<QualityDefinitionResource, QualityDefinition>, IHandle<CommandExecutedEvent>
{
private readonly IQualityDefinitionService _qualityDefinitionService;
public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService)
public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster)
: base(signalRBroadcaster)
{
_qualityDefinitionService = qualityDefinitionService;
@@ -50,5 +54,13 @@ namespace Sonarr.Api.V3.Qualities
.ToResource()
, HttpStatusCode.Accepted);
}
public void Handle(CommandExecutedEvent message)
{
if (message.Command.Name == "ResetQualityDefinitions")
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}
}

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

@@ -51,6 +51,7 @@ namespace Sonarr.Api.V3.System
return new
{
AppName = BuildInfo.AppName,
InstanceName = _configFileProvider.InstanceName,
Version = BuildInfo.Version.ToString(),
BuildTime = BuildInfo.BuildDateTime,
IsDebug = BuildInfo.IsDebug,

View File

@@ -61,6 +61,7 @@ namespace Sonarr.Http.Frontend
builder.AppendLine($" apiKey: '{_apiKey}',");
builder.AppendLine($" release: '{BuildInfo.Release}',");
builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',");
builder.AppendLine($" instanceName: '{_configFileProvider.InstanceName.ToString()}',");
builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',");
builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},");
builder.AppendLine($" urlBase: '{_urlBase}',");

View File

@@ -1,7 +1,7 @@
#! /bin/bash
# Increment packageVersion when package scripts change
packageVersion='3.0.8'
packageVersion='3.0.10'
# For now we keep the build version and package version the same
buildVersion=$packageVersion