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

Compare commits

...

28 Commits

Author SHA1 Message Date
Weblate
dac69445e4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-04-05 23:13:02 -07:00
Qstick
aca10f6f4f Fixed: Skip move when source and destination are the same
ignore-downstream

Co-Authored-By: Colin Hebert <makkhdyn@gmail.com>
(cherry picked from commit 7a5ae56a96700f401726ac80b3031a25207d8f75)
2024-04-05 23:11:37 -07:00
Mark McDowall
74cdf01e49 New: Set 'Release Type' during Manual Import
Closes #6681
2024-04-06 02:11:17 -04:00
Mark McDowall
a169ebff2a Fixed: Sending ntfy.sh notifications with unicode characters
Closes #6679
2024-04-06 02:11:03 -04:00
fireph
7fc3bebc91 New: Footnote to indicate some renaming tokens support truncation 2024-04-06 02:10:42 -04:00
Till Krüss
e672996dbb Improve text for file deleted through UI/API 2024-04-06 02:09:55 -04:00
Stevie Robinson
238ba85f0a New: Informational text on Custom Formats modal 2024-04-06 02:08:57 -04:00
Cuki
1562d3bae3 Fixed: Use widely supported display mode for PWA 2024-04-06 02:08:08 -04:00
Jendrik Weise
7776ec9955 Reimport files imported prematurely during script import 2024-04-05 23:07:38 -07:00
Jendrik Weise
af5a681ab7 Fix ignoring title based on pre-rename episodefile 2024-04-05 23:07:38 -07:00
Jendrik Weise
0a7f3a12c2 Do not remove all extras when script importing 2024-04-05 23:07:38 -07:00
Jendrik Weise
2ef46e5b90 Fix incorrect subtitle copy regex 2024-04-05 23:07:38 -07:00
Mark McDowall
6003ca1696 Fixed: Deleted episodes not being unmonitored when series folder has been deleted
Closes #6678
2024-04-05 23:07:07 -07:00
Mark McDowall
0937ee6fef Fixed: Path parsing incorrectly treating series title as episode number 2024-04-05 23:06:56 -07:00
Mark McDowall
60ee7cc716 Fixed: Cleanse BHD RSS key in log files
Closes #6666
2024-04-06 02:06:35 -04:00
Mark McDowall
4e83820511 Bump version to 4.0.3 2024-03-31 21:52:19 -07:00
Sonarr
5a66b949cf Automated API Docs update
ignore-downstream
2024-03-31 21:43:33 -07:00
Weblate
f010f56290 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: 王锋 <17611382361@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-03-31 21:43:09 -07:00
Louis R
060b789bc6 Fixed: Exceptions when checking for routable IPv4 addresses 2024-03-28 01:31:28 -04:00
Bogdan
7353fe479d New: Allow HEAD requests to ping endpoint
Closes #6656
2024-03-28 01:30:45 -04:00
Alex Cortelyou
1ec1ce58e9 New: Add additional fields to Webhook Manual Interaction Required events 2024-03-28 01:30:21 -04:00
Stevie Robinson
35d0e6a6f8 Fixed: Handling torrents with relative path in rTorrent 2024-03-28 01:29:15 -04:00
Carlos Gustavo Sarmiento
588372fd95 Fixed: qBittorrent not correctly handling retention during testing 2024-03-28 01:28:41 -04:00
Bogdan
13c925b341 New: Advanced settings toggle in import list, notification and download client modals 2024-03-27 22:27:51 -07:00
iceypotato
1335efd487 New: My Anime List import list
Closes #5148
2024-03-27 22:27:34 -07:00
Mark McDowall
d338425951 Fixed: Use custom formats from import during rename 2024-03-27 22:27:25 -07:00
Mark McDowall
fc6494c569 Fixed: Task with removed series causing error 2024-03-27 22:27:14 -07:00
Weblate
c403b2cdd5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Stanislav <prekop3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-03-27 22:27:07 -07:00
68 changed files with 1072 additions and 166 deletions

View File

@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.2
VERSION: 4.0.3
jobs:
backend:

View File

@@ -15,5 +15,5 @@
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "minimal-ui"
"display": "standalone"
}

View File

@@ -0,0 +1,17 @@
import ReleaseType from 'InteractiveImport/ReleaseType';
import translate from 'Utilities/String/translate';
export default function getReleaseTypeName(
releaseType?: ReleaseType
): string | null {
switch (releaseType) {
case 'singleEpisode':
return translate('SingleEpisode');
case 'multiEpisode':
return translate('MultiEpisode');
case 'seasonPack':
return translate('SeasonPack');
default:
return translate('Unknown');
}
}

View File

@@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import ReleaseType from 'InteractiveImport/ReleaseType';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
@@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase {
quality: QualityModel;
customFormats: CustomFormat[];
indexerFlags: number;
releaseType: ReleaseType;
mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean;
}

View File

