mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-15 15:54:35 -04:00
Compare commits
45 Commits
v3.0.8.150
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2639c069bc | ||
|
|
d185783987 | ||
|
|
e4c0e80e3e | ||
|
|
46bc711558 | ||
|
|
e35e24a4c2 | ||
|
|
7eb61fafa4 | ||
|
|
ab578566be | ||
|
|
35edef91c6 | ||
|
|
78dc2a7e13 | ||
|
|
5d976ac657 | ||
|
|
d5fff15f32 | ||
|
|
7c98c2397a | ||
|
|
ad6081aec6 | ||
|
|
1558929484 | ||
|
|
4a2f120bc1 | ||
|
|
6c0f22a11e | ||
|
|
41a821352e | ||
|
|
0991cfe27e | ||
|
|
97925feed9 | ||
|
|
93fc9abae9 | ||
|
|
2ea7b477cb | ||
|
|
c9df12e6bc | ||
|
|
b542dd0ddd | ||
|
|
c1e5b7f642 | ||
|
|
ccb88919b9 | ||
|
|
f9b2c2d843 | ||
|
|
d48950ec3c | ||
|
|
6a7d84f134 | ||
|
|
a81a80a00f | ||
|
|
c00cbb9a5a | ||
|
|
0d739cd26d | ||
|
|
d01e6d32de | ||
|
|
704cf7aebe | ||
|
|
52d95fa632 | ||
|
|
a71cc1081e | ||
|
|
edf1167a37 | ||
|
|
8f2c4fe4d1 | ||
|
|
cc9fc1e3c3 | ||
|
|
9fb29f42c4 | ||
|
|
9a1a320110 | ||
|
|
f6664b8b42 | ||
|
|
82646db70d | ||
|
|
97e40dc00a | ||
|
|
ae0e23fc8e | ||
|
|
893a6744ac |
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.series {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.series {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tvdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tvdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSearchResult.css';
|
||||
|
||||
class ImportSeriesSearchResult extends Component {
|
||||
function ImportSeriesSearchResult(props) {
|
||||
const {
|
||||
tvdbId,
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries,
|
||||
onPress
|
||||
} = props;
|
||||
|
||||
//
|
||||
// Listeners
|
||||
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tvdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.series}
|
||||
onPress={this.onPress}
|
||||
onPress={onPressCallback}
|
||||
>
|
||||
<ImportSeriesTitle
|
||||
title={title}
|
||||
@@ -36,8 +31,19 @@ class ImportSeriesSearchResult extends Component {
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.tvdbLink}
|
||||
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tvdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesSearchResult.propTypes = {
|
||||
|
||||
@@ -20,24 +20,27 @@ function ImportSeriesTitle(props) {
|
||||
|
||||
{
|
||||
!title.contains(year) &&
|
||||
year > 0 &&
|
||||
year > 0 ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label>{network}</Label>
|
||||
network ?
|
||||
<Label>{network}</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
isExistingSeries ?
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Sonarr">
|
||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -19,7 +19,7 @@ import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import Settings from 'Settings/Settings';
|
||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||
import Profiles from 'Settings/Profiles/Profiles';
|
||||
import Quality from 'Settings/Quality/Quality';
|
||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
@@ -158,7 +158,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/quality"
|
||||
component={Quality}
|
||||
component={QualityConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||
|
||||
function getState(status) {
|
||||
switch (status) {
|
||||
@@ -70,6 +71,7 @@ const mapDispatchToProps = {
|
||||
dispatchUpdateItem: updateItem,
|
||||
dispatchRemoveItem: removeItem,
|
||||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
@@ -221,6 +223,10 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleQualitydefinition = () => {
|
||||
this.props.dispatchFetchQualityDefinitions();
|
||||
}
|
||||
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.dispatchFetchQueue();
|
||||
@@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
|
||||
dispatchUpdateItem: PropTypes.func.isRequired,
|
||||
dispatchRemoveItem: PropTypes.func.isRequired,
|
||||
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
|
||||
@@ -103,6 +103,7 @@ class VirtualTable extends Component {
|
||||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowHeight,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -153,7 +154,7 @@ class VirtualTable extends Component {
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
@@ -194,7 +195,8 @@ VirtualTable.propTypes = {
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38
|
||||
headerHeight: 38,
|
||||
rowHeight: ROW_HEIGHT
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
||||
@@ -32,7 +32,8 @@ function MediaInfo(props) {
|
||||
audioCodec,
|
||||
audioLanguages,
|
||||
subtitles,
|
||||
videoCodec
|
||||
videoCodec,
|
||||
videoDynamicRangeType
|
||||
} = props;
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO) {
|
||||
@@ -72,6 +73,14 @@ function MediaInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||
return (
|
||||
<span>
|
||||
{videoDynamicRangeType}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,7 +90,8 @@ MediaInfo.propTypes = {
|
||||
audioCodec: PropTypes.string,
|
||||
audioLanguages: PropTypes.string,
|
||||
subtitles: PropTypes.string,
|
||||
videoCodec: PropTypes.string
|
||||
videoCodec: PropTypes.string,
|
||||
videoDynamicRangeType: PropTypes.string
|
||||
};
|
||||
|
||||
export default MediaInfo;
|
||||
|
||||
@@ -2,3 +2,4 @@ export const AUDIO = 'audio';
|
||||
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||
export const SUBTITLES = 'subtitles';
|
||||
export const VIDEO = 'video';
|
||||
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||
|
||||
@@ -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' }
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
}
|
||||
|
||||
.audioLanguages,
|
||||
.videoDynamicRangeType,
|
||||
.subtitles {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -238,6 +238,20 @@ class EpisodeRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'videoDynamicRangeType') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.videoDynamicRangeType}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -20,6 +20,7 @@ const requiresRestartKeys = [
|
||||
'bindAddress',
|
||||
'port',
|
||||
'urlBase',
|
||||
'instanceName',
|
||||
'enableSsl',
|
||||
'sslPort',
|
||||
'sslCertHash',
|
||||
|
||||
@@ -19,6 +19,7 @@ function HostSettings(props) {
|
||||
bindAddress,
|
||||
port,
|
||||
urlBase,
|
||||
instanceName,
|
||||
enableSsl,
|
||||
sslPort,
|
||||
sslCertHash,
|
||||
@@ -71,6 +72,22 @@ function HostSettings(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Instance Name</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="instanceName"
|
||||
helpText="Instance name in tab and for Syslog app name"
|
||||
helpTextWarning="Requires restart to take effect"
|
||||
onChange={onInputChange}
|
||||
{...instanceName}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
|
||||
@@ -51,7 +51,7 @@ class AddIndexerModalContent extends Component {
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.</div>
|
||||
<div>For more information on the individual indexers, clink on the info buttons.</div>
|
||||
<div>For more information on the individual indexers, click on the info buttons.</div>
|
||||
</Alert>
|
||||
|
||||
<FieldSet legend="Usenet">
|
||||
|
||||
@@ -139,7 +139,7 @@ class MediaManagement extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="episodeTitleRequired"
|
||||
helpText="Prevent importing for up to 24 hours if the episode title is in the naming format and the episode title is TBA"
|
||||
helpText="Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA"
|
||||
values={episodeTitleRequiredOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.episodeTitleRequired}
|
||||
|
||||
@@ -59,7 +59,7 @@ function NotificationEventItems(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onDownload"
|
||||
helpText="On Download"
|
||||
helpText="On Import"
|
||||
isDisabled={!supportsOnDownload.value}
|
||||
{...onDownload}
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -84,7 +84,7 @@ class DelayProfile extends Component {
|
||||
connectDragSource
|
||||
} = this.props;
|
||||
|
||||
let preferred = titleCase(preferredProtocol);
|
||||
let preferred = `Prefer ${titleCase(preferredProtocol)}`;
|
||||
|
||||
if (!enableUsenet) {
|
||||
preferred = 'Only Torrent';
|
||||
|
||||
@@ -81,7 +81,7 @@ class DelayProfiles extends Component {
|
||||
>
|
||||
<div>
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>Protocol</div>
|
||||
<div className={styles.column}>Preferred Protocol</div>
|
||||
<div className={styles.column}>Usenet Delay</div>
|
||||
<div className={styles.column}>Torrent Delay</div>
|
||||
<div className={styles.tags}>Tags</div>
|
||||
|
||||
@@ -16,6 +16,13 @@ import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import styles from './EditDelayProfileModalContent.css';
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
||||
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
||||
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
||||
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
||||
];
|
||||
|
||||
function EditDelayProfileModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
@@ -25,7 +32,6 @@ function EditDelayProfileModalContent(props) {
|
||||
saveError,
|
||||
item,
|
||||
protocol,
|
||||
protocolOptions,
|
||||
onInputChange,
|
||||
onProtocolChange,
|
||||
onSavePress,
|
||||
@@ -51,20 +57,22 @@ function EditDelayProfileModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
isFetching ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to add a new quality profile, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<div>Unable to add a new quality profile, please try again.</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
!isFetching && !error ?
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Protocol</FormLabel>
|
||||
<FormLabel>Preferred Protocol</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -140,19 +148,21 @@ function EditDelayProfileModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</Form>
|
||||
</Form> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id && id > 1 &&
|
||||
id && id > 1 ?
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDelayProfilePress}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
@@ -190,7 +200,6 @@ EditDelayProfileModalContent.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(delayProfileShape).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onProtocolChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -16,13 +16,6 @@ const newDelayProfile = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
{ key: 'preferUsenet', value: 'Prefer Usenet' },
|
||||
{ key: 'preferTorrent', value: 'Prefer Torrent' },
|
||||
{ key: 'onlyUsenet', value: 'Only Usenet' },
|
||||
{ key: 'onlyTorrent', value: 'Only Torrent' }
|
||||
];
|
||||
|
||||
function createDelayProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
@@ -78,7 +71,6 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
protocol,
|
||||
protocolOptions,
|
||||
...delayProfile
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||
|
||||
class Quality extends Component {
|
||||
|
||||
@@ -16,7 +21,8 @@ class Quality extends Component {
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false
|
||||
hasPendingChanges: false,
|
||||
isConfirmQualityDefinitionResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +37,14 @@ class Quality extends Component {
|
||||
this.setState(payload);
|
||||
}
|
||||
|
||||
onResetQualityDefinitionsPress = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
||||
}
|
||||
|
||||
onCloseResetQualityDefinitionsModal = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
@@ -43,6 +57,7 @@ class Quality extends Component {
|
||||
render() {
|
||||
const {
|
||||
isSaving,
|
||||
isResettingQualityDefinitions,
|
||||
hasPendingChanges
|
||||
} = this.state;
|
||||
|
||||
@@ -51,6 +66,18 @@ class Quality extends Component {
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Reset Definitions"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isResettingQualityDefinitions}
|
||||
onPress={this.onResetQualityDefinitionsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
@@ -60,9 +87,18 @@ class Quality extends Component {
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
<ResetQualityDefinitionsModal
|
||||
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
||||
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Quality.propTypes = {
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Quality;
|
||||
|
||||
38
frontend/src/Settings/Quality/QualityConnector.js
Normal file
38
frontend/src/Settings/Quality/QualityConnector.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Quality from './Quality';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
|
||||
(isResettingQualityDefinitions) => {
|
||||
return {
|
||||
isResettingQualityDefinitions
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class QualityConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Quality
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityConnector.propTypes = {
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(QualityConnector);
|
||||
@@ -0,0 +1,33 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ResetQualityDefinitionsModalContentConnector from './ResetQualityDefinitionsModalContentConnector';
|
||||
|
||||
function ResetQualityDefinitionsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ResetQualityDefinitionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ResetQualityDefinitionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ResetQualityDefinitionsModal;
|
||||
@@ -0,0 +1,3 @@
|
||||
.messageContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './ResetQualityDefinitionsModalContent.css';
|
||||
|
||||
class ResetQualityDefinitionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
resetDefinitionTitles: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onResetDefinitionTitlesChange = ({ value }) => {
|
||||
this.setState({ resetDefinitionTitles: value });
|
||||
}
|
||||
|
||||
onResetQualityDefinitionsConfirmed = () => {
|
||||
const resetDefinitionTitles = this.state.resetDefinitionTitles;
|
||||
|
||||
this.setState({ resetDefinitionTitles: false });
|
||||
this.props.onResetQualityDefinitions(resetDefinitionTitles);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose,
|
||||
isResettingQualityDefinitions
|
||||
} = this.props;
|
||||
|
||||
const resetDefinitionTitles = this.state.resetDefinitionTitles;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Reset Quality Definitions
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.messageContainer}>
|
||||
Are you sure you want to reset quality definitions?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Reset Titles</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="resetDefinitionTitles"
|
||||
value={resetDefinitionTitles}
|
||||
helpText="Reset definition titles as well as values"
|
||||
onChange={this.onResetDefinitionTitlesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onResetQualityDefinitionsConfirmed}
|
||||
isDisabled={isResettingQualityDefinitions}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ResetQualityDefinitionsModalContent.propTypes = {
|
||||
onResetQualityDefinitions: PropTypes.func.isRequired,
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ResetQualityDefinitionsModalContent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
|
||||
(isResettingQualityDefinitions) => {
|
||||
return {
|
||||
isResettingQualityDefinitions
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class ResetQualityDefinitionsModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onResetQualityDefinitions = (resetTitles) => {
|
||||
this.props.executeCommand({ name: commandNames.RESET_QUALITY_DEFINITIONS, resetTitles });
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ResetQualityDefinitionsModalContent
|
||||
{...this.props}
|
||||
onResetQualityDefinitions={this.onResetQualityDefinitions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ResetQualityDefinitionsModalContentConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ResetQualityDefinitionsModalContentConnector);
|
||||
@@ -74,6 +74,11 @@ export const defaultState = {
|
||||
label: 'Video Codec',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'videoDynamicRangeType',
|
||||
label: 'Video Dynamic Range',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'audioLanguages',
|
||||
label: 'Audio Languages',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -178,7 +178,8 @@ export const defaultState = {
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'seeders',
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://wiki.servarr.com/Sonarr_FAQ#Help_I_have_forgotten_my_password"
|
||||
href="https://wiki.servarr.com/sonarr/faq#help-i-have-locked-myself-out"
|
||||
class="forgot-password"
|
||||
>Forgot your password?</a
|
||||
>
|
||||
|
||||
@@ -3,11 +3,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
using Ical.Net.General;
|
||||
using Ical.Net.Interfaces.Serialization;
|
||||
using Ical.Net.Serialization;
|
||||
using Ical.Net.Serialization.iCalendar.Factory;
|
||||
using NzbDrone.Core.Tv;
|
||||
using Nancy.Responses;
|
||||
using NzbDrone.Core.Tags;
|
||||
@@ -116,7 +114,7 @@ namespace NzbDrone.Api.Calendar
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrence = calendar.Create<Event>();
|
||||
var occurrence = calendar.Create<CalendarEvent>();
|
||||
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
|
||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||
occurrence.Description = episode.Overview;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.6.6" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||
<PackageReference Include="Sentry" Version="1.2.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
@@ -30,4 +31,4 @@
|
||||
<LastGenOutput>ExceptionMessages.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -20,38 +20,50 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
private RemoteEpisode parseResultMulti;
|
||||
private RemoteEpisode parseResultSingle;
|
||||
private Series series;
|
||||
private List<Episode> episodes;
|
||||
private QualityDefinition qualityType;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
series = Builder<Series>.CreateNew()
|
||||
.Build();
|
||||
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(2).Build().ToList())
|
||||
.Build();
|
||||
|
||||
episodes = Builder<Episode>.CreateListOfSize(10)
|
||||
.All()
|
||||
.With(s => s.SeasonNumber = 1)
|
||||
.BuildList();
|
||||
|
||||
parseResultMultiSet = new RemoteEpisode
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||
Episodes = new List<Episode> { new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), new Episode() }
|
||||
};
|
||||
Episodes = Builder<Episode>.CreateListOfSize(6).All().With(s => s.SeasonNumber = 1).BuildList()
|
||||
};
|
||||
|
||||
parseResultMulti = new RemoteEpisode
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||
Episodes = new List<Episode> { new Episode(), new Episode() }
|
||||
};
|
||||
Episodes = Builder<Episode>.CreateListOfSize(2).All().With(s => s.SeasonNumber = 1).BuildList()
|
||||
};
|
||||
|
||||
parseResultSingle = new RemoteEpisode
|
||||
{
|
||||
Series = series,
|
||||
Release = new ReleaseInfo(),
|
||||
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||
Episodes = new List<Episode> { new Episode() { Id = 2 } }
|
||||
Episodes = new List<Episode> {
|
||||
Builder<Episode>.CreateNew()
|
||||
.With(s => s.SeasonNumber = 1)
|
||||
.With(s => s.EpisodeNumber = 1)
|
||||
.Build()
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(It.IsAny<Quality>()))
|
||||
@@ -67,18 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
||||
Mocker.GetMock<IEpisodeService>().Setup(
|
||||
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Returns(new List<Episode>() {
|
||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
|
||||
new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 }, new Episode() });
|
||||
}
|
||||
|
||||
private void GivenLastEpisode()
|
||||
{
|
||||
Mocker.GetMock<IEpisodeService>().Setup(
|
||||
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Returns(new List<Episode>() {
|
||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode(),
|
||||
new Episode(), new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 } });
|
||||
.Returns(episodes);
|
||||
}
|
||||
|
||||
[TestCase(30, 50, false)]
|
||||
@@ -92,6 +93,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
series.Runtime = runtime;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = 5;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||
}
|
||||
@@ -100,13 +102,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestCase(30, 1000, false)]
|
||||
[TestCase(60, 1000, true)]
|
||||
[TestCase(60, 2000, false)]
|
||||
public void single_episode_first_or_last(int runtime, int sizeInMegaBytes, bool expectedResult)
|
||||
public void should_return_expected_result_for_first_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = runtime;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = episodes.First().Id;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||
}
|
||||
|
||||
[TestCase(30, 500, true)]
|
||||
[TestCase(30, 1000, false)]
|
||||
[TestCase(60, 1000, true)]
|
||||
[TestCase(60, 2000, false)]
|
||||
public void should_return_expected_result_for_last_episode_of_season(int runtime, int sizeInMegaBytes, bool expectedResult)
|
||||
{
|
||||
series.Runtime = runtime;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = sizeInMegaBytes.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = episodes.Last().Id;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(expectedResult);
|
||||
}
|
||||
@@ -144,8 +159,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_size_is_zero()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 30;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 0;
|
||||
@@ -158,8 +171,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_unlimited_30_minute()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 30;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 18457280000;
|
||||
@@ -171,8 +182,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_return_true_if_unlimited_60_minute()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 60;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 36857280000;
|
||||
@@ -184,8 +193,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_treat_daily_series_as_single_episode()
|
||||
{
|
||||
GivenLastEpisode();
|
||||
|
||||
series.Runtime = 60;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
|
||||
@@ -216,5 +223,94 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_series_runtime_is_zero_and_single_episode_is_not_from_first_season()
|
||||
{
|
||||
series.Runtime = 0;
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Episodes.First().Id = 5;
|
||||
parseResultSingle.Release.Size = 200.Megabytes();
|
||||
parseResultSingle.Episodes.First().SeasonNumber = 2;
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_series_runtime_is_zero_and_single_episode_aired_more_than_24_hours_after_first_aired_episode()
|
||||
{
|
||||
series.Runtime = 0;
|
||||
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 200.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = 5;
|
||||
parseResultSingle.Episodes.First().SeasonNumber = 1;
|
||||
parseResultSingle.Episodes.First().EpisodeNumber = 2;
|
||||
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_series_runtime_is_zero_and_single_episode_aired_less_than_24_hours_after_first_aired_episode()
|
||||
{
|
||||
series.Runtime = 0;
|
||||
|
||||
parseResultSingle.Series = series;
|
||||
parseResultSingle.Release.Size = 200.Megabytes();
|
||||
parseResultSingle.Episodes.First().Id = 5;
|
||||
parseResultSingle.Episodes.First().SeasonNumber = 1;
|
||||
parseResultSingle.Episodes.First().EpisodeNumber = 2;
|
||||
parseResultSingle.Episodes.First().AirDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultSingle, null).Accepted.Should().Be(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_is_not_from_first_season()
|
||||
{
|
||||
series.Runtime = 0;
|
||||
parseResultMulti.Series = series;
|
||||
parseResultMulti.Release.Size = 200.Megabytes();
|
||||
parseResultMulti.Episodes.ForEach(e => e.SeasonNumber = 2);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_series_runtime_is_zero_and_multi_episode_aired_more_than_24_hours_after_first_aired_episode()
|
||||
{
|
||||
var airDateUtc = episodes.First().AirDateUtc.Value.AddDays(7);
|
||||
|
||||
series.Runtime = 0;
|
||||
|
||||
parseResultMulti.Series = series;
|
||||
parseResultMulti.Release.Size = 200.Megabytes();
|
||||
parseResultMulti.Episodes.ForEach(e =>
|
||||
{
|
||||
e.SeasonNumber = 1;
|
||||
e.AirDateUtc = airDateUtc;
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_series_runtime_is_zero_and_multi_episode_aired_less_than_24_hours_after_first_aired_episode()
|
||||
{
|
||||
var airDateUtc = episodes.First().AirDateUtc.Value.AddHours(1);
|
||||
|
||||
series.Runtime = 0;
|
||||
|
||||
parseResultMulti.Series = series;
|
||||
parseResultMulti.Release.Size = 200.Megabytes();
|
||||
parseResultMulti.Episodes.ForEach(e =>
|
||||
{
|
||||
e.SeasonNumber = 1;
|
||||
e.AirDateUtc = airDateUtc;
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(parseResultMulti, null).Accepted.Should().Be(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,5 +518,46 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.GetEpisodesBySceneSeason(It.IsAny<int>(), It.IsAny<int>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_season_zero_when_looking_up_is_partial_special_episode_found_by_title()
|
||||
{
|
||||
_series.UseSceneNumbering = false;
|
||||
_parsedEpisodeInfo.SeasonNumber = 1;
|
||||
_parsedEpisodeInfo.EpisodeNumbers = new int[] { 0 };
|
||||
_parsedEpisodeInfo.ReleaseTitle = "Series.Title.S01E00.My.Special.Episode.1080p.AMZN.WEB-DL.DDP5.1.H264-TEPES";
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||
.Returns(
|
||||
Builder<Episode>.CreateNew()
|
||||
.With(e => e.SeasonNumber = 0)
|
||||
.With(e => e.EpisodeNumber = 1)
|
||||
.Build()
|
||||
);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(_series.TvdbId, 0, 1), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_original_parse_result_when_special_episode_lookup_by_title_fails()
|
||||
{
|
||||
_series.UseSceneNumbering = false;
|
||||
_parsedEpisodeInfo.SeasonNumber = 1;
|
||||
_parsedEpisodeInfo.EpisodeNumbers = new int[] { 0 };
|
||||
_parsedEpisodeInfo.ReleaseTitle = "Series.Title.S01E00.My.Special.Episode.1080p.AMZN.WEB-DL.DDP5.1.H264-TEPES";
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Setup(s => s.FindEpisodeByTitle(_series.TvdbId, 0, _parsedEpisodeInfo.ReleaseTitle))
|
||||
.Returns((Episode)null);
|
||||
|
||||
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>()
|
||||
.Verify(v => v.FindEpisode(_series.TvdbId, _parsedEpisodeInfo.SeasonNumber, _parsedEpisodeInfo.EpisodeNumbers.First()), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][1080p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
|
||||
[TestCase("[LostYears] Series Title - 01-17 (WEB 1080p x264 10-bit AAC) [Dual-Audio]", false)]
|
||||
[TestCase("Series.and.Titles.S01.1080p.NF.WEB.DD2.0.x264-SNEAkY", false)]
|
||||
[TestCase("Series.Title.S02E02.This.Year.Will.Be.Different.1080p.WEB.H 265", false)]
|
||||
public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||
{
|
||||
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[星空字幕组] 剃须。然后捡到女高中生。 / Anime Series Title [05][1080p][简日内嵌]", "Anime Series Title", "星空字幕组", 5)]
|
||||
[TestCase("【DHR动研字幕组】[多田君不恋爱_Anime Series Title][13完][繁体][720P][MP4]", "Anime Series Title", "DHR动研字幕组", 13)]
|
||||
[TestCase("【动漫国字幕组】★01月新番[Anime Series Title~!][01][1080P][简体][MP4]", "Anime Series Title~!", "动漫国字幕组", 1)]
|
||||
[TestCase("[风车字幕组][名侦探柯南][857][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||
[TestCase("[风车字幕组][名侦探柯南][857集][米花町反复变化之谜(前篇)][简体][MP4][1080P]", "名侦探柯南", "风车字幕组", 857)]
|
||||
public void should_parse_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
||||
{
|
||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
||||
@@ -44,6 +46,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[Lilith-Raws] 艾梅洛閣下 II 世事件簿 -魔眼蒐集列車 Grace note- / Anime-Series Title - 04 [BiliBili][WEB-DL][1080p][AVC AAC][CHT][MKV]", "Anime-Series Title", "Lilith-Raws", 4)]
|
||||
[TestCase("[NC-Raws] 影宅 / Anime-Series Title - 07 [B-Global][WEB-DL][1080p][AVC AAC][CHS_CHT_ENG_TH_SRT][MKV]", "Anime-Series Title", "NC-Raws", 7)]
|
||||
[TestCase("[NC-Raws] ANIME-SERIES TITLE-影宅- / Anime-Series Title - 07 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]", "Anime-Series Title", "NC-Raws", 7)]
|
||||
[TestCase("[OPFans楓雪動漫][ANIME SERIES 海賊王][第1008話][典藏版][1080P][MKV][簡繁]", "ANIME SERIES", "OPFans", 1008)]
|
||||
[TestCase("[Skymoon-Raws][Anime Series 海賊王][1008][ViuTV][WEB-RIP][CHT][SRTx2][1080p][MKV]", "Anime Series", "Skymoon-Raws", 1008)]
|
||||
public void should_parse_unbracketed_chinese_anime_releases(string postTitle, string title, string subgroup, int absoluteEpisodeNumber)
|
||||
{
|
||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
||||
|
||||
@@ -39,9 +39,13 @@ namespace NzbDrone.Core.Configuration
|
||||
string SslCertHash { get; }
|
||||
string UrlBase { get; }
|
||||
string UiFolder { get; }
|
||||
string InstanceName { get; }
|
||||
bool UpdateAutomatically { get; }
|
||||
UpdateMechanism UpdateMechanism { get; }
|
||||
string UpdateScriptPath { get; }
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -202,6 +206,19 @@ namespace NzbDrone.Core.Configuration
|
||||
// public string UiFolder => GetValue("UiFolder", "UI", false);GetValue("UiFolder", "UI", false);
|
||||
public string UiFolder => "UI";
|
||||
|
||||
public string InstanceName
|
||||
{
|
||||
get
|
||||
{
|
||||
var instanceName = GetValue("InstanceName", BuildInfo.AppName);
|
||||
|
||||
if (instanceName.StartsWith(BuildInfo.AppName) || instanceName.EndsWith(BuildInfo.AppName) )
|
||||
{
|
||||
return instanceName;
|
||||
}
|
||||
return BuildInfo.AppName;
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
|
||||
|
||||
@@ -209,7 +226,13 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
|
||||
|
||||
public int GetValueInt(string key, int defaultValue)
|
||||
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
|
||||
|
||||
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
|
||||
|
||||
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
|
||||
|
||||
public int GetValueInt(string key, int defaultValue, bool persist = true)
|
||||
{
|
||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
removeFailedDownloads = false;
|
||||
}
|
||||
|
||||
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN (\"RTorrent\", \"Flood\") THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
|
||||
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN ('RTorrent', 'Flood') THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
|
||||
{
|
||||
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
|
||||
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(169)]
|
||||
public class add_malayalam_and_ukrainian_languages : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(ConvertProfile);
|
||||
}
|
||||
|
||||
private void ConvertProfile(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updater = new LanguageProfileUpdater169(conn, tran);
|
||||
|
||||
updater.AppendMissing();
|
||||
|
||||
updater.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public class LanguageProfile169 : ModelBase
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<LanguageProfileItem169> Languages { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public Language Cutoff { get; set; }
|
||||
}
|
||||
|
||||
public class LanguageProfileItem169
|
||||
{
|
||||
public int Language { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class LanguageProfileUpdater169
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
private readonly IDbTransaction _transaction;
|
||||
|
||||
private List<LanguageProfile169> _profiles;
|
||||
private HashSet<LanguageProfile169> _changedProfiles = new HashSet<LanguageProfile169>();
|
||||
|
||||
public LanguageProfileUpdater169(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
_connection = conn;
|
||||
_transaction = tran;
|
||||
|
||||
_profiles = GetProfiles();
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
foreach (var profile in _changedProfiles)
|
||||
{
|
||||
using (var updateProfileCmd = _connection.CreateCommand())
|
||||
{
|
||||
updateProfileCmd.Transaction = _transaction;
|
||||
updateProfileCmd.CommandText = "UPDATE LanguageProfiles SET Languages = ? WHERE Id = ?";
|
||||
updateProfileCmd.AddParameter(profile.Languages.ToJson());
|
||||
updateProfileCmd.AddParameter(profile.Id);
|
||||
|
||||
updateProfileCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
_changedProfiles.Clear();
|
||||
}
|
||||
|
||||
public void AppendMissing()
|
||||
{
|
||||
foreach (var profile in _profiles)
|
||||
{
|
||||
var hash = new HashSet<int>(profile.Languages.Select(v => v.Language));
|
||||
|
||||
var missing = Language.All.Where(l => !hash.Contains(l.Id))
|
||||
.OrderByDescending(l => l.Name)
|
||||
.ToList();
|
||||
|
||||
if (missing.Any())
|
||||
{
|
||||
profile.Languages.InsertRange(0, missing.Select(l => new LanguageProfileItem169 { Language = l.Id, Allowed = false }));
|
||||
|
||||
_changedProfiles.Add(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<LanguageProfile169> GetProfiles()
|
||||
{
|
||||
var profiles = new List<LanguageProfile169>();
|
||||
|
||||
using (var getProfilesCmd = _connection.CreateCommand())
|
||||
{
|
||||
getProfilesCmd.Transaction = _transaction;
|
||||
getProfilesCmd.CommandText = @"SELECT Id, Name, Languages, UpgradeAllowed, Cutoff FROM LanguageProfiles";
|
||||
|
||||
using (var profileReader = getProfilesCmd.ExecuteReader())
|
||||
{
|
||||
while (profileReader.Read())
|
||||
{
|
||||
profiles.Add(new LanguageProfile169
|
||||
{
|
||||
Id = profileReader.GetInt32(0),
|
||||
Name = profileReader.GetString(1),
|
||||
Languages = Json.Deserialize<List<LanguageProfileItem169>>(profileReader.GetString(2)),
|
||||
UpgradeAllowed = profileReader.GetBoolean(3),
|
||||
Cutoff = Language.FindById(profileReader.GetInt32(4))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
public override bool SupportsTransactions => true;
|
||||
|
||||
public override bool 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)|
|
||||
|
||||
@@ -42,5 +42,10 @@ namespace NzbDrone.Core.Parser
|
||||
input = _regex.Replace(input, _replacementFormat);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _regex.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,5 @@ namespace NzbDrone.Core.Qualities
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}',");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user