@@ -36,6 +36,7 @@ import InteractiveImport, {
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
@@ -73,7 +74,8 @@ type SelectType =
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
| 'indexerFlags'
| 'releaseType';
type FilterExistingFiles = 'all' | 'new';
@@ -128,6 +130,12 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'releaseType',
label: () => translate('ReleaseType'),
isSortable: true,
isVisible: true,
},
{
name: 'customFormats',
label: React.createElement(Icon, {
@@ -369,6 +377,10 @@ function InteractiveImportModalContent(
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
{
key: 'releaseType',
value: translate('SelectReleaseType'),
},
];
if (allowSeriesChange) {
@@ -511,6 +523,7 @@ function InteractiveImportModalContent(
languages,
indexerFlags,
episodeFileId,
releaseType,
} = item;
if (!series) {
@@ -560,6 +573,7 @@ function InteractiveImportModalContent(
quality,
languages,
indexerFlags,
releaseType,
});
return;
@@ -575,6 +589,7 @@ function InteractiveImportModalContent(
quality,
languages,
indexerFlags,
releaseType,
downloadId,
episodeFileId,
});
@@ -787,6 +802,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const onReleaseTypeSelect = useCallback(
(releaseType: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const orderedSelectedIds = items.reduce((acc: number[], file) => {
if (selectedIds.includes(file.id)) {
acc.push(file.id);
@@ -1000,6 +1031,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType="unknown"
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}

View File

@@ -12,6 +12,7 @@ import Episode from 'Episode/Episode';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import getReleaseTypeName from 'Episode/getReleaseTypeName';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
@@ -20,6 +21,8 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
@@ -44,7 +47,8 @@ type SelectType =
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
| 'indexerFlags'
| 'releaseType';
type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean;
@@ -61,6 +65,7 @@ interface InteractiveImportRowProps {
quality?: QualityModel;
languages?: Language[];
size: number;
releaseType: ReleaseType;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
@@ -86,6 +91,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
languages,
releaseGroup,
size,
releaseType,
customFormats,
customFormatScore,
indexerFlags,
@@ -315,6 +321,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectReleaseTypePress = useCallback(() => {
setSelectModalOpen('releaseType');
}, [setSelectModalOpen]);
const onReleaseTypeSelect = useCallback(
(releaseType: ReleaseType) => {
dispatch(
updateInteractiveImportItem({
id,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
@@ -461,6 +488,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
<TableRowCell>{formatBytes(size)}</TableRowCell>
<TableRowCellButton
title={translate('ClickToChangeReleaseType')}
onPress={onSelectReleaseTypePress}
>
{getReleaseTypeName(releaseType)}
</TableRowCellButton>
<TableRowCell>
{customFormats?.length ? (
<Popover
@@ -572,6 +606,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onModalClose={onSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType={releaseType ?? 'unknown'}
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}

View File

@@ -15,6 +15,7 @@ export interface InteractiveImportCommandOptions {
quality: QualityModel;
languages: Language[];
indexerFlags: number;
releaseType: ReleaseType;
downloadId?: string;
episodeFileId?: number;
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModalContent from './SelectReleaseTypeModalContent';
interface SelectQualityModalProps {
isOpen: boolean;
releaseType: ReleaseType;
modalTitle: string;
onReleaseTypeSelect(releaseType: ReleaseType): void;
onModalClose(): void;
}
function SelectReleaseTypeModal(props: SelectQualityModalProps) {
const { isOpen, releaseType, modalTitle, onReleaseTypeSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectReleaseTypeModalContent
releaseType={releaseType}
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectReleaseTypeModal;

View File

@@ -0,0 +1,99 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import ReleaseType from 'InteractiveImport/ReleaseType';
import translate from 'Utilities/String/translate';
const options = [
{
key: 'unknown',
get value() {
return translate('Unknown');
},
},
{
key: 'singleEpisode',
get value() {
return translate('SingleEpisode');
},
},
{
key: 'multiEpisode',
get value() {
return translate('MultiEpisode');
},
},
{
key: 'seasonPack',
get value() {
return translate('SeasonPack');
},
},
];
interface SelectReleaseTypeModalContentProps {
releaseType: ReleaseType;
modalTitle: string;
onReleaseTypeSelect(releaseType: ReleaseType): void;
onModalClose(): void;
}
function SelectReleaseTypeModalContent(
props: SelectReleaseTypeModalContentProps
) {
const { modalTitle, onReleaseTypeSelect, onModalClose } = props;
const [releaseType, setReleaseType] = useState(props.releaseType);
const handleReleaseTypeChange = useCallback(
({ value }: { value: string }) => {
setReleaseType(value as ReleaseType);
},
[setReleaseType]
);
const handleReleaseTypeSelect = useCallback(() => {
onReleaseTypeSelect(releaseType);
}, [releaseType, onReleaseTypeSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectReleaseType')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseType')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="releaseType"
value={releaseType}
values={options}
onChange={handleReleaseTypeChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={handleReleaseTypeSelect}>
{translate('SelectReleaseType')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectReleaseTypeModalContent;

View File

@@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
</Form>
<FieldSet legend={translate('Conditions')}>
<Alert kind={kinds.INFO}>
<div>
{translate('CustomFormatsSettingsTriggerInfo')}
</div>
</Alert>
<div className={styles.customFormats}>
{
specifications.map((tag) => {

View File

@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
@@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient
testDownloadClient,
toggleAdvancedSettings
};
class EditDownloadClientModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -19,6 +19,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
@@ -38,6 +39,7 @@ function EditImportListModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress,
...otherProps
} = props;
@@ -288,6 +290,12 @@ function EditImportListModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -327,6 +335,7 @@ EditImportListModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setImportListValue,
setImportListFieldValue,
saveImportList,
testImportList
testImportList,
toggleAdvancedSettings
};
class EditImportListModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditImportListModalContentConnector extends Component {
this.props.testImportList({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditImportListModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditImportListModalContentConnector.propTypes = {
setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -80,19 +80,19 @@ const fileNameTokens = [
];
const seriesTokens = [
{ token: '{Series Title}', example: 'The Series Title\'s!' },
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' },
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' },
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleFirstCharacter}', example: 'S' },
{ token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 },
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 },
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 },
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 },
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 },
{ token: '{Series Year}', example: '2010' }
];
@@ -124,8 +124,8 @@ const absoluteTokens = [
];
const episodeTitleTokens = [
{ token: '{Episode Title}', example: 'Episode\'s Title' },
{ token: '{Episode CleanTitle}', example: 'Episodes Title' }
{ token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 },
{ token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 }
];
const qualityTokens = [
@@ -149,7 +149,7 @@ const mediaInfoTokens = [
];
const otherTokens = [
{ token: '{Release Group}', example: 'Rls Grp' },
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 },
{ token: '{Custom Formats}', example: 'iNTERNAL' },
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
];
@@ -305,7 +305,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('Series')}>
<div className={styles.groups}>
{
seriesTokens.map(({ token, example }) => {
seriesTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -313,6 +313,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -322,6 +323,11 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('SeriesFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('SeriesID')}>
@@ -451,7 +457,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('EpisodeTitle')}>
<div className={styles.groups}>
{
episodeTitleTokens.map(({ token, example }) => {
episodeTitleTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -459,6 +465,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -468,6 +475,10 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('EpisodeTitleFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Quality')}>
@@ -523,7 +534,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('Other')}>
<div className={styles.groups}>
{
otherTokens.map(({ token, example }) => {
otherTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -531,6 +542,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -558,6 +570,11 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Original')}>

View File

@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css';
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteNotificationPress,
...otherProps
} = props;
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
import {
saveNotification,
setNotificationFieldValue,
setNotificationValue,
testNotification,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
saveNotification,
testNotification
testNotification,
toggleAdvancedSettings
};
class EditNotificationModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
this.props.testNotification({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
setNotificationFieldValue: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -1,12 +1,21 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Series from 'Series/Series';
function createMultiSeriesSelector(seriesIds: number[]) {
return createSelector(
(state: AppState) => state.series.itemMap,
(state: AppState) => state.series.items,
(itemMap, allSeries) => {
return seriesIds.map((seriesId) => allSeries[itemMap[seriesId]]);
return seriesIds.reduce((acc: Series[], seriesId) => {
const series = allSeries[itemMap[seriesId]];
if (series) {
acc.push(series);
}
return acc;
}, []);
}
);
}

View File

@@ -18,6 +18,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]

View File

@@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -30,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly ICached<CredentialCache> _credentialCache;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
ICacheManager cacheManager,
Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
@@ -43,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
_logger = logger;
}
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
@@ -249,19 +255,27 @@ namespace NzbDrone.Common.Http.Dispatchers
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
private static bool HasRoutableIPv4Address()
private bool HasRoutableIPv4Address()
{
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
try
{
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
return networkInterfaces.Any(ni =>
ni.OperationalStatus == OperationalStatus.Up &&
ni.GetIPProperties().UnicastAddresses.Any(ip =>
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
!IPAddress.IsLoopback(ip.Address)));
return networkInterfaces.Any(ni =>
ni.OperationalStatus == OperationalStatus.Up &&
ni.GetIPProperties().UnicastAddresses.Any(ip =>
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
!IPAddress.IsLoopback(ip.Address)));
}
catch (Exception e)
{
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
return true;
}
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
@@ -285,7 +299,9 @@ namespace NzbDrone.Common.Http.Dispatchers
catch
{
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
useIPv6 = !HasRoutableIPv4Address();
var routableIPv4 = HasRoutableIPv4Address();
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
useIPv6 = !routableIPv4;
}
finally
{

View File

@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
}

View File

@@ -9,13 +9,14 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
[TestFixture]
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
{
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 5.1].mkv", "", "Name (2020) - S01E20 - [FLAC 2.0].fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [FLAC 2.0].mkv")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
public void should_do_basic_parse(string relativePath, string originalFilePath, string path, string fileNameBeforeRename)
{
var episodeFile = new EpisodeFile
{
@@ -23,7 +24,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
OriginalFilePath = originalFilePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, fileNameBeforeRename);
subtitleTitleInfo.Title.Should().BeNull();
subtitleTitleInfo.Copy.Should().Be(0);
@@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
RelativePath = relativePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, null);
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
}

View File

@@ -444,6 +444,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle 1.en.ass", new string[0], "mytitle 1", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")]
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
{

View File

@@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)]
[TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
[TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
[TestCase(@"C:\Test\Series\Season 1\02.04.24 - S01E01 - The Rabbit Hole", 1, 1)]
[TestCase(@"C:\Test\Series\Season 1\8 Series Rules - S01E01 - Pilot", 1, 1)]
// [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0
public void should_parse_from_path(string path, int season, int episode)

View File

@@ -388,16 +388,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
var minimumRetention = 60 * 24 * 14;
return new DownloadClientInfo
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) },
RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)
RemovesCompletedDownloads = RemovesCompletedDownloads(config)
};
}
private bool RemovesCompletedDownloads(QBittorrentPreferences config)
{
var minimumRetention = 60 * 24 * 14; // 14 days in minutes
return (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles);
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
@@ -448,7 +452,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// Complain if qBittorrent is configured to remove torrents on max ratio
var config = Proxy.GetConfig(Settings);
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles))
if (RemovesCompletedDownloads(config))
{
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimit"))
{

View File

@@ -139,12 +139,14 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
// Ignore torrents with an empty path
if (torrent.Path.IsNullOrWhiteSpace())
{
_logger.Warn("Torrent '{0}' has an empty download path and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
continue;
}
if (torrent.Path.StartsWith("."))
{
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
_logger.Warn("Torrent '{0}' has a download path starting with '.' and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
continue;
}
var item = new DownloadClientItem();

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Extras
{
public interface IExistingExtraFiles
{
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles);
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename);
}
public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent>
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras
_logger = logger;
}
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles)
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing extra files in {0}", series.Path);
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Extras
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
{
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles);
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles, fileNameBeforeRename);
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
}
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Extras
{
var series = message.Series;
var possibleExtraFiles = message.PossibleExtraFiles;
var importedFiles = ImportExtraFiles(series, possibleExtraFiles);
var importedFiles = ImportExtraFiles(series, possibleExtraFiles, null);
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
}

View File

@@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras
public interface IImportExistingExtraFiles
{
int Order { get; }
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
}
}

View File

@@ -19,12 +19,21 @@ namespace NzbDrone.Core.Extras
}
public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles)
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries)
{
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id);
if (keepExistingEntries)
{
var incompleteImports = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance).Select(f => f.Id);
_extraFileService.DeleteMany(incompleteImports);
return Filter(series, filesOnDisk, importedFiles, new List<TExtraFile>());
}
Clean(series, filesOnDisk, importedFiles, seriesFiles);
return Filter(series, filesOnDisk, importedFiles, seriesFiles);

View File

@@ -33,12 +33,12 @@ namespace NzbDrone.Core.Extras.Metadata
public override int Order => 0;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing metadata in {0}", series.Path);
var metadataFiles = new List<MetadataFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
{

View File

@@ -28,12 +28,12 @@ namespace NzbDrone.Core.Extras.Others
public override int Order => 2;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing extra files in {0}", series.Path);
var extraFiles = new List<OtherExtraFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleExtraFile in filterResult.FilesOnDisk)
{

View File

@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles
public override int Order => 1;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing subtitle files in {0}", series.Path);
var subtitleFiles = new List<SubtitleFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
{
@@ -46,7 +46,8 @@ namespace NzbDrone.Core.Extras.Subtitles
{
FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile),
Series = series,
Path = possibleSubtitleFile
Path = possibleSubtitleFile,
FileNameBeforeRename = fileNameBeforeRename
};
try

View File

@@ -190,6 +190,23 @@ namespace NzbDrone.Core.ImportLists
item.Title = mappedSeries.Title;
}
// Map by MyAniList ID if we have it
if (item.TvdbId <= 0 && item.MalId > 0)
{
var mappedSeries = _seriesSearchService.SearchForNewSeriesByMyAnimeListId(item.MalId)
.FirstOrDefault();
if (mappedSeries == null)
{
_logger.Debug("Rejected, unable to find matching TVDB ID for MAL ID: {0} [{1}]", item.MalId, item.Title);
continue;
}
item.TvdbId = mappedSeries.TvdbId;
item.Title = mappedSeries.Title;
}
if (item.TvdbId == 0)
{
_logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title);

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public class MyAnimeListImport : HttpImportListBase<MyAnimeListSettings>
{
public const string OAuthPath = "oauth/myanimelist/authorize";
public const string RedirectUriPath = "oauth/myanimelist/auth";
public const string RenewUriPath = "oauth/myanimelist/renew";
public override string Name => "MyAnimeList";
public override ImportListType ListType => ImportListType.Other;
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6);
private readonly IImportListRepository _importListRepository;
private readonly IHttpRequestBuilderFactory _requestBuilder;
// This constructor the first thing that is called when sonarr creates a button
public MyAnimeListImport(IImportListRepository netImportRepository, IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, localizationService, logger)
{
_importListRepository = netImportRepository;
_requestBuilder = requestBuilder.Services;
}
public override ImportListFetchResult Fetch()
{
if (Settings.Expires < DateTime.UtcNow.AddMinutes(5))
{
RefreshToken();
}
return FetchItems(g => g.GetListItems());
}
// MAL OAuth info: https://myanimelist.net/blog.php?eid=835707
// The whole process is handled through Sonarr's services.
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "startOAuth")
{
var request = _requestBuilder.Create()
.Resource(OAuthPath)
.AddQueryParam("state", query["callbackUrl"])
.AddQueryParam("redirect_uri", _requestBuilder.Create().Resource(RedirectUriPath).Build().Url.ToString())
.Build();
return new
{
OauthUrl = request.Url.ToString()
};
}
else if (action == "getOAuthToken")
{
return new
{
accessToken = query["access_token"],
expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])),
refreshToken = query["refresh_token"]
};
}
return new { };
}
public override IParseImportListResponse GetParser()
{
return new MyAnimeListParser();
}
public override IImportListRequestGenerator GetRequestGenerator()
{
return new MyAnimeListRequestGenerator()
{
Settings = Settings,
};
}
private void RefreshToken()
{
_logger.Trace("Refreshing Token");
Settings.Validate().Filter("RefreshToken").ThrowOnError();
var httpReq = _requestBuilder.Create()
.Resource(RenewUriPath)
.AddQueryParam("refresh_token", Settings.RefreshToken)
.Build();
try
{
var httpResp = _httpClient.Get<MyAnimeListAuthToken>(httpReq);
if (httpResp?.Resource != null)
{
var token = httpResp.Resource;
Settings.AccessToken = token.AccessToken;
Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn);
Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken;
if (Definition.Id > 0)
{
_importListRepository.UpdateSettings((ImportListDefinition)Definition);
}
}
}
catch (HttpRequestException)
{
_logger.Error("Error trying to refresh MAL access token.");
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public class MyAnimeListParser : IParseImportListResponse
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MyAnimeListParser));
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
{
var jsonResponse = Json.Deserialize<MyAnimeListResponse>(importListResponse.Content);
var series = new List<ImportListItemInfo>();
foreach (var show in jsonResponse.Animes)
{
series.AddIfNotNull(new ImportListItemInfo
{
Title = show.AnimeListInfo.Title,
MalId = show.AnimeListInfo.Id
});
}
return series;
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public class MyAnimeListRequestGenerator : IImportListRequestGenerator
{
public MyAnimeListSettings Settings { get; set; }
private static readonly Dictionary<MyAnimeListStatus, string> StatusMapping = new Dictionary<MyAnimeListStatus, string>
{
{ MyAnimeListStatus.Watching, "watching" },
{ MyAnimeListStatus.Completed, "completed" },
{ MyAnimeListStatus.OnHold, "on_hold" },
{ MyAnimeListStatus.Dropped, "dropped" },
{ MyAnimeListStatus.PlanToWatch, "plan_to_watch" },
};
public virtual ImportListPageableRequestChain GetListItems()
{
var pageableReq = new ImportListPageableRequestChain();
pageableReq.Add(GetSeriesRequest());
return pageableReq;
}
private IEnumerable<ImportListRequest> GetSeriesRequest()
{
var status = (MyAnimeListStatus)Settings.ListStatus;
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim());
requestBuilder.Resource("users/@me/animelist");
requestBuilder.AddQueryParam("fields", "list_status");
requestBuilder.AddQueryParam("limit", "1000");
requestBuilder.Accept(HttpAccept.Json);
if (status != MyAnimeListStatus.All && StatusMapping.TryGetValue(status, out var statusName))
{
requestBuilder.AddQueryParam("status", statusName);
}
var httpReq = new ImportListRequest(requestBuilder.Build());
httpReq.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.AccessToken}");
yield return httpReq;
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public class MyAnimeListResponse
{
[JsonProperty("data")]
public List<MyAnimeListItem> Animes { get; set; }
}
public class MyAnimeListItem
{
[JsonProperty("node")]
public MyAnimeListItemInfo AnimeListInfo { get; set; }
[JsonProperty("list_status")]
public MyAnimeListStatusResult ListStatus { get; set; }
}
public class MyAnimeListStatusResult
{
public string Status { get; set; }
}
public class MyAnimeListItemInfo
{
public int Id { get; set; }
public string Title { get; set; }
}
public class MyAnimeListIds
{
[JsonProperty("mal_id")]
public int MalId { get; set; }
[JsonProperty("thetvdb_id")]
public int TvdbId { get; set; }
}
public class MyAnimeListAuthToken
{
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}
}

View File

@@ -0,0 +1,58 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public class MalSettingsValidator : AbstractValidator<MyAnimeListSettings>
{
public MalSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.AccessToken).NotEmpty()
.OverridePropertyName("SignIn")
.WithMessage("Must authenticate with MyAnimeList");
RuleFor(c => c.ListStatus).Custom((status, context) =>
{
if (!Enum.IsDefined(typeof(MyAnimeListStatus), status))
{
context.AddFailure($"Invalid status: {status}");
}
});
}
}
public class MyAnimeListSettings : IImportListSettings
{
public string BaseUrl { get; set; }
protected AbstractValidator<MyAnimeListSettings> Validator => new MalSettingsValidator();
public MyAnimeListSettings()
{
BaseUrl = "https://api.myanimelist.net/v2";
}
[FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")]
public int ListStatus { get; set; }
[FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessToken { get; set; }
[FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string RefreshToken { get; set; }
[FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public DateTime Expires { get; set; }
[FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)]
public string SignIn { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,25 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.MyAnimeList
{
public enum MyAnimeListStatus
{
[FieldOption(label: "All")]
All = 0,
[FieldOption(label: "Watching")]
Watching = 1,
[FieldOption(label: "Completed")]
Completed = 2,
[FieldOption(label: "On Hold")]
OnHold = 3,
[FieldOption(label: "Dropped")]
Dropped = 4,
[FieldOption(label: "Plan to Watch")]
PlanToWatch = 5
}
}

View File

@@ -261,7 +261,7 @@
"AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}",
"AutoRedownloadFailedHelpText": "Cerca i intenta baixar automàticament una versió diferent",
"AutoTaggingNegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
"AutoTaggingRequiredHelpText": "Aquesta condició {implementationName} ha de coincidir perquè s'apliqui la regla d'etiquetatge automàtic. En cas contrari, una única coincidència {implementationName} és suficient.",
"AutoTaggingRequiredHelpText": "La condició {implementationName} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {implementationName}.",
"BlocklistLoadError": "No es pot carregar la llista de bloqueig",
"BlocklistRelease": "Publicació de la llista de bloqueig",
"BranchUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern",
@@ -451,7 +451,7 @@
"DeleteEpisodesFilesHelpText": "Suprimeix els fitxers de l'episodi i la carpeta de la sèrie",
"DeleteRemotePathMapping": "Editeu el mapa de camins remots",
"DefaultNotFoundMessage": "Deu estar perdut, no hi ha res a veure aquí.",
"DelayMinutes": "{delay} minuts",
"DelayMinutes": "{delay} Minuts",
"DelayProfile": "Perfil de retard",
"DeleteImportListExclusionMessageText": "Esteu segur que voleu suprimir aquesta exclusió de la llista d'importació?",
"DeleteReleaseProfile": "Suprimeix el perfil de llançament",
@@ -673,5 +673,43 @@
"AutoTaggingSpecificationSeriesType": "Tipus de Sèries",
"AutoTaggingSpecificationStatus": "Estat",
"BlocklistAndSearch": "Llista de bloqueig i cerca",
"BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat"
"BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
"Directory": "Directori",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}",
"Destination": "Destinació",
"Umask": "UMask",
"ConnectionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}",
"DoNotBlocklist": "No afegiu a la llista de bloqueig",
"DoNotBlocklistHint": "Elimina sense afegir a la llista de bloqueig",
"Donate": "Dona",
"DownloadClientDelugeTorrentStateError": "Deluge està informant d'un error",
"NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
"TorrentDelayTime": "Retard del torrent: {torrentDelay}",
"CustomFormatsSpecificationRegularExpression": "Expressió regular",
"RemoveFromDownloadClient": "Elimina del client de baixada",
"StartupDirectory": "Directori d'inici",
"ClickToChangeIndexerFlags": "Feu clic per canviar els indicadors de l'indexador",
"ImportListsSettingsSummary": "Importa des d'una altra instància {appName} o llistes de Trakt i gestiona les exclusions de llistes",
"DeleteSpecificationHelpText": "Esteu segur que voleu suprimir l'especificació '{name}'?",
"DeleteSpecification": "Esborra especificació",
"UsenetDelayTime": "Retard d'Usenet: {usenetDelay}",
"DownloadClientDelugeSettingsDirectory": "Directori de baixada",
"DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat",
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
"RetryingDownloadOn": "S'està retardant la baixada fins al {date} a les {time}",
"ListWillRefreshEveryInterval": "La llista s'actualitzarà cada {refreshInterval}",
"BlocklistAndSearchMultipleHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
"BlocklistMultipleOnlyHint": "Afegeix a la llista de bloqueig sense cercar substituts",
"BlocklistOnly": "Sols afegir a la llista de bloqueig",
"BlocklistOnlyHint": "Afegir a la llista de bloqueig sense cercar substituts",
"ChangeCategory": "Canvia categoria",
"ChangeCategoryHint": "Canvia la baixada a la \"Categoria post-importació\" des del client de descàrrega",
"ChangeCategoryMultipleHint": "Canvia les baixades a la \"Categoria post-importació\" des del client de descàrrega",
"BlocklistReleaseHelpText": "Impedeix que {appName} baixi aquesta versió mitjançant RSS o cerca automàtica",
"MinutesSixty": "60 minuts: {sixty}",
"CustomFilter": "Filtres personalitzats",
"CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules",
"CustomFormatsSpecificationFlag": "Bandera"
}

View File

@@ -218,6 +218,7 @@
"ClickToChangeLanguage": "Click to change language",
"ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group",
"ClickToChangeReleaseType": "Click to change release type",
"ClickToChangeSeason": "Click to change season",
"ClickToChangeSeries": "Click to change series",
"ClientPriority": "Client Priority",
@@ -278,6 +279,7 @@
"CustomFormats": "Custom Formats",
"CustomFormatsLoadError": "Unable to load Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationLanguage": "Language",
@@ -378,7 +380,7 @@
"DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?",
"Deleted": "Deleted",
"DeletedReasonEpisodeMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the episode in the database",
"DeletedReasonManual": "File was deleted by via UI",
"DeletedReasonManual": "File was deleted using {appName}, either manually or by another tool through the API",
"DeletedReasonUpgrade": "File was deleted to import an upgrade",
"DeletedSeriesDescription": "Series was deleted from TheTVDB",
"Destination": "Destination",
@@ -635,6 +637,7 @@
"EpisodeRequested": "Episode Requested",
"EpisodeSearchResultsLoadError": "Unable to load results for this episode search. Try again later",
"EpisodeTitle": "Episode Title",
"EpisodeTitleFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Episode Title:30}`) or the beginning (e.g. `{Episode Title:-30}`) are both supported. Episode titles will be automatically truncated to file system limitations if necessary.",
"EpisodeTitleRequired": "Episode Title Required",
"EpisodeTitleRequiredHelpText": "Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA",
"Episodes": "Episodes",
@@ -838,6 +841,9 @@
"ImportListsImdbSettingsListId": "List ID",
"ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)",
"ImportListsLoadError": "Unable to load Import Lists",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authenticate with MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "List Status",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Type of list you want to import from, set to 'All' for all lists",
"ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv",
"ImportListsPlexSettingsWatchlistName": "Plex Watchlist",
"ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS",
@@ -1586,6 +1592,7 @@
"RelativePath": "Relative Path",
"Release": "Release",
"ReleaseGroup": "Release Group",
"ReleaseGroupFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Release Group:30}`) or the beginning (e.g. `{Release Group:-30}`) are both supported.`).",
"ReleaseGroups": "Release Groups",
"ReleaseHash": "Release Hash",
"ReleaseProfile": "Release Profile",
@@ -1771,11 +1778,13 @@
"SelectLanguages": "Select Languages",
"SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group",
"SelectReleaseType": "Select Release Type",
"SelectSeason": "Select Season",
"SelectSeasonModalTitle": "{modalTitle} - Select Season",
"SelectSeries": "Select Series",
"SendAnonymousUsageData": "Send Anonymous Usage Data",
"Series": "Series",
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .",
"SeriesCannotBeFound": "Sorry, that series cannot be found.",
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files",

View File

@@ -324,7 +324,7 @@
"ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados",
"ConnectSettings": "Conectar Ajustes",
"CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'",
"XmlRpcPath": "Ruta XML RPC",
"XmlRpcPath": "Ruta RPC de XML",
"AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no se aplicará si esta condición {implementationName} coincide.",
"CloneCustomFormat": "Clonar formato personalizado",
"Close": "Cerrar",
@@ -530,20 +530,20 @@
"DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series",
"DoNotPrefer": "No preferir",
"DoNotUpgradeAutomatically": "No actualizar automáticamente",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, en blanco usa el defecto por el cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"Download": "Descargar",
"Donate": "Donar",
"DownloadClientDelugeValidationLabelPluginFailure": "Falló la configuración de la etiqueta",
"DownloadClientDelugeTorrentStateError": "Deluge está informando de un error",
"DownloadClientDownloadStationValidationFolderMissing": "No existe la carpeta",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en los ajustes de DownloadStation en BT/HTTP/FTP/NZB -> Ubicación.",
"DownloadClientFreeboxSettingsAppIdHelpText": "ID de la aplicación cuando se crea acceso a la API de Freebox (i.e. 'app_id')",
"DownloadClientFreeboxSettingsAppToken": "Token de la aplicación",
"DownloadClientFreeboxSettingsAppIdHelpText": "ID de la app dada cuando se crea acceso a la API de Freebox (esto es 'app_id')",
"DownloadClientFreeboxSettingsAppToken": "Token de la app",
"DownloadClientFreeboxUnableToReachFreebox": "No es posible acceder a la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})",
"DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene varios archivos y no está en una carpeta de trabajo: {outputPath}",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0",
"DownloadClientOptionsLoadError": "No es posible cargar las opciones del cliente de descarga",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta tendrá que ser accesible desde XBMC",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC",
"DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb",
"Docker": "Docker",
"DockerUpdater": "Actualiza el contenedor docker para recibir la actualización",
@@ -570,17 +570,17 @@
"DotNetVersion": ".NET",
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Ningún cliente de descarga disponible",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es posible comunicarse con {downloadClientName}. {errorMessage}",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo a la url json de deluge, ver {url}",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo al url del json de deluge, vea {url}",
"DownloadClientDownloadStationValidationApiVersion": "Versión de la API de la Estación de Descarga no soportada, debería ser al menos {requiredVersion}. Soporte desde {minVersion} hasta {maxVersion}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "No existe la carpeta '{downloadDir}', debe ser creada manualmente dentro de la Carpeta Compartida '{sharedFolder}'.",
"DownloadClientDownloadStationValidationNoDefaultDestination": "Sin destino predeterminado",
"DownloadClientFreeboxNotLoggedIn": "No ha iniciado sesión",
"DownloadClientFreeboxSettingsApiUrl": "URL de la API",
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API de Freebox con la versión de la API, p.ej. '{url}', por defecto es '{defaultApiUrl}'",
"DownloadClientFreeboxSettingsAppId": "ID de la aplicación",
"DownloadClientFreeboxSettingsAppTokenHelpText": "App token recuperado cuando se crea el acceso a la API de Freebox (i.e. 'app_token')",
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, por defecto es '{port}'",
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)",
"DownloadClientFreeboxSettingsApiUrl": "URL de API",
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'",
"DownloadClientFreeboxSettingsAppId": "ID de la app",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')",
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'",
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)",
"DownloadClientFreeboxUnableToReachFreeboxApi": "No es posible acceder a la API de Freebox. Verifica la configuración 'URL de la API' para la URL base y la versión.",
"DownloadClientNzbgetValidationKeepHistoryOverMax": "La opción KeepHistory de NZBGet debería ser menor de 25000",
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.",
@@ -605,7 +605,7 @@
"EnableHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato",
"EnableMediaInfoHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.",
"TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, por defecto es '.magnet'",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, predeterminado a '.magnet'",
"DownloadIgnoredEpisodeTooltip": "Descarga de episodio ignorada",
"EditDelayProfile": "Editar perfil de retraso",
"DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}",
@@ -613,13 +613,13 @@
"EditReleaseProfile": "Editar perfil de lanzamiento",
"DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm",
"DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría",
"DownloadClientRTorrentSettingsUrlPath": "Ruta de la url",
"DownloadClientRTorrentSettingsUrlPath": "Ruta de url",
"DownloadClientSabnzbdValidationDevelopVersion": "Versión de desarrollo de Sabnzbd, asumiendo versión 3.0.0 o superior.",
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} de rastrear nuevas descargas. Sabnzbd también recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que resulta más efectivo.",
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debe deshabilitar la ordenación por fechas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo",
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url de {clientName}, como {url}",
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}",
"DownloadClientStatusAllClientHealthCheckMessage": "Ningún cliente de descarga está disponible debido a fallos",
"DownloadClientValidationGroupMissing": "El grupo no existe",
"DownloadClientValidationSslConnectFailure": "No es posible conectarse a través de SSL",
@@ -641,7 +641,7 @@
"EnableInteractiveSearchHelpText": "Se usará cuando se utilice la búsqueda interactiva",
"DoneEditingGroups": "Terminado de editar grupos",
"DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades multimedia como etiquetas. Sugerencias a modo de ejemplo.",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades de medios como etiquetas. Los consejos son ejemplos.",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Añade etiquetas después de que se importe una descarga.",
"DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basada en el criterio de sembrado actual en Ajustes -> Indexadores",
"DownloadClientFloodSettingsPostImportTags": "Etiquetas tras importación",
@@ -649,7 +649,7 @@
"DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir",
"DownloadClientFreeboxApiError": "La API de Freebox devolvió el error: {errorDescription}",
"DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. El motivo: {errorDescription}",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Se importarán los archivos .strm en esta carpeta por drone",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error",
"DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial",
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado",
@@ -658,26 +658,26 @@
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar descargas completadas sin una categoría.",
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Poner en cola el torrent no está habilitado en los ajustes de su qBittorrent. Habilítelo en qBittorrent o seleccione 'Último' como prioridad.",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} no podrá efectuar el Manejo de Descargas Completadas según lo configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'",
"DownloadClientRTorrentSettingsAddStopped": "Añadir parados",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitarlo añadirá los torrents y magnets a rTorrent en un estado parado. Esto puede romper los archivos magnet.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de rTorrent",
"DownloadClientRTorrentSettingsAddStopped": "Añadir detenido",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El cliente de descarga {downloadClientName} se establece para eliminar las descargas completadas. Esto puede resultar en descargas siendo eliminadas de tu cliente antes de que {appName} pueda importarlas.",
"DownloadClientRootFolderHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en la carpeta raíz {rootFolderPath}. No debería descargar a una carpeta raíz.",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar la ordenación por fechas",
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debe deshabilitar la ordenación de películas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSettingsCategorySubFolderHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.",
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, déjelo en blanco para usar el predeterminado",
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado",
"DownloadClientSettingsInitialState": "Estado inicial",
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a {clientName}",
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}",
"DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión hace más de 14 días",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está descargando metadatos",
"DownloadClientSettingsPostImportCategoryHelpText": "Categoría para {appName} que se establece después de que se haya importado la descarga. {appName} no eliminará los torrents en esa categoría incluso si finalizó la siembra. Déjelo en blanco para mantener la misma categoría.",
"DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión dentro de los últimos 14 días",
"DownloadClientSettingsUseSslHelpText": "Usa conexión segura cuando haya una conexión a {clientName}",
"DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}",
"DownloadClientSortingHealthCheckMessage": "El cliente de descarga {downloadClientName} tiene habilitada la ordenación {sortingMode} para la categoría de {appName}. Debería deshabilitar la ordenación en su cliente de descarga para evitar problemas al importar.",
"DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de descarga no disponibles debido a fallos: {downloadClientNames}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de Transmission",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p.ej. {url}, por defecto es '{defaultUrl}'",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'",
"DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error",
"DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta",
"DownloadClientValidationApiKeyRequired": "Clave API requerida",
@@ -706,11 +706,11 @@
"DownloadClientSabnzbdValidationUnknownVersion": "Versión desconocida: {rawVersion}",
"DownloadClientSettingsAddPaused": "Añadir pausado",
"DownloadClientSeriesTagHelpText": "Solo use este cliente de descarga para series con al menos una etiqueta coincidente. Déjelo en blanco para usarlo con todas las series.",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Consulte Opciones -> Interfaz Web -> 'Usar HTTPS en lugar de HTTP' en qBittorrent.",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} no pudo añadir la etiqueta a qBittorrent.",
"DownloadClientQbittorrentValidationCategoryUnsupported": "La categoría no está soportada",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Poner en cola no está habilitado",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, vea {url}. Esto es usualmente RPC2 o [ruta a rTorrent]{url2} cuando se usa rTorrent.",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.",
"DownloadClientQbittorrentTorrentStatePathError": "No es posible importar. La ruta coincide con el directorio de descarga base del cliente, ¿es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Diseño de contenido de torrent' NO se haya establecido en 'Original' o 'Crear subcarpeta'?",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar la ordenación de películas",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenación de TV",
@@ -733,8 +733,8 @@
"DownloadClientValidationSslConnectFailureDetail": "{appName} no se puede conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor, intente configurar tanto {appName} como {clientName} para no usar SSL.",
"DownloadFailedEpisodeTooltip": "La descarga del episodio falló",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primero las primeras y últimas",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Tenga en cuenta que Torrents forzados no se atiene a las restricciones de sembrado",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una Carpeta Compartida con el nombre '{sharedFolder}', ¿estás seguro que lo has especificado correctamente?",
"EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas",
@@ -1005,8 +1005,8 @@
"Forecast": "Previsión",
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
"IndexerHDBitsSettingsCodecs": "Códecs",
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerPriority": "Prioridad del indexador",
"IconForFinales": "Icono para Finales",
"IgnoreDownload": "Ignorar descarga",
@@ -1068,23 +1068,23 @@
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores",
"IndexerSearchNoInteractiveHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Interactiva activada, {appName} no proporcionará ningún resultado de búsquedas interactivas",
"PasswordConfirmation": "Confirmación de Contraseña",
"IndexerSettingsAdditionalParameters": "Parámetros Adicionales",
"IndexerSettingsAdditionalParameters": "Parámetros adicionales",
"IndexerSettingsAllowZeroSizeHelpText": "Activar esta opción le permitirá utilizar fuentes que no especifiquen el tamaño del lanzamiento, pero tenga cuidado, no se realizarán comprobaciones relacionadas con el tamaño.",
"IndexerSettingsAllowZeroSize": "Permitir Tamaño Cero",
"StopSelecting": "Detener la Selección",
"IndexerSettingsCookie": "Cookie",
"IndexerSettingsCategories": "Categorías",
"IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semillas necesario.",
"IndexerSettingsSeedRatio": "Proporción de Semillado",
"IndexerSettingsSeedRatio": "Ratio de sembrado",
"StartupDirectory": "Directorio de Arranque",
"IndexerSettingsAdditionalParametersNyaa": "Parámetros Adicionales",
"IndexerSettingsPasskey": "Clave de acceso",
"IndexerSettingsSeasonPackSeedTime": "Tiempo de Semillado de los Pack de Temporada",
"IndexerSettingsAnimeStandardFormatSearch": "Formato Estándar de Búsqueda de Anime",
"IndexerSettingsAnimeStandardFormatSearchHelpText": "Buscar también anime utilizando la numeración estándar",
"IndexerSettingsApiPathHelpText": "Ruta a la api, normalmente {url}",
"IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}",
"IndexerSettingsSeasonPackSeedTimeHelpText": "La cantidad de tiempo que un torrent de pack de temporada debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
"IndexerSettingsSeedTime": "Tiempo de Semillado",
"IndexerSettingsSeedTime": "Tiempo de sembrado",
"IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores",
"IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.",
"NotificationsDiscordSettingsAuthor": "Autor",
@@ -1097,12 +1097,12 @@
"IndexerSettingsMinimumSeeders": "Semillas mínimas",
"IndexerSettingsRssUrl": "URL de RSS",
"IndexerSettingsAnimeCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar anime",
"IndexerSettingsApiPath": "Ruta de la API",
"IndexerSettingsApiPath": "Ruta de API",
"IndexerSettingsCookieHelpText": "Si su sitio requiere una cookie de inicio de sesión para acceder al RSS, tendrá que conseguirla a través de un navegador.",
"IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}",
"IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}",
"IndexerHDBitsSettingsMediums": "Medios",
"IndexerSettingsSeedTimeHelpText": "La cantidad de tiempo que un torrent debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vació usa el predeterminado del cliente de descarga",
"IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.",
"NotificationsEmailSettingsUseEncryption": "Usar Cifrado",
"LastDuration": "Última Duración",
@@ -2057,5 +2057,8 @@
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador."
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador.",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "Estado de lista",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas"
}

View File

@@ -2057,5 +2057,8 @@
"NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise",
"DatabaseMigration": "Migration des bases de données",
"Filters": "Filtres",
"ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double."
"ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double.",
"ImportListsMyAnimeListSettingsListStatus": "Statut de la liste",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Type de liste à partir de laquelle vous souhaitez importer, défini sur 'All' pour toutes les listes",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authentifiez-vous avec MyAnimeList"
}

View File

@@ -2057,5 +2057,8 @@
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Local opcional para mover os downloads concluídos, deixe em branco para usar o local padrão do Deluge",
"DownloadClientDelugeSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Deluge",
"EpisodeRequested": "Episódio Pedido",
"ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador."
"ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador.",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar com MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "Status da Lista",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como 'Todas' para todas as listas"
}

View File

@@ -198,5 +198,7 @@
"AppUpdated": "{appName} actualizat",
"ShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute",
"WeekColumnHeader": "Antetul coloanei săptămânii",
"TimeFormat": "Format ora"
"TimeFormat": "Format ora",
"CustomFilter": "Filtru personalizat",
"CustomFilters": "Filtre personalizate"
}

View File

@@ -1 +1,41 @@
{}
{
"Activity": "Aktivita",
"Absolute": "Celkom",
"AddImportList": "Pridať zoznam importov",
"AddConditionImplementation": "Pridať podmienku - {implementationName}",
"AddConnectionImplementation": "Pridať pripojenie - {implementationName}",
"AddImportListExclusion": "Pridať vylúčenie zoznamu importov",
"AddDownloadClientImplementation": "Pridať klienta pre sťahovanie - {implementationName}",
"AddImportListImplementation": "Pridať zoznam importov - {implementationName}",
"AddReleaseProfile": "Pridať profil vydania",
"AddRemotePathMapping": "Pridať vzdialené mapovanie ciest",
"AddToDownloadQueue": "Pridať do fronty sťahovania",
"AllFiles": "Všetky súbory",
"AddAutoTag": "Pridať automatickú značku",
"AddCondition": "Pridať podmienku",
"AddingTag": "Pridávanie značky",
"Add": "Pridať",
"AgeWhenGrabbed": "Vek (po uchopení)",
"All": "Všetko",
"Age": "Vek",
"About": "O",
"Actions": "Akcie",
"AddAutoTagError": "Nie je možné pridať novú automatickú značku, skúste to znova.",
"AddConnection": "Pridať podmienku",
"AddConditionError": "Nie je možné pridať novú podmienku, skúste to znova.",
"AfterManualRefresh": "Po ručnom obnovení",
"AllResultsAreHiddenByTheAppliedFilter": "Použitý filter skryje všetky výsledky",
"Always": "Vždy",
"AnalyticsEnabledHelpText": "Odosielajte anonymné informácie o používaní a chybách na servery aplikácie {appName}. Zahŕňa to informácie o vašom prehliadači, ktoré stránky webového používateľského rozhrania {appName} používate, hlásenia chýb, ako aj verziu operačného systému a spustenia. Tieto informácie použijeme na stanovenie priorít funkcií a opráv chýb.",
"RestartRequiredHelpTextWarning": "Vyžaduje sa reštart, aby sa zmeny prejavili",
"ApplyTagsHelpTextAdd": "Pridať: Pridať značky do existujúceho zoznamu značiek",
"AddRootFolder": "Pridať koreňový priečinok",
"AddedToDownloadQueue": "Pridané do fronty sťahovania",
"Analytics": "Analytika",
"AddIndexerImplementation": "Pridať Indexer - {implementationName}",
"AddQualityProfile": "Pridať profil kvality",
"Added": "Pridané",
"AlreadyInYourLibrary": "Už vo vašej knižnici",
"AlternateTitles": "Alternatívny názov",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Ako použiť značky na vybratých klientov na sťahovanie"
}

View File

@@ -6,7 +6,7 @@
"AddConnection": "Bağlantı Ekle",
"AddConditionImplementation": "Koşul Ekle - {implementationName}",
"EditConnectionImplementation": "Koşul Ekle - {implementationName}",
"AddConnectionImplementation": "Koşul Ekle - {implementationName}",
"AddConnectionImplementation": "Bağlantı Ekle - {implementationName}",
"AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}",
"EditIndexerImplementation": "Koşul Ekle - {implementationName}",
"AddToDownloadQueue": "İndirme kuyruğuna ekleyin",

View File

@@ -94,5 +94,15 @@
"AudioLanguages": "Мови аудіо",
"AuthForm": "Форми (сторінка входу)",
"Authentication": "Автентифікація",
"AuthenticationMethod": "Метод автентифікації"
"AuthenticationMethod": "Метод автентифікації",
"Yes": "Так",
"AuthenticationRequired": "Потрібна Автентифікація",
"UpdateAll": "Оновити все",
"WhatsNew": "Що нового ?",
"Yesterday": "Вчора",
"AddedToDownloadQueue": "Додано в чергу на завантаження",
"AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.",
"AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм",
"AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль",
"AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача"
}

View File

@@ -174,10 +174,16 @@ namespace NzbDrone.Core.MediaFiles
fileInfoStopwatch.Stop();
_logger.Trace("Reprocessing existing files complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
var filesOnDisk = GetNonVideoFiles(series.Path);
var possibleExtraFiles = FilterPaths(series.Path, filesOnDisk);
RemoveEmptySeriesFolder(series.Path);
var possibleExtraFiles = new List<string>();
if (_diskProvider.FolderExists(series.Path))
{
var extraFiles = GetNonVideoFiles(series.Path);
possibleExtraFiles = FilterPaths(series.Path, extraFiles);
}
CompletedScanning(series, possibleExtraFiles);
}

View File

@@ -78,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
{
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path));
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path), null, localEpisode.CustomFormats);
EnsureEpisodeFolder(episodeFile, localEpisode, filePath);
@@ -89,7 +89,7 @@ namespace NzbDrone.Core.MediaFiles
public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
{
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path));
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path), null, localEpisode.CustomFormats);
EnsureEpisodeFolder(episodeFile, localEpisode, filePath);
@@ -122,6 +122,7 @@ namespace NzbDrone.Core.MediaFiles
}
episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath);
localEpisode.FileNameBeforeRename = episodeFile.RelativePath;
if (localEpisode is not null && _scriptImportDecider.TryImport(episodeFilePath, destinationFilePath, localEpisode, episodeFile, mode) is var scriptImportDecision && scriptImportDecision != ScriptImportDecision.DeferMove)
{
@@ -130,7 +131,6 @@ namespace NzbDrone.Core.MediaFiles
try
{
MoveEpisodeFile(episodeFile, series, episodeFile.Episodes);
localEpisode.FileRenamedAfterScriptImport = true;
}
catch (SameFilenameException)
{

View File

@@ -38,16 +38,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
var firstEpisode = localEpisode.Episodes.First();
var episodeFile = firstEpisode.EpisodeFile.Value;
localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path);
localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path, localEpisode.FileNameBeforeRename);
return localEpisode;
}
public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path)
public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path, string fileNameBeforeRename)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path);
var episodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
var episodeFileTitle = Path.GetFileNameWithoutExtension(fileNameBeforeRename ?? episodeFile.RelativePath);
var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.OriginalFilePath) ?? string.Empty;
if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase)))

View File

@@ -123,6 +123,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
else
{
episodeFile.IndexerFlags = localEpisode.IndexerFlags;
episodeFile.ReleaseType = localEpisode.ReleaseType;
}
// Fall back to parsed information if history is unavailable or missing
@@ -176,9 +177,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
if (localEpisode.ScriptImported)
{
_existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles);
_existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, localEpisode.FileNameBeforeRename);
if (localEpisode.FileRenamedAfterScriptImport)
if (localEpisode.FileNameBeforeRename != episodeFile.RelativePath)
{
_extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile);
}

View File

@@ -129,28 +129,30 @@ namespace NzbDrone.Core.MediaFiles
[EventHandleOrder(EventHandleOrder.Last)]
public void Handle(EpisodeFileDeletedEvent message)
{
if (_configService.DeleteEmptyFolders)
if (!_configService.DeleteEmptyFolders || message.Reason == DeleteMediaFileReason.MissingFromDisk)
{
var series = message.EpisodeFile.Series.Value;
var seriesPath = series.Path;
var folder = message.EpisodeFile.Path.GetParentPath();
return;
}
while (seriesPath.IsParentPath(folder))
var series = message.EpisodeFile.Series.Value;
var seriesPath = series.Path;
var folder = message.EpisodeFile.Path.GetParentPath();
while (seriesPath.IsParentPath(folder))
{
if (_diskProvider.FolderExists(folder))
{
if (_diskProvider.FolderExists(folder))
{
_diskProvider.RemoveEmptySubfolders(folder);
}
folder = folder.GetParentPath();
_diskProvider.RemoveEmptySubfolders(folder);
}
_diskProvider.RemoveEmptySubfolders(seriesPath);
folder = folder.GetParentPath();
}
if (_diskProvider.FolderEmpty(seriesPath))
{
_diskProvider.DeleteFolder(seriesPath, true);
}
_diskProvider.RemoveEmptySubfolders(seriesPath);
if (_diskProvider.FolderEmpty(seriesPath))
{
_diskProvider.DeleteFolder(seriesPath, true);
}
}
}

View File

@@ -9,5 +9,6 @@ namespace NzbDrone.Core.MetadataSource
List<Series> SearchForNewSeriesByImdbId(string imdbId);
List<Series> SearchForNewSeriesByAniListId(int aniListId);
List<Series> SearchForNewSeriesByTmdbId(int tmdbId);
List<Series> SearchForNewSeriesByMyAnimeListId(int malId);
}
}

View File

@@ -90,6 +90,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return results;
}
public List<Series> SearchForNewSeriesByMyAnimeListId(int malId)
{
var results = SearchForNewSeries($"mal:{malId}");
return results;
}
public List<Series> SearchForNewSeriesByTmdbId(int tmdbId)
{
var results = SearchForNewSeries($"tmdb:{tmdbId}");

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
@@ -115,18 +114,18 @@ namespace NzbDrone.Core.Notifications.Ntfy
{
try
{
requestBuilder.Headers.Add("X-Title", title);
requestBuilder.Headers.Add("X-Message", message);
requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString());
requestBuilder.AddQueryParam("title", title);
requestBuilder.AddQueryParam("message", message);
requestBuilder.AddQueryParam("priority", settings.Priority.ToString());
if (settings.Tags.Any())
{
requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(","));
requestBuilder.AddQueryParam("tags", settings.Tags.Join(","));
}
if (!settings.ClickUrl.IsNullOrWhiteSpace())
{
requestBuilder.Headers.Add("X-Click", settings.ClickUrl);
requestBuilder.AddQueryParam("click", settings.ClickUrl);
}
if (!settings.AccessToken.IsNullOrWhiteSpace())

View File

@@ -178,6 +178,8 @@ namespace NzbDrone.Core.Notifications.Webhook
DownloadClient = message.DownloadClientInfo?.Name,
DownloadClientType = message.DownloadClientInfo?.Type,
DownloadId = message.DownloadId,
DownloadStatus = message.TrackedDownload.Status.ToString(),
DownloadStatusMessages = message.TrackedDownload.StatusMessages.Select(x => new WebhookDownloadStatusMessage(x)).ToList(),
CustomFormatInfo = new WebhookCustomFormatInfo(remoteEpisode.CustomFormats, remoteEpisode.CustomFormatScore),
Release = new WebhookGrabbedRelease(message.Release)
};

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Download.TrackedDownloads;
namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookDownloadStatusMessage
{
public string Title { get; set; }
public List<string> Messages { get; set; }
public WebhookDownloadStatusMessage(TrackedDownloadStatusMessage statusMessage)
{
Title = statusMessage.Title;
Messages = statusMessage.Messages.ToList();
}
}
}

View File

@@ -10,6 +10,8 @@ namespace NzbDrone.Core.Notifications.Webhook
public string DownloadClient { get; set; }
public string DownloadClientType { get; set; }
public string DownloadId { get; set; }
public string DownloadStatus { get; set; }
public List<WebhookDownloadStatusMessage> DownloadStatusMessages { get; set; }
public WebhookCustomFormatInfo CustomFormatInfo { get; set; }
public WebhookGrabbedRelease Release { get; set; }
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?<tags1>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*[-_. ](?<title>[^.]*)(\.((?<tags2>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleTitleRegex = new Regex(@"((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled);
private static readonly Regex SubtitleTitleRegex = new Regex(@"^((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled);
public static List<Language> ParseLanguages(string title)
{

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Parser.Model
public int CustomFormatScore { get; set; }
public GrabbedReleaseInfo Release { get; set; }
public bool ScriptImported { get; set; }
public bool FileRenamedAfterScriptImport { get; set; }
public string FileNameBeforeRename { get; set; }
public bool ShouldImportExtras { get; set; }
public List<string> PossibleExtraFiles { get; set; }
public SubtitleTitleInfo SubtitleInfo { get; set; }

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Parser.Model
public bool IsMultiSeason { get; set; }
public bool IsSeasonExtra { get; set; }
public bool IsSplitEpisode { get; set; }
public bool IsMiniSeries { get; set; }
public bool Special { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }

View File

@@ -555,7 +555,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?<season>(?<!\d+)\d{1,4}(?!\d+))(?:[_. ]+(?!\d+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ](?!\d+)(?<remaining>.+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
@@ -564,31 +564,40 @@ namespace NzbDrone.Core.Parser
public static ParsedEpisodeInfo ParsePath(string path)
{
var fileInfo = new FileInfo(path);
var result = ParseTitle(fileInfo.Name);
// Parse using the folder and file separately, but combine if they both parse correctly.
var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name);
var episodeNumberMatch = SimpleEpisodeNumberRegex.Match(fileInfo.Name);
if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null)
if (episodeNumberMatch.Success && fileInfo.Directory?.Name != null && (result == null || result.IsMiniSeries || result.AbsoluteEpisodeNumbers.Any()))
{
var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name);
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
if (parsedFileInfo != null)
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
{
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
var episodeCaptures = episodeNumberMatch.Groups["episode"].Captures.Cast<Capture>().ToList();
var first = ParseNumber(episodeCaptures.First().Value);
var last = ParseNumber(episodeCaptures.Last().Value);
var pathTitle = $"S{seasonMatch.Groups["season"].Value}E{first:00}";
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
if (first != last)
{
parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
pathTitle += $"-E{last:00}";
}
if (episodeNumberMatch.Groups["remaining"].Success)
{
pathTitle += $" {episodeNumberMatch.Groups["remaining"].Value}";
}
var parsedFileInfo = ParseTitle(pathTitle);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
}
}
var result = ParseTitle(fileInfo.Name);
if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number))
{
Logger.Debug("Attempting to parse episode info using directory and file names. {0}", fileInfo.Directory.Name);
@@ -1107,6 +1116,7 @@ namespace NzbDrone.Core.Parser
{
// If no season was found and it's not an absolute only release it should be treated as a mini series and season 1
result.SeasonNumber = 1;
result.IsMiniSeries = true;
}
}
else

View File

@@ -1,6 +1,7 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -51,6 +52,12 @@ namespace NzbDrone.Core.Tv
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", series.Title, sourcePath, destinationPath);
}
if (sourcePath.PathEquals(destinationPath))
{
_logger.ProgressInfo("{0} is already in the specified location '{1}'.", series, destinationPath);
return;
}
try
{
// Ensure the parent of the series folder exists, this will often just be the root folder, but

View File

@@ -43,6 +43,7 @@ namespace Sonarr.Api.V3.ManualImport
item.SeasonNumber = processedItem.SeasonNumber;
item.Episodes = processedItem.Episodes.ToResource();
item.ReleaseType = processedItem.ReleaseType;
item.IndexerFlags = processedItem.IndexerFlags;
item.Rejections = processedItem.Rejections;
item.CustomFormats = processedItem.CustomFormats.ToResource(false);

View File

@@ -5143,6 +5143,23 @@
}
}
}
},
"head": {
"tags": [
"Ping"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PingResource"
}
}
}
}
}
}
},
"/api/v3/qualitydefinition/{id}": {

View File

@@ -22,6 +22,7 @@ namespace NzbDrone.Http
[AllowAnonymous]
[HttpGet("/ping")]
[HttpHead("/ping")]
[Produces("application/json")]
public ActionResult<PingResource> GetStatus()
{