From 428569106499b5e3a463f1990ae2996d1ae4ab49 Mon Sep 17 00:00:00 2001 From: The Dark <12370876+CheAle14@users.noreply.github.com> Date: Sun, 3 Mar 2024 05:19:02 +0000 Subject: [PATCH 01/98] New: Import list exclusion pagination Closes #6079 --- frontend/src/App/State/AppSectionState.ts | 1 + frontend/src/App/State/SettingsAppState.ts | 11 + .../src/Helpers/Hooks/useModalOpenState.ts | 17 ++ .../EditImportListExclusionModal.js | 27 -- .../EditImportListExclusionModal.tsx | 41 +++ .../EditImportListExclusionModalConnector.js | 43 ---- .../EditImportListExclusionModalContent.js | 139 ----------- .../EditImportListExclusionModalContent.tsx | 188 ++++++++++++++ ...mportListExclusionModalContentConnector.js | 117 --------- .../ImportListExclusion.css | 25 -- .../ImportListExclusion.css.d.ts | 3 - .../ImportListExclusion.js | 112 --------- .../ImportListExclusionRow.css | 6 + .../ImportListExclusionRow.css.d.ts | 7 + .../ImportListExclusionRow.tsx | 68 +++++ .../ImportListExclusions.css | 23 -- .../ImportListExclusions.css.d.ts | 3 - .../ImportListExclusions.js | 105 -------- .../ImportListExclusions.tsx | 234 ++++++++++++++++++ .../ImportListExclusionsConnector.js | 59 ----- .../ImportLists/ImportListSettings.js | 4 +- .../Actions/Settings/importListExclusions.js | 43 +++- .../createSettingsSectionSelector.ts | 51 ++-- frontend/src/typings/ImportListExclusion.ts | 6 + .../Exclusions/ImportListExclusionService.cs | 7 + .../ImportListExclusionController.cs | 13 + 26 files changed, 663 insertions(+), 690 deletions(-) create mode 100644 frontend/src/Helpers/Hooks/useModalOpenState.ts delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js create mode 100644 frontend/src/typings/ImportListExclusion.ts diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index cabc39b1c..5bc7dfbac 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -38,6 +38,7 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; + pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index a0bea0973..e4322db69 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -3,10 +3,12 @@ import AppSectionState, { AppSectionItemState, AppSectionSaveState, AppSectionSchemaState, + PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; +import ImportListExclusion from 'typings/ImportListExclusion'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; @@ -41,6 +43,14 @@ export interface ImportListOptionsSettingsAppState extends AppSectionItemState, AppSectionSaveState {} +export interface ImportListExclusionsSettingsAppState + extends AppSectionState, + AppSectionSaveState, + PagedAppSectionState, + AppSectionDeleteState { + pendingChanges: Partial; +} + export type IndexerFlagSettingsAppState = AppSectionState; export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; @@ -48,6 +58,7 @@ export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts new file mode 100644 index 000000000..f5b5a96f0 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; + +export default function useModalOpenState( + initialState: boolean +): [boolean, () => void, () => void] { + const [isOpen, setOpen] = useState(initialState); + + const setModalOpen = useCallback(() => { + setOpen(true); + }, [setOpen]); + + const setModalClosed = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return [isOpen, setModalOpen, setModalClosed]; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js deleted file mode 100644 index 57a7b0e2d..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector'; - -function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditImportListExclusionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx new file mode 100644 index 000000000..9b7afb3ba --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; + +interface EditImportListExclusionModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModal( + props: EditImportListExclusionModalProps +) { + const { isOpen, onModalClose, ...otherProps } = props; + + const dispatch = useDispatch(); + + const onModalClosePress = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.importListExclusions', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js deleted file mode 100644 index cd4338621..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditImportListExclusionModal from './EditImportListExclusionModal'; - -function mapStateToProps() { - return {}; -} - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditImportListExclusionModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.importListExclusions' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js deleted file mode 100644 index 284d1100c..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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 { numberSettingShape, stringSettingShape } from 'Helpers/Props/Shapes/settingShape'; -import translate from 'Utilities/String/translate'; -import styles from './EditImportListExclusionModalContent.css'; - -function EditImportListExclusionModalContent(props) { - const { - id, - isFetching, - error, - isSaving, - saveError, - item, - onInputChange, - onSavePress, - onModalClose, - onDeleteImportListExclusionPress, - ...otherProps - } = props; - - const { - title, - tvdbId - } = item; - - return ( - - - {id ? translate('EditImportListExclusion') : translate('AddImportListExclusion')} - - - - { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('AddImportListExclusionError')} - - } - - { - !isFetching && !error && -
- - {translate('Title')} - - - - - - {translate('TvdbId')} - - - -
- } -
- - - { - id && - - } - - - - - {translate('Save')} - - -
- ); -} - -const ImportListExclusionShape = { - title: PropTypes.shape(stringSettingShape).isRequired, - tvdbId: PropTypes.shape(numberSettingShape).isRequired -}; - -EditImportListExclusionModalContent.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.shape(ImportListExclusionShape).isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onDeleteImportListExclusionPress: PropTypes.func -}; - -export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx new file mode 100644 index 000000000..8570d1acf --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { + saveImportListExclusion, + setImportListExclusionValue, +} from 'Store/Actions/settingsActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import { PendingSection } from 'typings/pending'; +import translate from 'Utilities/String/translate'; +import styles from './EditImportListExclusionModalContent.css'; + +const newImportListExclusion = { + title: '', + tvdbId: 0, +}; + +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function createImportListExclusionSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + const { isFetching, error, isSaving, saveError, pendingChanges, items } = + importListExclusions; + + const mapping = id + ? items.find((i) => i.id === id) + : newImportListExclusion; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +function EditImportListExclusionModalContent( + props: EditImportListExclusionModalContentProps +) { + const { id, onModalClose, onDeleteImportListExclusionPress } = props; + + const dispatch = useDispatch(); + + const dispatchSetImportListExclusionValue = (payload: { + name: string; + value: string | number; + }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }; + + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(props.id)); + const previousIsSaving = usePrevious(isSaving); + + const { title, tvdbId } = item; + + useEffect(() => { + if (!id) { + Object.keys(newImportListExclusion).forEach((name) => { + dispatchSetImportListExclusionValue({ + name, + value: + newImportListExclusion[name as keyof typeof newImportListExclusion], + }); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }); + + const onSavePress = useCallback(() => { + dispatch(saveImportListExclusion({ id })); + }, [dispatch, id]); + + const onInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }, + [dispatch] + ); + + return ( + + + {id + ? translate('EditImportListExclusion') + : translate('AddImportListExclusion')} + + + + {isFetching && } + + {!isFetching && !!error && ( + + {translate('AddImportListExclusionError')} + + )} + + {!isFetching && !error && ( +
+ + {translate('Title')} + + + + + + {translate('TvdbId')} + + + +
+ )} +
+ + + {id && ( + + )} + + + + + {translate('Save')} + + +
+ ); +} + +export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js deleted file mode 100644 index 059223231..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveImportListExclusion, setImportListExclusionValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; - -const newImportListExclusion = { - title: '', - tvdbId: 0 -}; - -function createImportListExclusionSelector() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.importListExclusions, - (id, importListExclusions) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = importListExclusions; - - const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion; - const settings = selectSettings(mapping, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -function createMapStateToProps() { - return createSelector( - createImportListExclusionSelector(), - (importListExclusion) => { - return { - ...importListExclusion - }; - } - ); -} - -const mapDispatchToProps = { - setImportListExclusionValue, - saveImportListExclusion -}; - -class EditImportListExclusionModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newImportListExclusion).forEach((name) => { - this.props.setImportListExclusionValue({ - name, - value: newImportListExclusion[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setImportListExclusionValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveImportListExclusion({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalContentConnector.propTypes = { - id: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setImportListExclusionValue: PropTypes.func.isRequired, - saveImportListExclusion: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css deleted file mode 100644 index 92e533c7e..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css +++ /dev/null @@ -1,25 +0,0 @@ -.importListExclusion { - display: flex; - align-items: stretch; - margin-bottom: 10px; - height: 30px; - border-bottom: 1px solid var(--borderColor); - line-height: 30px; -} - -.title { - @add-mixin truncate; - - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.actions { - display: flex; - justify-content: flex-end; - flex: 1 0 auto; - padding-right: 10px; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts index 213f9816d..d8ea83dc1 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts @@ -2,9 +2,6 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'importListExclusion': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js deleted file mode 100644 index e95561b82..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js +++ /dev/null @@ -1,112 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import styles from './ImportListExclusion.css'; - -class ImportListExclusion extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onEditImportListExclusionPress = () => { - this.setState({ isEditImportListExclusionModalOpen: true }); - }; - - onEditImportListExclusionModalClose = () => { - this.setState({ isEditImportListExclusionModalOpen: false }); - }; - - onDeleteImportListExclusionPress = () => { - this.setState({ - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: true - }); - }; - - onDeleteImportListExclusionModalClose = () => { - this.setState({ isDeleteImportListExclusionModalOpen: false }); - }; - - onConfirmDeleteImportListExclusion = () => { - this.props.onConfirmDeleteImportListExclusion(this.props.id); - }; - - // - // Render - - render() { - const { - id, - title, - tvdbId - } = this.props; - - return ( -
-
{title}
-
{tvdbId}
- -
- - - -
- - - - -
- ); - } -} - -ImportListExclusion.propTypes = { - id: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - tvdbId: PropTypes.number.isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -ImportListExclusion.defaultProps = { - // The drag preview will not connect the drag handle. - connectDragSource: (node) => node -}; - -export default ImportListExclusion; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css new file mode 100644 index 000000000..c154fa5a3 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css @@ -0,0 +1,6 @@ +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 35px; + white-space: nowrap; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts new file mode 100644 index 000000000..d8ea83dc1 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'actions': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx new file mode 100644 index 000000000..37de7940a --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import styles from './ImportListExclusionRow.css'; + +interface ImportListExclusionRowProps extends ImportListExclusion { + onConfirmDeleteImportListExclusion: (id: number) => void; +} + +function ImportListExclusionRow(props: ImportListExclusionRowProps) { + const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props; + + const [ + isEditImportListExclusionModalOpen, + setEditImportListExclusionModalOpen, + setEditImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const onConfirmDeleteImportListExclusionPress = useCallback(() => { + onConfirmDeleteImportListExclusion(id); + }, [id, onConfirmDeleteImportListExclusion]); + + return ( + + {title} + {tvdbId} + + + + + + + + + + ); +} + +export default ImportListExclusionRow; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css deleted file mode 100644 index ecb080585..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css +++ /dev/null @@ -1,23 +0,0 @@ -.importListExclusionsHeader { - display: flex; - margin-bottom: 10px; - font-weight: bold; -} - -.title { - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.addImportListExclusion { - display: flex; - justify-content: flex-end; - padding-right: 10px; -} - -.addButton { - text-align: center; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts index 6cb93f7ce..626717e71 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts @@ -3,9 +3,6 @@ interface CssExports { 'addButton': string; 'addImportListExclusion': string; - 'importListExclusionsHeader': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js deleted file mode 100644 index 9bb7814d9..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js +++ /dev/null @@ -1,105 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import ImportListExclusion from './ImportListExclusion'; -import styles from './ImportListExclusions.css'; - -class ImportListExclusions extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onAddImportListExclusionPress = () => { - this.setState({ isAddImportListExclusionModalOpen: true }); - }; - - onModalClose = () => { - this.setState({ isAddImportListExclusionModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - onConfirmDeleteImportListExclusion, - ...otherProps - } = this.props; - - return ( -
- -
-
- {translate('Title')} -
-
- {translate('TvdbId')} -
-
- -
- { - items.map((item, index) => { - return ( - - ); - }) - } -
- -
- - - -
- - - -
-
- ); - } -} - -ImportListExclusions.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx new file mode 100644 index 000000000..7a15bca91 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -0,0 +1,234 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import IconButton from 'Components/Link/IconButton'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import ImportListExclusionRow from './ImportListExclusionRow'; + +const COLUMNS = [ + { + name: 'title', + label: () => translate('Title'), + isVisible: true, + isSortable: true, + }, + { + name: 'tvdbid', + label: () => translate('TvdbId'), + isVisible: true, + isSortable: true, + }, + { + name: 'actions', + isVisible: true, + isSortable: false, + }, +]; + +interface ImportListExclusionsProps { + useCurrentPage: number; + totalRecords: number; +} + +function createImportListExlucionsSelector() { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + return { + ...importListExclusions, + }; + } + ); +} + +function ImportListExclusions(props: ImportListExclusionsProps) { + const { useCurrentPage, totalRecords } = props; + + const dispatch = useDispatch(); + + const fetchImportListExclusions = useCallback(() => { + dispatch(importListExclusionActions.fetchImportListExclusions()); + }, [dispatch]); + + const deleteImportListExclusion = useCallback( + (payload: { id: number }) => { + dispatch(importListExclusionActions.deleteImportListExclusion(payload)); + }, + [dispatch] + ); + + const gotoImportListExclusionFirstPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + }, [dispatch]); + + const gotoImportListExclusionPreviousPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage()); + }, [dispatch]); + + const gotoImportListExclusionNextPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionNextPage()); + }, [dispatch]); + + const gotoImportListExclusionLastPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionLastPage()); + }, [dispatch]); + + const gotoImportListExclusionPage = useCallback( + (page: number) => { + dispatch( + importListExclusionActions.gotoImportListExclusionPage({ page }) + ); + }, + [dispatch] + ); + + const setImportListExclusionSort = useCallback( + (sortKey: { sortKey: string }) => { + dispatch( + importListExclusionActions.setImportListExclusionSort({ sortKey }) + ); + }, + [dispatch] + ); + + const setImportListTableOption = useCallback( + (payload: { pageSize: number }) => { + dispatch( + importListExclusionActions.setImportListExclusionTableOption(payload) + ); + + if (payload.pageSize) { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + } + }, + [dispatch] + ); + + const repopulate = useCallback(() => { + gotoImportListExclusionFirstPage(); + }, [gotoImportListExclusionFirstPage]); + + useEffect(() => { + registerPagePopulator(repopulate); + + if (useCurrentPage) { + fetchImportListExclusions(); + } else { + gotoImportListExclusionFirstPage(); + } + + return () => unregisterPagePopulator(repopulate); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onConfirmDeleteImportListExclusion = useCallback( + (id: number) => { + deleteImportListExclusion({ id }); + repopulate(); + }, + [deleteImportListExclusion, repopulate] + ); + + const selected = useSelector(createImportListExlucionsSelector()); + + const { + isFetching, + isPopulated, + items, + pageSize, + sortKey, + error, + sortDirection, + ...otherProps + } = selected; + + const [ + isAddImportListExclusionModalOpen, + setAddImportListExclusionModalOpen, + setAddImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const isFetchingForFirstTime = isFetching && !isPopulated; + + return ( +
+ + + + {items.map((item) => { + return ( + + ); + })} + + + + + + + + + + +
+ + + + +
+
+ ); +} + +export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js deleted file mode 100644 index 184788cec..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js +++ /dev/null @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteImportListExclusion, fetchImportListExclusions } from 'Store/Actions/settingsActions'; -import ImportListExclusions from './ImportListExclusions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.importListExclusions, - (importListExclusions) => { - return { - ...importListExclusions - }; - } - ); -} - -const mapDispatchToProps = { - fetchImportListExclusions, - deleteImportListExclusion -}; - -class ImportListExclusionsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchImportListExclusions(); - } - - // - // Listeners - - onConfirmDeleteImportListExclusion = (id) => { - this.props.deleteImportListExclusion({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ImportListExclusionsConnector.propTypes = { - fetchImportListExclusions: PropTypes.func.isRequired, - deleteImportListExclusion: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index de1d486b6..1ec50526e 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; +import ImportListsExclusions from './ImportListExclusions/ImportListExclusions'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; import ImportListOptions from './Options/ImportListOptions'; @@ -113,7 +113,7 @@ class ImportListSettings extends Component { onChildStateChange={this.onChildStateChange} /> - + ; -type GetSectionState = AppState['settings'][Name]; -type GetSettingsSectionItemType = - GetSectionState extends AppSectionItemState - ? R - : GetSectionState extends AppSectionState - ? R +type SectionsWithItemNames = { + [K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState + ? K : never; +}[keyof SettingsAppState]; -type AppStateWithPending = { - item?: GetSettingsSectionItemType; - pendingChanges?: Partial>; - saveError?: Error; -} & GetSectionState; +type GetSectionState = + SettingsAppState[Name]; +type GetSettingsSectionItemType = + GetSectionState extends AppSectionItemState ? R : never; -function createSettingsSectionSelector( - section: Name -) { +function createSettingsSectionSelector< + Name extends SectionsWithItemNames, + T extends GetSettingsSectionItemType +>(section: Name) { return createSelector( (state: AppState) => state.settings[section], (sectionSettings) => { - const { item, pendingChanges, saveError, ...other } = - sectionSettings as AppStateWithPending; + const { item, pendingChanges, ...other } = sectionSettings; - const { settings, ...rest } = selectSettings( - item, - pendingChanges, - saveError - ); + const saveError = + 'saveError' in sectionSettings ? sectionSettings.saveError : undefined; + + const { + settings, + pendingChanges: selectedPendingChanges, + ...rest + } = selectSettings(item, pendingChanges, saveError); return { ...other, saveError, - settings: settings as PendingSection>, + settings: settings as PendingSection, + pendingChanges: selectedPendingChanges as Partial, ...rest, }; } diff --git a/frontend/src/typings/ImportListExclusion.ts b/frontend/src/typings/ImportListExclusion.ts new file mode 100644 index 000000000..ec9add4dd --- /dev/null +++ b/frontend/src/typings/ImportListExclusion.ts @@ -0,0 +1,6 @@ +import ModelBase from 'App/ModelBase'; + +export default interface ImportListExclusion extends ModelBase { + tvdbId: number; + title: string; +} diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs index 09871fef3..2a9f0a9ec 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -9,6 +10,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions { ImportListExclusion Add(ImportListExclusion importListExclusion); List All(); + PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); ImportListExclusion Get(int id); ImportListExclusion FindByTvdbId(int tvdbId); @@ -54,6 +56,11 @@ namespace NzbDrone.Core.ImportLists.Exclusions return _repo.All().ToList(); } + public PagingSpec Paged(PagingSpec pagingSpec) + { + return _repo.GetPaged(pagingSpec); + } + public void HandleAsync(SeriesDeletedEvent message) { if (!message.AddImportListExclusion) diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 4a701347c..d9cd55c03 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ImportLists.Exclusions; using Sonarr.Http; +using Sonarr.Http.Extensions; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; @@ -29,11 +31,22 @@ namespace Sonarr.Api.V3.ImportLists [HttpGet] [Produces("application/json")] + [Obsolete("Deprecated")] public List GetImportListExclusions() { return _importListExclusionService.All().ToResource(); } + [HttpGet("paged")] + [Produces("application/json")] + public PagingResource GetImportListExclusionsPaged([FromQuery] PagingRequestResource paging) + { + var pagingResource = new PagingResource(paging); + var pageSpec = pagingResource.MapToPagingSpec(); + + return pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource); + } + [RestPostById] [Consumes("application/json")] public ActionResult AddImportListExclusion(ImportListExclusionResource resource) From 7f061a9583870d9b60427c8be2d55f695cc545d7 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 3 Mar 2024 00:24:11 +0000 Subject: [PATCH 02/98] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore-downstream Co-authored-by: Fixer Co-authored-by: GkhnGRBZ Co-authored-by: Havok Dan Co-authored-by: Nicolò Castagnola Co-authored-by: Weblate Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 18 +--- src/NzbDrone.Core/Localization/Core/it.json | 1 - .../Localization/Core/pt_BR.json | 4 +- src/NzbDrone.Core/Localization/Core/ro.json | 1 - src/NzbDrone.Core/Localization/Core/ru.json | 1 - src/NzbDrone.Core/Localization/Core/tr.json | 82 ++++++++++++++++++- .../Localization/Core/zh_CN.json | 1 - 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 40ea83ad4..fd571509c 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -6,7 +6,7 @@ "RemoveFailedDownloads": "Entferne fehlgeschlagene Downloads", "ApplyChanges": "Änderungen anwenden", "AutomaticAdd": "Automatisch hinzufügen", - "CountSeasons": "{Anzahl} Staffeln", + "CountSeasons": "{count} Staffeln", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Es ist kein Download-Client verfügbar", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich.", "DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {downloadClientName} legt Downloads im Stammordner {rootFolderPath} ab. Sie sollten nicht in einen Stammordner herunterladen.", @@ -157,7 +157,6 @@ "VisitTheWikiForMoreDetails": "Besuchen Sie das Wiki für weitere Details: ", "UpgradeUntilEpisodeHelpText": "Sobald diese Qualität erreicht ist, lädt {appName} keine Episoden mehr herunter", "SslCertPasswordHelpText": "Passwort für die PFX-Datei", - "ShowMonitoredHelpText": "", "SeriesEditRootFolderHelpText": "Durch das Verschieben von Serien in denselben Stammordner können Serienordner umbenannt werden, um sie an den aktualisierten Titel oder das Benennungsformat anzupassen", "SelectLanguages": "Sprache auswählen", "SelectEpisodesModalTitle": "{modalTitle} – Episode(n) auswählen", @@ -242,7 +241,6 @@ "ApplyTagsHelpTextHowToApplyImportLists": "So wenden Sie Tags auf die ausgewählten Importlisten an", "ApplyTagsHelpTextHowToApplyDownloadClients": "So wenden Sie Tags auf die ausgewählten Download-Clients an", "ApplyTagsHelpTextHowToApplyIndexers": "So wenden Sie Tags auf die ausgewählten Indexer an", - "Retention": "", "RestrictionsLoadError": "Einschränkungen können nicht geladen werden", "SslCertPath": "SSL-Zertifikatpfad", "TheTvdb": "TheTVDB", @@ -313,8 +311,6 @@ "DeleteEpisodeFromDisk": "Episode von der Festplatte löschen", "DeleteEpisodesFilesHelpText": "Löschen Sie die Episodendateien und den Serienordner", "DeleteImportList": "Importliste löschen", - "DeleteImportListExclusion": "", - "DeleteImportListExclusionMessageText": "", "DeleteIndexerMessageText": "Sind Sie sicher, dass Sie den Indexer „{name}“ löschen möchten?", "DeleteQualityProfile": "Qualitätsprofil löschen", "DeleteReleaseProfile": "Release-Profil löschen", @@ -342,7 +338,6 @@ "DoneEditingGroups": "Bearbeiten der Gruppen abgeschlossen", "DotNetVersion": ".NET", "Download": "Herunterladen", - "DownloadClient": "", "DownloadClientDelugeSettingsUrlBaseHelpText": "Fügt der Deluge-JSON-URL ein Präfix hinzu, siehe {url}", "DownloadClientDelugeTorrentStateError": "Deluge meldet einen Fehler", "DownloadClientDelugeValidationLabelPluginFailure": "Konfiguration des Labels fehlgeschlagen", @@ -483,7 +478,6 @@ "Settings": "Einstellungen", "SetTags": "Tags festlegen", "SetPermissionsLinuxHelpTextWarning": "Wenn Sie nicht sicher sind, was diese Einstellungen bewirken, ändern Sie sie nicht.", - "ShowMonitored": "", "ShowEpisodeInformation": "Episodeninformationen anzeigen", "ShowAdvanced": "Erweitert anzeigen", "Space": "Platz", @@ -492,8 +486,6 @@ "StartImport": "Import starten", "StartProcessing": "Verarbeitung starten", "Tasks": "Aufgaben", - "TagIsNotUsedAndCanBeDeleted": "", - "TagDetails": "", "ThemeHelpText": "Ändern Sie das Benutzeroberflächen-Design der Anwendung. Das „Auto“-Design verwendet Ihr Betriebssystemdesign, um den Hell- oder Dunkelmodus festzulegen. Inspiriert vom Theme.Park", "Theme": "Design", "TestAllLists": "Prüfe alle Listen", @@ -501,13 +493,11 @@ "TimeLeft": "Zeit übrig", "Title": "Titel", "ToggleMonitoredToUnmonitored": "Überwacht, klicken Sie, um die Überwachung aufzuheben", - "ToggleMonitoredSeriesUnmonitored ": "", "TorrentBlackholeSaveMagnetFiles": "Speicher Magnetdateien", "Total": "Gesamt", "TorrentsDisabled": "Torrents deaktiviert", "Torrents": "Torrents", "TvdbIdExcludeHelpText": "Die TVDB-ID der auszuschließenden Serie", - "Trace": "", "UiSettingsLoadError": "Die Benutzeroberflächen Einstellungen können nicht geladen werden", "Umask750Description": "{octal} – Besitzer schreibt, Gruppe liest", "Umask": "Umask", @@ -519,7 +509,6 @@ "Unavailable": "Nicht verfügbar", "UnselectAll": "Alle abwählen", "UnsavedChanges": "Nicht gespeicherte Änderungen", - "Unmonitored": "", "UpdateAutomaticallyHelpText": "Updates automatisch herunterladen und installieren. Sie können weiterhin über System: Updates installieren", "UpdateAvailableHealthCheckMessage": "Neues Update ist verfügbar", "UpdateMechanismHelpText": "Verwenden Sie den integrierten Updater von {appName} oder ein Skript", @@ -535,7 +524,6 @@ "Username": "Nutzername", "UsenetDelayTime": "Usenet-Verzögerung: {usenetDelay}", "UsenetDelayHelpText": "Verzögerung in Minuten, bevor Sie eine Veröffentlichung aus dem Usenet erhalten", - "VideoDynamicRange": "", "VideoCodec": "Video-Codec", "VersionNumber": "Version {Version}", "Version": "Version", @@ -572,7 +560,6 @@ "DeleteRemotePathMappingMessageText": "Sind Sie sicher, dass Sie diese Remote-Pfadzuordnung löschen möchten?", "DeleteSelectedEpisodeFilesHelpText": "Sind Sie sicher, dass Sie die ausgewählten Episodendateien löschen möchten?", "DeleteSpecificationHelpText": "Sind Sie sicher, dass Sie die Spezifikation „{name}“ löschen möchten?", - "DeleteTag": "", "Donations": "Spenden", "Release": "Veröffentlichung", "RelativePath": "Relativer Pfad", @@ -606,7 +593,6 @@ "RemoveSelectedItems": "Markierte Einträge löschen", "RetentionHelpText": "Nur Usenet: Auf Null setzen, um eine unbegrenzte Aufbewahrung festzulegen", "Standard": "Standard", - "Tags": "", "Usenet": "Usenet", "ConnectionLostReconnect": "{appName} wird versuchen, automatisch eine Verbindung herzustellen, oder Sie können unten auf „Neu laden“ klicken.", "CustomFormatJson": "Benutzerdefiniertes JSON-Format", @@ -620,7 +606,6 @@ "UsenetDisabled": "Usenet deaktiviert", "UrlBase": "URL-Basis", "UpgradeUntilThisQualityIsMetOrExceeded": "Führe ein Upgrade durch, bis diese Qualität erreicht oder überschritten wird", - "UpgradesAllowedHelpText": "", "RemovedSeriesMultipleRemovedHealthCheckMessage": "Die Serien {series} wurden aus TheTVDB entfernt", "RemovedFromTaskQueue": "Aus der Aufgabenwarteschlange entfernt", "SceneNumbering": "Szenennummerierung", @@ -770,7 +755,6 @@ "UsenetDelay": "Usenet-Verzögerung", "UsenetBlackholeNzbFolder": "NZB-Ordner", "UrlBaseHelpText": "Für die Reverse-Proxy-Unterstützung ist der Standardwert leer", - "UpgradeUntilCustomFormatScoreEpisodeHelpText": "", "TestParsing": "Parsing testen", "Test": "Prüfen", "TestAll": "Alle prüfen", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index d1e93e9d3..2b98de2c4 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -167,7 +167,6 @@ "Custom": "Personalizzato", "CustomFormatJson": "Formato Personalizzato JSON", "Day": "Giorno", - "AddListExclusion": "Aggiungi Lista Esclusioni", "AddedDate": "Aggiunto: {date}", "AirsTbaOn": "Verrà trasmesso su {networkLabel}", "AirsTimeOn": "alle {time} su {networkLabel}", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8f5e7e63a..940cb956f 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2050,5 +2050,7 @@ "CustomFilter": "Filtro Personalizado", "Filters": "Filtros", "Label": "Rótulo", - "LabelIsRequired": "Rótulo é requerido" + "LabelIsRequired": "Rótulo é requerido", + "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo a URL {connectionName}, como {url}", + "ReleaseType": "Tipo de Lançamento" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 68252092b..0a28d0f4a 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -132,7 +132,6 @@ "AuthForm": "Formulare (Pagina de autentificare)", "AuthenticationMethodHelpText": "Solicitați nume utilizator și parola pentru a accesa {appName}", "AuthenticationRequired": "Autentificare necesara", - "Authentication": "", "AddNewSeriesError": "Nu s-au putut încărca rezultatele căutării, încercați din nou.", "AddSeriesWithTitle": "Adăugați {title}", "AlreadyInYourLibrary": "Deja în biblioteca dvs.", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index f5271efdd..95e74778b 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -197,7 +197,6 @@ "AlreadyInYourLibrary": "Уже в вашей библиотеке", "Always": "Всегда", "Conditions": "Условия", - "AddAutoTag": "", "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index c1633f7ce..787097f4e 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -16,7 +16,7 @@ "Actions": "Eylemler", "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası", "AddListExclusionError": "Yeni bir hariç tutma listesi eklenemiyor, lütfen tekrar deneyin.", - "AddListExclusion": "Liste Hariç Tutma Ekle", + "AddListExclusion": "Hariç Tutma Listesine Ekle", "AddNewRestriction": "Yeni kısıtlama ekle", "AddedDate": "Eklendi: {date}", "Activity": "Etkinlik", @@ -65,5 +65,83 @@ "AddListError": "Yeni bir liste eklenemiyor, lütfen tekrar deneyin.", "AddNew": "Yeni Ekle", "AddListExclusionSeriesHelpText": "Dizilerin {appName} listeler tarafından eklenmesini önleyin", - "AddRootFolderError": "Kök klasör eklenemiyor" + "AddRootFolderError": "Kök klasör eklenemiyor", + "CountImportListsSelected": "{count} içe aktarma listesi seçildi", + "CustomFormatsSpecificationFlag": "Bayrak", + "ClickToChangeIndexerFlags": "Dizin oluşturucu bayraklarını değiştirmek için tıklayın", + "ClickToChangeReleaseGroup": "Sürüm grubunu değiştirmek için tıklayın", + "AppUpdated": "{appName} Güncellendi", + "ApplicationURL": "Uygulama URL'si", + "ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin", + "ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır?", + "ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldırın", + "AuthenticationRequiredPasswordHelpTextWarning": "Yeni şifre girin", + "AuthenticationRequiredUsernameHelpTextWarning": "Yeni kullanıcı adınızı girin", + "AuthenticationMethodHelpTextWarning": "Lütfen geçerli bir kimlik doğrulama yöntemi seçin", + "AutoTaggingRequiredHelpText": "Otomatik etiketleme kuralının uygulanabilmesi için bu {implementationName} koşulunun eşleşmesi gerekir. Aksi takdirde tek bir {implementationName} eşleşmesi yeterlidir.", + "BlocklistLoadError": "Engellenenler listesi yüklenemiyor", + "BypassDelayIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip sürüm olduğunda gecikmeyi atlayın", + "ConnectionLostToBackend": "{appName}'ın arka uçla bağlantısı kesildi ve işlevselliğin geri kazanılması için yeniden yüklenmesi gerekecek.", + "CustomFormatJson": "Özel JSON Formatı", + "AutomaticAdd": "Otomatik Ekle", + "CustomFilter": "Özel Filtre", + "CustomFormatUnknownConditionOption": "'{implementation}' koşulu için bilinmeyen seçenek '{key}'", + "AutoTagging": "Otomatik Etiketleme", + "AutoTaggingNegateHelpText": "İşaretlenirse, {implementationName} koşulu eşleştiğinde otomatik etiketleme kuralı uygulanmayacaktır.", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Seçilen indirme istemcilerine etiketler nasıl uygulanır?", + "ApplyTagsHelpTextHowToApplyImportLists": "Seçilen içe aktarma listelerine etiketler nasıl uygulanır?", + "AuthenticationRequiredHelpText": "İstekler için Kimlik doğrulamanın gereklilik ayarını değiştirin. Riskleri anlamadığınız sürece değiştirmeyin.", + "AutoTaggingLoadError": "Otomatik etiketleme yüklenemiyor", + "BypassDelayIfAboveCustomFormatScore": "Özel Format Koşullarının Üstündeyse Baypas Et", + "Clone": "Klon", + "CouldNotFindResults": "'{term}' için herhangi bir sonuç bulunamadı", + "AudioLanguages": "Ses Dilleri", + "ApplicationUrlHelpText": "Bu uygulamanın http(s)://, bağlantı noktası ve URL tabanını içeren harici URL'si", + "ApplyChanges": "Değişiklikleri Uygula", + "BlocklistAndSearch": "Engellenenler Listesi ve Arama", + "BlocklistAndSearchHint": "Engellenenler listesine ekledikten sonra yenisini aramaya başlayın", + "BlocklistAndSearchMultipleHint": "Engellenenler listesine ekledikten sonra yedekleri aramaya başlayın", + "BlocklistMultipleOnlyHint": "Yedekleri aramadan engelleme listesi", + "BlocklistOnly": "Yalnızca Engellenenler Listesi", + "BlocklistOnlyHint": "Yenisini aramadan engelleme listesi", + "BlocklistReleaseHelpText": "Bu sürümün {appName} tarafından RSS veya Otomatik Arama yoluyla yeniden indirilmesi engelleniyor", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Sürümün puanı, yapılandırılan minimum özel format puanından yüksek olduğunda bypass'ı etkinleştirin", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Tercih edilen protokolde gecikmeyi atlamak için gereken Minimum Özel Format Puanı", + "BypassDelayIfHighestQuality": "En Yüksek Kalitedeyse Atla", + "ChangeCategory": "Kategoriyi Değiştir", + "ChangeCategoryHint": "İndirme İstemcisi'nden indirme işlemini 'İçe Aktarma Sonrası Kategorisi' olarak değiştirir", + "ChangeCategoryMultipleHint": "İndirme istemcisinden indirmeleri 'İçe Aktarma Sonrası Kategorisi' olarak değiştirir", + "ChooseImportMode": "İçe Aktarma Modunu Seçin", + "ClearBlocklist": "Engellenenler listesini temizle", + "ConnectSettingsSummary": "Bildirimler, medya sunucularına/oynatıcılara bağlantılar ve özel komut dosyaları", + "CountDownloadClientsSelected": "{count} indirme istemcisi seçildi", + "CustomFormatUnknownCondition": "Bilinmeyen Özel Biçim koşulu '{implementation}'", + "CustomFormatsSpecificationRegularExpression": "Düzenli ifade", + "AppDataDirectory": "Uygulama Veri Dizini", + "ChownGroup": "Chown Grubu", + "ConditionUsingRegularExpressions": "Bu koşul Normal İfadeler kullanılarak eşleşir. `\\^$.|?*+()[{` karakterlerinin özel anlamlara sahip olduğunu ve `\\` ile kaçılması gerektiğini unutmayın.", + "BlackholeFolderHelpText": "{appName} uygulamasının {extension} dosyasını depolayacağı klasör", + "BlackholeWatchFolder": "İzleme Klasörü", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimum Özel Format Puanı", + "Cancel": "Vazgeç", + "Category": "Kategori", + "CertificateValidationHelpText": "HTTPS sertifika doğrulamasının sıkılığını değiştirin. Riskleri anlamadığınız sürece değişmeyin.", + "CloneCondition": "Klon Durumu", + "CountIndexersSelected": "{count} dizin oluşturucu seçildi", + "CustomFormatsSpecificationRegularExpressionHelpText": "Özel Format RegEx Büyük/Küçük Harfe Duyarsızdır", + "AutoRedownloadFailed": "Yeniden İndirme Başarısız", + "AutoRedownloadFailedFromInteractiveSearch": "Etkileşimli Aramadan Yeniden İndirme Başarısız Oldu", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Başarısız indirmeler, etkileşimli aramada bulunduğunda otomatik olarak farklı bir versiyonu arayın ve indirmeyi deneyin", + "ApplyTagsHelpTextReplace": "Değiştir: Etiketleri girilen etiketlerle değiştirin (tüm etiketleri kaldırmak için etiket girmeyin)", + "AuthenticationMethod": "Kimlik Doğrulama Yöntemi", + "AuthenticationRequired": "Kimlik Doğrulama Gerekli", + "AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz.", + "ClearBlocklistMessageText": "Engellenenler listesindeki tüm öğeleri temizlemek istediğinizden emin misiniz?", + "AutomaticUpdatesDisabledDocker": "Docker güncelleme mekanizması kullanıldığında otomatik güncellemeler doğrudan desteklenmez. Kapsayıcı görüntüsünü {appName} dışında güncellemeniz veya bir komut dosyası kullanmanız gerekecek", + "ConnectionLostReconnect": "{appName} otomatik bağlanmayı deneyecek veya aşağıda yeniden yükle seçeneğini işaretleyebilirsiniz.", + "BlackholeWatchFolderHelpText": "{appName} uygulamasının tamamlanmış indirmeleri içe aktaracağı klasör", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Yeni şifreyi onayla", + "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", + "CloneAutoTag": "Otomatik Etiketi Klonla" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index f95f32f6b..0c6733450 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1762,7 +1762,6 @@ "NotificationsNtfySettingsServerUrl": "服务器 URL", "NotificationsNtfySettingsPasswordHelpText": "密码,可选", "NotificationsPushBulletSettingSenderIdHelpText": "发送通知的设备 ID,使用 pushbullet.com 设备 URL 中的 device_iden 参数值,或者留空来自行发送", - "NotificationsPushBulletSettingsAccessToken": "", "NotificationsPushBulletSettingsChannelTags": "频道标签", "NotificationsPushBulletSettingsChannelTagsHelpText": "通知的目标频道标签列表", "NotificationsPushBulletSettingsDeviceIds": "设备 ID", From 64c6a8879beb1b17122c8f6f74bf7b3cf4dd1570 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 1 Mar 2024 23:24:47 -0800 Subject: [PATCH 03/98] Queue Manual Import commands at high priority --- src/Sonarr.Api.V3/Commands/CommandController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/Commands/CommandController.cs b/src/Sonarr.Api.V3/Commands/CommandController.cs index c5b892a98..1b17916de 100644 --- a/src/Sonarr.Api.V3/Commands/CommandController.cs +++ b/src/Sonarr.Api.V3/Commands/CommandController.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Composition; using NzbDrone.Common.Serializer; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ProgressMessaging; @@ -61,6 +62,9 @@ namespace Sonarr.Api.V3.Commands using (var reader = new StreamReader(Request.Body)) { var body = reader.ReadToEnd(); + var priority = commandType == typeof(ManualImportCommand) + ? CommandPriority.High + : CommandPriority.Normal; dynamic command = STJson.Deserialize(body, commandType); @@ -69,7 +73,8 @@ namespace Sonarr.Api.V3.Commands command.SendUpdatesToClient = true; command.ClientUserAgent = Request.Headers["UserAgent"]; - var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + var trackedCommand = _commandQueueManager.Push(command, priority, CommandTrigger.Manual); + return Created(trackedCommand.Id); } } From 71c2c0570b45c0b7613e61b453cce23ef6e34980 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 2 Mar 2024 21:19:44 -0800 Subject: [PATCH 04/98] Renamed SeasonPackSpecification to ReleaseTypeSpecification --- ...pecification.cs => ReleaseTypeSpecification.cs} | 4 ++-- .../Migration/205_rename_season_pack_spec.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) rename src/NzbDrone.Core/CustomFormats/Specifications/{SeasonPackSpecification.cs => ReleaseTypeSpecification.cs} (92%) create mode 100644 src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs similarity index 92% rename from src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs rename to src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs index acc6d9c4d..d14a6e041 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { - public class SeasonPackSpecificationValidator : AbstractValidator + public class SeasonPackSpecificationValidator : AbstractValidator { public SeasonPackSpecificationValidator() { @@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats } } - public class SeasonPackSpecification : CustomFormatSpecificationBase + public class ReleaseTypeSpecification : CustomFormatSpecificationBase { private static readonly SeasonPackSpecificationValidator Validator = new (); diff --git a/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs b/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs new file mode 100644 index 000000000..23e40e0e5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(205)] + public class rename_season_pack_spec : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.Sql("UPDATE \"CustomFormats\" SET \"Specifications\" = REPLACE(\"Specifications\", 'SeasonPackSpecification', 'ReleaseTypeSpecification')"); + } + } +} From 13af6f57796e54c3949cf340e03f020e6f8575c4 Mon Sep 17 00:00:00 2001 From: Louis R Date: Sun, 3 Mar 2024 06:20:36 +0100 Subject: [PATCH 05/98] Fixed: Don't disable IPv6 in IPv6-only Environment Closes #6545 --- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 9eb4cc1e4..2215a953f 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,7 +1,9 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; using System.Text; @@ -247,6 +249,18 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } + private static bool HasRoutableIPv4Address() + { + // Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses + 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))); + } + private static async ValueTask 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. @@ -270,10 +284,8 @@ namespace NzbDrone.Common.Http.Dispatchers } catch { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance) - // but in the interest of keeping this implementation simple, this is acceptable. - useIPv6 = false; + // Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections. + useIPv6 = !HasRoutableIPv4Address(); } finally { From e5f19f01fa657325cc981421dc44e2f5eb14b6d3 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 2 Mar 2024 23:21:16 -0600 Subject: [PATCH 06/98] Update AddSeries Messaging and Logging --- src/NzbDrone.Core/Tv/AddSeriesService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs index f985b96fb..4afa17f4d 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesService.cs @@ -78,13 +78,13 @@ namespace NzbDrone.Core.Tv series.Added = added; if (existingSeriesTvdbIds.Any(f => f == series.TvdbId)) { - _logger.Debug("TVDB ID {0} was not added due to validation failure: Series already exists in database", s.TvdbId); + _logger.Debug("TVDB ID {0} was not added due to validation failure: Series {1} already exists in database", s.TvdbId, s); continue; } if (seriesToAdd.Any(f => f.TvdbId == series.TvdbId)) { - _logger.Debug("TVDB ID {0} was not added due to validation failure: Series already exists on list", s.TvdbId); + _logger.Trace("TVDB ID {0} was already added from another import list, not adding series {1} again", s.TvdbId, s); continue; } @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Tv throw; } - _logger.Debug("TVDB ID {0} was not added due to validation failures. {1}", s.TvdbId, ex.Message); + _logger.Debug("Series {0} with TVDB ID {1} was not added due to validation failures. {2}", s, s.TvdbId, ex.Message); } } @@ -121,7 +121,7 @@ namespace NzbDrone.Core.Tv } catch (SeriesNotFoundException) { - _logger.Error("TVDB ID {0} was not found, it may have been removed from TheTVDB. Path: {1}", newSeries.TvdbId, newSeries.Path); + _logger.Error("Series {0} with TVDB ID {1} was not found, it may have been removed from TheTVDB. Path: {2}", newSeries, newSeries.TvdbId, newSeries.Path); throw new ValidationException(new List { From 20273b07ad0cd1ba7d9797a91c50980054bd41ca Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 2 Mar 2024 14:00:58 -0800 Subject: [PATCH 07/98] Properly type validation errors/warnings --- frontend/src/typings/pending.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index 53e885bcb..5cdcbc003 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -1,7 +1,21 @@ +export interface ValidationFailure { + propertyName: string; + errorMessage: string; + severity: 'error' | 'warning'; +} + +export interface ValidationError extends ValidationFailure { + isWarning: false; +} + +export interface ValidationWarning extends ValidationFailure { + isWarning: true; +} + export interface Pending { value: T; - errors: any[]; - warnings: any[]; + errors: ValidationError[]; + warnings: ValidationWarning[]; } export type PendingSection = { From 07bd159436935a7adb87ae1b6924a4d42d719b0f Mon Sep 17 00:00:00 2001 From: nopoz Date: Sat, 2 Mar 2024 21:22:03 -0800 Subject: [PATCH 08/98] New: Add download directory & move completed for Deluge Closes #4575 --- .../Download/Clients/Deluge/DelugeProxy.cs | 42 ++++++++++++++----- .../Download/Clients/Deluge/DelugeSettings.cs | 6 +++ src/NzbDrone.Core/Localization/Core/en.json | 4 ++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 0b39ebc01..ea670cfd6 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Dynamic; using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -101,11 +103,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings) { - var options = new - { - add_paused = settings.AddPaused, - remove_at_ratio = false - }; + dynamic options = new ExpandoObject(); + + options.add_paused = settings.AddPaused; + options.remove_at_ratio = false; + + if (settings.DownloadDirectory.IsNotNullOrWhiteSpace()) + { + options.download_location = settings.DownloadDirectory; + } + + if (settings.CompletedDirectory.IsNotNullOrWhiteSpace()) + { + options.move_completed_path = settings.CompletedDirectory; + options.move_completed = true; + } var response = ProcessRequest(settings, "core.add_torrent_magnet", magnetLink, options); @@ -114,11 +126,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings) { - var options = new - { - add_paused = settings.AddPaused, - remove_at_ratio = false - }; + dynamic options = new ExpandoObject(); + + options.add_paused = settings.AddPaused; + options.remove_at_ratio = false; + + if (settings.DownloadDirectory.IsNotNullOrWhiteSpace()) + { + options.download_location = settings.DownloadDirectory; + } + + if (settings.CompletedDirectory.IsNotNullOrWhiteSpace()) + { + options.move_completed_path = settings.CompletedDirectory; + options.move_completed = true; + } var response = ProcessRequest(settings, "core.add_torrent_file", filename, fileContent, options); diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index 03f266189..f18643510 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -61,6 +61,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } + [FieldDefinition(10, Label = "DownloadClientDelugeSettingsDirectory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryHelpText")] + public string DownloadDirectory { get; set; } + + [FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")] + public string CompletedDirectory { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 40cc2f581..ede7a0ca3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -413,6 +413,10 @@ "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} was unable to add the label to {clientName}.", "DownloadClientDelugeValidationLabelPluginInactive": "Label plugin not activated", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "You must have the Label plugin enabled in {clientName} to use categories.", + "DownloadClientDelugeSettingsDirectory": "Download Directory", + "DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location", + "DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location", "DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Optional shared folder to put downloads into, leave blank to use the default Download Station location", "DownloadClientDownloadStationValidationApiVersion": "Download Station API version not supported, should be at least {requiredVersion}. It supports from {minVersion} to {maxVersion}", From 32c32e2f884adf136cc5c42ab8f32a8058ea7707 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 2 Mar 2024 17:18:55 -0800 Subject: [PATCH 09/98] Fixed: Issue extracting subtitle information for unknown episodes --- .../Extras/Subtitles/ExistingSubtitleImporter.cs | 9 +++++---- .../Aggregation/Aggregators/AggregateSubtitleInfo.cs | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index 6c5a5481e..05fdf5770 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -78,11 +79,11 @@ namespace NzbDrone.Core.Extras.Subtitles SeasonNumber = localEpisode.SeasonNumber, EpisodeFileId = firstEpisode.EpisodeFileId, RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), - Language = localEpisode.SubtitleInfo.Language, - LanguageTags = localEpisode.SubtitleInfo.LanguageTags, - Title = localEpisode.SubtitleInfo.Title, + Language = localEpisode.SubtitleInfo?.Language ?? Language.Unknown, + LanguageTags = localEpisode.SubtitleInfo?.LanguageTags ?? new List(), + Title = localEpisode.SubtitleInfo?.Title, Extension = extension, - Copy = localEpisode.SubtitleInfo.Copy + Copy = localEpisode.SubtitleInfo?.Copy ?? 0 }; subtitleFiles.Add(subtitleFile); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs index 5beabf7d5..65eaf0cca 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Parser; @@ -30,6 +31,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators return localEpisode; } + if (localEpisode.Episodes.Empty()) + { + return localEpisode; + } + var firstEpisode = localEpisode.Episodes.First(); var episodeFile = firstEpisode.EpisodeFile.Value; localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path); From 653963a2478924fa5ec54ba5de1fc87861062dd7 Mon Sep 17 00:00:00 2001 From: Sonarr Date: Sun, 3 Mar 2024 05:21:40 +0000 Subject: [PATCH 10/98] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e500eba23..54c1bdd7b 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -3070,7 +3070,8 @@ } } } - } + }, + "deprecated": true }, "post": { "tags": [ @@ -3109,6 +3110,59 @@ } } }, + "/api/v3/importlistexclusion/paged": { + "get": { + "tags": [ + "ImportListExclusion" + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "sortKey", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sortDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SortDirection" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResourcePagingResource" + } + } + } + } + } + } + }, "/api/v3/importlistexclusion/{id}": { "put": { "tags": [ @@ -8889,6 +8943,38 @@ }, "additionalProperties": false }, + "ImportListExclusionResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListExclusionResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "ImportListResource": { "type": "object", "properties": { From fa4c11a943bc685a260180f0647f86449c359a8b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 2 Mar 2024 17:10:32 -0800 Subject: [PATCH 11/98] New: Do not automatically unmonitor episodes renamed outside of Sonarr Closes #6584 --- .../HandleEpisodeFileDeletedFixture.cs | 36 +++++- src/NzbDrone.Core/Tv/EpisodeService.cs | 104 +++++++++++++----- 2 files changed, 108 insertions(+), 32 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs index 6f90c9716..fe8e521b3 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Test.Framework; @@ -14,14 +16,20 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests [TestFixture] public class HandleEpisodeFileDeletedFixture : CoreTest { + private Series _series; private EpisodeFile _episodeFile; private List _episodes; [SetUp] public void Setup() { + _series = Builder + .CreateNew() + .Build(); + _episodeFile = Builder .CreateNew() + .With(e => e.SeriesId = _series.Id) .Build(); } @@ -30,6 +38,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests _episodes = Builder .CreateListOfSize(1) .All() + .With(e => e.SeriesId = _series.Id) .With(e => e.Monitored = true) .Build() .ToList(); @@ -44,6 +53,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests _episodes = Builder .CreateListOfSize(2) .All() + .With(e => e.SeriesId = _series.Id) .With(e => e.Monitored = true) .Build() .ToList(); @@ -85,9 +95,31 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests .Returns(true); Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); + Subject.HandleAsync(new SeriesScannedEvent(_series, new List())); + + Mocker.GetMock() + .Verify(v => v.SetMonitored(It.IsAny>(), false), Times.Once()); + } + + [Test] + public void should_leave_monitored_if_autoUnmonitor_is_true_and_missing_episode_is_replaced() + { + GivenSingleEpisodeFile(); + + var newEpisodeFile = _episodeFile.JsonClone(); + newEpisodeFile.Id = 123; + newEpisodeFile.Episodes = new LazyLoaded>(_episodes); + + Mocker.GetMock() + .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedEpisodes) + .Returns(true); + + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); + Subject.Handle(new EpisodeFileAddedEvent(newEpisodeFile)); + Subject.HandleAsync(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() - .Verify(v => v.ClearFileId(It.IsAny(), true), Times.Once()); + .Verify(v => v.SetMonitored(It.IsAny>(), false), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 241037799..20dad0582 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -42,16 +43,19 @@ namespace NzbDrone.Core.Tv public class EpisodeService : IEpisodeService, IHandle, IHandle, - IHandleAsync + IHandleAsync, + IHandleAsync { private readonly IEpisodeRepository _episodeRepository; private readonly IConfigService _configService; + private readonly ICached> _cache; private readonly Logger _logger; - public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger) + public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, ICacheManager cacheManager, Logger logger) { _episodeRepository = episodeRepository; _configService = configService; + _cache = cacheManager.GetCache>(GetType()); _logger = logger; } @@ -215,34 +219,6 @@ namespace NzbDrone.Core.Tv _episodeRepository.DeleteMany(episodes); } - public void HandleAsync(SeriesDeletedEvent message) - { - var episodes = _episodeRepository.GetEpisodesBySeriesIds(message.Series.Select(s => s.Id).ToList()); - _episodeRepository.DeleteMany(episodes); - } - - public void Handle(EpisodeFileDeletedEvent message) - { - foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) - { - _logger.Debug("Detaching episode {0} from file.", episode.Id); - - var unmonitorForReason = message.Reason != DeleteMediaFileReason.Upgrade && - message.Reason != DeleteMediaFileReason.ManualOverride; - - _episodeRepository.ClearFileId(episode, unmonitorForReason && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes); - } - } - - public void Handle(EpisodeFileAddedEvent message) - { - foreach (var episode in message.EpisodeFile.Episodes.Value) - { - _episodeRepository.SetFileId(episode, message.EpisodeFile.Id); - _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); - } - } - private Episode FindOneByAirDate(int seriesId, string date, int? part) { var episodes = _episodeRepository.Find(seriesId, date); @@ -277,5 +253,73 @@ namespace NzbDrone.Core.Tv throw new InvalidOperationException($"Multiple episodes with the same air date found. Date: {date}"); } + + public void Handle(EpisodeFileDeletedEvent message) + { + foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) + { + _logger.Debug("Detaching episode {0} from file.", episode.Id); + + var unmonitorEpisodes = _configService.AutoUnmonitorPreviouslyDownloadedEpisodes; + + var unmonitorForReason = message.Reason != DeleteMediaFileReason.Upgrade && + message.Reason != DeleteMediaFileReason.ManualOverride && + message.Reason != DeleteMediaFileReason.MissingFromDisk; + + // If episode is being unlinked because it's missing from disk store it for + if (message.Reason == DeleteMediaFileReason.MissingFromDisk && unmonitorEpisodes) + { + lock (_cache) + { + var ids = _cache.Get(episode.SeriesId.ToString(), () => new HashSet()); + + ids.Add(episode.Id); + } + } + + _episodeRepository.ClearFileId(episode, unmonitorForReason && unmonitorEpisodes); + } + } + + public void Handle(EpisodeFileAddedEvent message) + { + foreach (var episode in message.EpisodeFile.Episodes.Value) + { + _episodeRepository.SetFileId(episode, message.EpisodeFile.Id); + + lock (_cache) + { + var ids = _cache.Find(episode.SeriesId.ToString()); + + if (ids?.Contains(episode.Id) == true) + { + ids.Remove(episode.Id); + } + } + + _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); + } + } + + public void HandleAsync(SeriesDeletedEvent message) + { + var episodes = _episodeRepository.GetEpisodesBySeriesIds(message.Series.Select(s => s.Id).ToList()); + _episodeRepository.DeleteMany(episodes); + } + + public void HandleAsync(SeriesScannedEvent message) + { + lock (_cache) + { + var ids = _cache.Find(message.Series.Id.ToString()); + + if (ids?.Any() == true) + { + _episodeRepository.SetMonitored(ids, false); + } + + _cache.Remove(message.Series.Id.ToString()); + } + } } } From 7f09903a06d0d5657d20fba9eb5e286d455140be Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 23 Feb 2024 04:51:35 +0200 Subject: [PATCH 12/98] New: Episode Requested filter for Interactive Search --- frontend/src/Store/Actions/releaseActions.js | 6 ++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 7 insertions(+) diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index b14bc19e4..6d7495321 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -250,6 +250,12 @@ export const defaultState = { label: () => translate('SeasonPack'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'episodeRequested', + label: () => translate('EpisodeRequested'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL } ], diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ede7a0ca3..15674ddc7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -632,6 +632,7 @@ "EpisodeNaming": "Episode Naming", "EpisodeNumbers": "Episode Number(s)", "EpisodeProgress": "Episode Progress", + "EpisodeRequested": "Episode Requested", "EpisodeSearchResultsLoadError": "Unable to load results for this episode search. Try again later", "EpisodeTitle": "Episode Title", "EpisodeTitleRequired": "Episode Title Required", From 0183812cc58dad0e555125ddd8b33a85cbdecbf2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 3 Mar 2024 09:34:02 -0800 Subject: [PATCH 13/98] Fixed: Overly aggressive exception release group parsing Closes #6591 --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index b2d13a262..b22d7c43a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Erai-raws] Series - 0955 ~ 1005 [1080p]", "Erai-raws")] [TestCase("[Exiled-Destiny] Series Title", "Exiled-Destiny")] [TestCase("Series.Title.S01E09.1080p.DSNP.WEB-DL.DDP2.0.H.264-VARYG", "VARYG")] + [TestCase("Stargate SG-1 (1997) - S01E01-02 - Children of the Gods (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")] // [TestCase("", "")] public void should_parse_release_group(string title, string expected) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 421ca70cf..693eaa36d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -533,7 +533,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); // groups whose releases end with RlsGroup) or RlsGroup] - private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); From 2068c5393e130d51d7bf2ae948b1fcb254c7106f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:44:47 +0200 Subject: [PATCH 14/98] Fixed: URL Base setting for Kodi connections --- src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs | 2 +- src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs index 028a64a18..7f5ddb780 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Notifications.Xbmc private string ProcessRequest(XbmcSettings settings, string method, params object[] parameters) { - var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc"); + var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); var requestBuilder = new JsonRpcRequestBuilder(url, method, parameters); requestBuilder.LogResponseContent = true; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs index 2d54157f2..97331f333 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs @@ -14,16 +14,18 @@ namespace NzbDrone.Core.Notifications.Xbmc { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.DisplayTime).GreaterThanOrEqualTo(2); + RuleFor(c => c.UrlBase).ValidUrlBase(); } } public class XbmcSettings : IProviderConfig { - private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator(); + private static readonly XbmcSettingsValidator Validator = new (); public XbmcSettings() { Port = 8080; + UrlBase = "/jsonrpc"; DisplayTime = 5; } @@ -65,7 +67,7 @@ namespace NzbDrone.Core.Notifications.Xbmc public bool AlwaysUpdate { get; set; } [JsonIgnore] - public string Address => $"{Host.ToUrlHost()}:{Port}"; + public string Address => $"{Host.ToUrlHost()}:{Port}{UrlBase}"; public NzbDroneValidationResult Validate() { From f211433b778a4ccd5d575b721ba0528e040c5770 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:45:17 +0200 Subject: [PATCH 15/98] Remove debugger from metadata source and rearrange some imports --- frontend/src/Settings/MetadataSource/TheTvdb.js | 1 - frontend/src/Store/Actions/Settings/importListExclusions.js | 4 ++-- frontend/src/Store/Actions/settingsActions.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/Settings/MetadataSource/TheTvdb.js b/frontend/src/Settings/MetadataSource/TheTvdb.js index b1abb0c99..79a1e6e40 100644 --- a/frontend/src/Settings/MetadataSource/TheTvdb.js +++ b/frontend/src/Settings/MetadataSource/TheTvdb.js @@ -4,7 +4,6 @@ import translate from 'Utilities/String/translate'; import styles from './TheTvdb.css'; function TheTvdb(props) { - debugger; return ( <div className={styles.container}> <img diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index 5bf7d37a2..d6371946f 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -1,11 +1,11 @@ import { createAction } from 'redux-actions'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; +import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import { createThunk, handleThunks } from 'Store/thunks'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; -import createServerSideCollectionHandlers from '../Creators/createServerSideCollectionHandlers'; -import createSetTableOptionReducer from '../Creators/Reducers/createSetTableOptionReducer'; // // Variables diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index e7b5e40f6..a030c028a 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,5 +1,4 @@ import { createAction } from 'redux-actions'; -import indexerFlags from 'Store/Actions/Settings/indexerFlags'; import { handleThunks } from 'Store/thunks'; import createHandleActions from './Creators/createHandleActions'; import autoTaggings from './Settings/autoTaggings'; @@ -13,6 +12,7 @@ import general from './Settings/general'; import importListExclusions from './Settings/importListExclusions'; import importListOptions from './Settings/importListOptions'; import importLists from './Settings/importLists'; +import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; From e81bb3b993adac705fd61dc9e281b040ca2338f5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:37:24 +0200 Subject: [PATCH 16/98] Persist page size for Import List Exclusions --- frontend/src/Store/Actions/settingsActions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index a030c028a..440f20000 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -91,7 +91,8 @@ export const defaultState = { }; export const persistState = [ - 'settings.advancedSettings' + 'settings.advancedSettings', + 'settings.importListExclusions.pageSize' ]; // From d0e9504af0d88391a74e04b90638e4b2d99fb476 Mon Sep 17 00:00:00 2001 From: CheAle14 <12370876+CheAle14@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:37:20 +0000 Subject: [PATCH 17/98] Fix import list exclusion props --- frontend/src/App/State/AppSectionState.ts | 1 + .../ImportListExclusions/ImportListExclusions.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 5bc7dfbac..30af90d34 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -19,6 +19,7 @@ export interface AppSectionSaveState { export interface PagedAppSectionState { pageSize: number; + totalRecords?: number; } export interface AppSectionFilterState<T> { diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index 7a15bca91..8c7033686 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import FieldSet from 'Components/FieldSet'; @@ -41,11 +42,6 @@ const COLUMNS = [ }, ]; -interface ImportListExclusionsProps { - useCurrentPage: number; - totalRecords: number; -} - function createImportListExlucionsSelector() { return createSelector( (state: AppState) => state.settings.importListExclusions, @@ -57,8 +53,9 @@ function createImportListExlucionsSelector() { ); } -function ImportListExclusions(props: ImportListExclusionsProps) { - const { useCurrentPage, totalRecords } = props; +function ImportListExclusions() { + const history = useHistory(); + const useCurrentPage = history.action === 'POP'; const dispatch = useDispatch(); @@ -155,6 +152,7 @@ function ImportListExclusions(props: ImportListExclusionsProps) { sortKey, error, sortDirection, + totalRecords, ...otherProps } = selected; From c7dd7abf892eead7796fcc482aa2f2aabaf88712 Mon Sep 17 00:00:00 2001 From: Helvio Pedreschi <helvio88@gmail.com> Date: Thu, 7 Mar 2024 20:29:50 -0500 Subject: [PATCH 18/98] Fixed: WebApp functionality on Apple devices --- frontend/src/index.ejs | 7 +++++-- frontend/src/login.html | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 97a0104ee..3f5ec6f2a 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,13 +3,16 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-capable" content="yes" /> <!-- Chrome, Opera, and Firefox OS --> <meta name="theme-color" content="#3a3f51" /> <!-- Windows Phone --> <meta name="msapplication-navbutton-color" content="#3a3f51" /> + <!-- Android/Apple Phone --> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> + <meta name="format-detection" content="telephone=no"> <meta name="description" content="Sonarr" /> diff --git a/frontend/src/login.html b/frontend/src/login.html index 4c16da6be..e89099276 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,13 +3,16 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-capable" content="yes" /> <!-- Chrome, Opera, and Firefox OS --> <meta name="theme-color" content="#3a3f51" /> <!-- Windows Phone --> <meta name="msapplication-navbutton-color" content="#3a3f51" /> + <!-- Android/Apple Phone --> + <meta name="mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-capable" content="yes" /> + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> + <meta name="format-detection" content="telephone=no"> <meta name="description" content="Sonarr" /> From 18aadb544e2567b26eecbf8dec70f7bd7c47f3e9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Fri, 8 Mar 2024 03:30:20 +0200 Subject: [PATCH 19/98] Fixed: Maintain release type for items in Manual Import --- frontend/src/InteractiveImport/InteractiveImport.ts | 2 ++ frontend/src/InteractiveImport/ReleaseType.ts | 3 +++ .../src/Store/Actions/interactiveImportActions.js | 1 + .../EpisodeImport/Manual/ManualImportService.cs | 11 +++++++---- .../ManualImport/ManualImportController.cs | 2 +- .../ManualImport/ManualImportReprocessResource.cs | 2 ++ 6 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 frontend/src/InteractiveImport/ReleaseType.ts diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 9ec91a4aa..1feea60c0 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -1,5 +1,6 @@ import ModelBase from 'App/ModelBase'; import Episode from 'Episode/Episode'; +import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import Series from 'Series/Series'; @@ -33,6 +34,7 @@ interface InteractiveImport extends ModelBase { qualityWeight: number; customFormats: object[]; indexerFlags: number; + releaseType: ReleaseType; rejections: Rejection[]; episodeFileId?: number; } diff --git a/frontend/src/InteractiveImport/ReleaseType.ts b/frontend/src/InteractiveImport/ReleaseType.ts new file mode 100644 index 000000000..7bfa8550d --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType.ts @@ -0,0 +1,3 @@ +type ReleaseType = 'unknown' | 'singleEpisode' | 'multiEpisode' | 'seasonPack'; + +export default ReleaseType; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index ce6da8a21..ed05ed548 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -163,6 +163,7 @@ export const actionHandlers = handleThunks({ languages: item.languages, releaseGroup: item.releaseGroup, indexerFlags: item.indexerFlags, + releaseType: item.releaseType, downloadId: item.downloadId }; }); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 58af3323d..f1fcd03cf 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { List<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags, ReleaseType releaseType); } public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService @@ -139,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles); } - public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags) + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags, ReleaseType releaseType) { var rootFolder = Path.GetDirectoryName(path); var series = _seriesService.GetSeries(seriesId); @@ -169,9 +169,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup; localEpisode.Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages; localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; + localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; + localEpisode.ReleaseType = releaseType; + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; - localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null); } @@ -199,7 +201,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality, - IndexerFlags = (IndexerFlags)indexerFlags + IndexerFlags = (IndexerFlags)indexerFlags, + ReleaseType = releaseType }; return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index f537f2e2f..eb6787c5b 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -39,7 +39,7 @@ namespace Sonarr.Api.V3.ManualImport { foreach (var item in items) { - var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags); + var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType); item.SeasonNumber = processedItem.SeasonNumber; item.Episodes = processedItem.Episodes.ToResource(); diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 66bb78ba9..4eb2bbe4b 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.Episodes; @@ -22,6 +23,7 @@ namespace Sonarr.Api.V3.ManualImport public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public IEnumerable<Rejection> Rejections { get; set; } } } From 2c252458609dd8cc2a7ee193e6f043cdb5356f80 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 8 Mar 2024 01:27:05 +0000 Subject: [PATCH 20/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: Mark Martines <mark-martines@hotmail.com> Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com> Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: linkin931 <931linkin@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/el/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ko/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 8 +- src/NzbDrone.Core/Localization/Core/cs.json | 6 +- src/NzbDrone.Core/Localization/Core/de.json | 21 +- src/NzbDrone.Core/Localization/Core/el.json | 4 +- src/NzbDrone.Core/Localization/Core/es.json | 726 +++++++++++++++++- src/NzbDrone.Core/Localization/Core/fi.json | 37 +- src/NzbDrone.Core/Localization/Core/fr.json | 472 ++++++++---- src/NzbDrone.Core/Localization/Core/hu.json | 17 +- src/NzbDrone.Core/Localization/Core/it.json | 2 +- src/NzbDrone.Core/Localization/Core/ko.json | 2 +- src/NzbDrone.Core/Localization/Core/nl.json | 41 +- src/NzbDrone.Core/Localization/Core/pt.json | 8 +- .../Localization/Core/pt_BR.json | 15 +- src/NzbDrone.Core/Localization/Core/ro.json | 2 +- src/NzbDrone.Core/Localization/Core/ru.json | 10 +- src/NzbDrone.Core/Localization/Core/tr.json | 4 +- .../Localization/Core/zh_CN.json | 18 +- 17 files changed, 1133 insertions(+), 260 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 0bba6b2a8..08c1aba65 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -164,7 +164,7 @@ "AirsTbaOn": "Pendent el seu anunci a {networkLabel}", "AllFiles": "Tots els fitxers", "AllSeriesAreHiddenByTheAppliedFilter": "Tots els resultats estan ocults pel filtre aplicat", - "AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {0}", + "AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {path}", "AlreadyInYourLibrary": "Ja a la vostra biblioteca", "AlternateTitles": "Títols alternatius", "AnalyseVideoFilesHelpText": "Extraieu informació de vídeo com ara la resolució, el temps d'execució i la informació del còdec dels fitxers. Això requereix que {appName} llegeixi parts del fitxer que poden provocar una activitat elevada al disc o a la xarxa durant les exploracions.", @@ -183,7 +183,7 @@ "DeleteRootFolder": "Suprimeix la carpeta arrel", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es pot comunicar amb {downloadClientName}. {errorMessage}", "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de baixada no disponibles a causa d'errors: {downloadClientNames}", - "DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {path}. No s'hauria de baixar a una carpeta arrel.", + "DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {rootFolderPath}. No s'hauria de baixar a una carpeta arrel.", "DownloadClientSortingHealthCheckMessage": "El client de baixada {downloadClientName} té l'ordenació {sortingMode} activada per a la categoria de {appName}. Hauríeu de desactivar l'ordenació al vostre client de descàrrega per a evitar problemes d'importació.", "HiddenClickToShow": "Amagat, feu clic per a mostrar", "ImportUsingScript": "Importa amb script", @@ -213,7 +213,7 @@ "FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions", "False": "Fals", "Implementation": "Implementació", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFolderInfo}", "ImportListRootFolderMissingRootHealthCheckMessage": "Falta la carpeta arrel per a les llistes d'importació: {rootFolderInfo}", "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hi ha indexadors disponibles amb la sincronització RSS activada, {appName} no capturarà els nous llançaments automàticament", "ImportListStatusAllUnavailableHealthCheckMessage": "Totes les llistes no estan disponibles a causa d'errors", @@ -301,7 +301,7 @@ "Clone": "Clona", "CloneProfile": "Clona el perfil", "CompletedDownloadHandling": "Gestió de descàrregues completades", - "CountSeriesSelected": "{selectedCount} sèries seleccionades", + "CountSeriesSelected": "{count} sèries seleccionades", "InteractiveImportLoadError": "No es poden carregar els elements d'importació manual", "ChownGroupHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega utilitza el mateix grup que {appName}.", "ChmodFolderHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega estableixi correctament els permisos.", diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 352a2cfd9..0f168f6d7 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -30,7 +30,7 @@ "CancelProcessing": "Zrušit zpracování", "CheckDownloadClientForDetails": "zkontrolujte klienta pro stahování pro více informací", "ChmodFolderHelpText": "Octal, aplikováno během importu / přejmenování na mediální složky a soubory (bez provádění bitů)", - "ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", + "ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", "ChooseAnotherFolder": "Vyberte jinou složku", "ChownGroup": "Skupina chown", "ConnectSettings": "Nastavení připojení", @@ -67,7 +67,7 @@ "CalendarLoadError": "Nelze načíst kalendář", "CertificateValidationHelpText": "Změňte přísnost ověřování certifikátů HTTPS. Neměňte, pokud nerozumíte rizikům.", "ChownGroupHelpText": "Název skupiny nebo gid. Použijte gid pro vzdálené systémy souborů.", - "ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", + "ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient stahování používal stejnou skupinu jako {appName}.", "ClientPriority": "Priorita klienta", "Clone": "Klonovat", "CloneIndexer": "Klonovat indexátor", @@ -319,5 +319,5 @@ "EditSelectedImportLists": "Upravit vybrané seznamy k importu", "FormatDateTime": "{formattedDate} {formattedTime}", "AddRootFolderError": "Nepodařilo se přidat kořenový adresář", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {1}." + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}." } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index fd571509c..0ee4eb808 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -8,7 +8,7 @@ "AutomaticAdd": "Automatisch hinzufügen", "CountSeasons": "{count} Staffeln", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Es ist kein Download-Client verfügbar", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {downloadClientName} legt Downloads im Stammordner {rootFolderPath} ab. Sie sollten nicht in einen Stammordner herunterladen.", "DownloadClientSortingHealthCheckMessage": "Im Download-Client {downloadClientName} ist die Sortierung {sortingMode} für die Kategorie von {appName} aktiviert. Sie sollten die Sortierung in Ihrem Download-Client deaktivieren, um Importprobleme zu vermeiden.", "DownloadClientStatusSingleClientHealthCheckMessage": "Download-Clients sind aufgrund von Fehlern nicht verfügbar: {downloadClientNames}", @@ -86,7 +86,7 @@ "QuickSearch": "Schnelle Suche", "ReadTheWikiForMoreInformation": "Lesen Sie das Wiki für weitere Informationen", "Real": "Real", - "RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {Pfad}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.", + "RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {path}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.", "RecyclingBin": "Papierkorb", "RecyclingBinCleanup": "Papierkorb leeren", "RefreshSeries": "Serie aktualisieren", @@ -202,7 +202,7 @@ "AuthBasic": "Basis (Browser-Popup)", "AuthForm": "Formulare (Anmeldeseite)", "Authentication": "Authentifizierung", - "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich.", + "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", "AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss zutreffen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht eine einzelne {implementationName}-Übereinstimmung aus.", @@ -288,15 +288,15 @@ "Day": "Tag", "Default": "Standard", "DefaultCase": "Standardfall", - "DefaultNameCopiedProfile": "{Name} – Kopieren", - "DefaultNameCopiedSpecification": "{Name} – Kopieren", + "DefaultNameCopiedProfile": "{name} – Kopieren", + "DefaultNameCopiedSpecification": "{name} – Kopieren", "DefaultNotFoundMessage": "Sie müssen verloren sein, hier gibt es nichts zu sehen.", "DelayMinutes": "{delay} Minuten", "DelayProfile": "Verzögerungsprofil", "DelayProfileProtocol": "Protokoll: {preferredProtocol}", "DelayProfiles": "Verzögerungsprofile", "DelayProfilesLoadError": "Verzögerungsprofile können nicht geladen werden", - "DelayingDownloadUntil": "Download wird bis zum {Datum} um {Uhrzeit} verzögert", + "DelayingDownloadUntil": "Download wird bis zum {date} um {time} verzögert", "DeleteAutoTag": "Auto-Tag löschen", "DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?", "DeleteBackup": "Sicherung löschen", @@ -350,7 +350,7 @@ "DownloadClientDownloadStationValidationFolderMissing": "Ordner existiert nicht", "DownloadClientDownloadStationValidationFolderMissingDetail": "Der Ordner „{downloadDir}“ existiert nicht, er muss manuell im freigegebenen Ordner „{sharedFolder}“ erstellt werden.", "DownloadClientDownloadStationValidationNoDefaultDestination": "Kein Standardziel", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {Benutzername} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {username} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.", "DownloadClientDownloadStationValidationSharedFolderMissing": "Der freigegebene Ordner existiert nicht", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Die Diskstation verfügt nicht über einen freigegebenen Ordner mit dem Namen „{sharedFolder}“. Sind Sie sicher, dass Sie ihn richtig angegeben haben?", "DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags", @@ -372,7 +372,7 @@ "DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „app_token“)", "DownloadClientFreeboxSettingsHostHelpText": "Hostname oder Host-IP-Adresse der Freebox, standardmäßig „{url}“ (funktioniert nur im selben Netzwerk)", "DownloadClientFreeboxSettingsPortHelpText": "Port, der für den Zugriff auf die Freebox-Schnittstelle verwendet wird, standardmäßig ist „{port}“", - "DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {ExceptionMessage})", + "DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {exceptionMessage})", "DownloadClientFreeboxUnableToReachFreeboxApi": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellung „API-URL“ für Basis-URL und Version.", "DownloadClientNzbVortexMultipleFilesMessage": "Der Download enthält mehrere Dateien und befindet sich nicht in einem Jobordner: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0", @@ -446,7 +446,7 @@ "Restore": "Wiederherstellen", "RestartRequiredWindowsService": "Je nachdem, welcher Benutzer den {appName}-Dienst ausführt, müssen Sie {appName} möglicherweise einmal als Administrator neu starten, bevor der Dienst automatisch gestartet wird.", "RestartSonarr": "{appName} neu starten", - "RetryingDownloadOn": "Erneuter Downloadversuch am {Datum} um {Uhrzeit}", + "RetryingDownloadOn": "Erneuter Downloadversuch am {date} um {time}", "SceneInfo": "Szeneninfo", "Scene": "Szene", "SaveSettings": "Einstellungen speichern", @@ -525,7 +525,7 @@ "UsenetDelayTime": "Usenet-Verzögerung: {usenetDelay}", "UsenetDelayHelpText": "Verzögerung in Minuten, bevor Sie eine Veröffentlichung aus dem Usenet erhalten", "VideoCodec": "Video-Codec", - "VersionNumber": "Version {Version}", + "VersionNumber": "Version {version}", "Version": "Version", "WantMoreControlAddACustomFormat": "Möchten Sie mehr Kontrolle darüber haben, welche Downloads bevorzugt werden? Fügen Sie ein [benutzerdefiniertes Format] hinzu (/settings/customformats)", "WaitingToProcess": "Warten auf Bearbeitung", @@ -549,7 +549,6 @@ "CountImportListsSelected": "{count} Importliste(n) ausgewählt", "CountIndexersSelected": "{count} Indexer ausgewählt", "CountSelectedFiles": "{selectedCount} ausgewählte Dateien", - "CustomFormatUnknownCondition": "Unknown Custom Format condition '{implementation}'", "CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“", "CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen", "Daily": "Täglich", diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 4be715996..46a7e5880 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -15,8 +15,8 @@ "RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {selectedCount} αντικείμενα από την ουρά;", "CloneCondition": "Κλωνοποίηση συνθήκης", "RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;", - "AddConditionImplementation": "Προσθήκη", + "AddConditionImplementation": "Προσθήκη - {implementationName}", "AppUpdated": "{appName} Ενημερώθηκε", "AutoAdd": "Προσθήκη", - "AddConnectionImplementation": "Προσθήκη" + "AddConnectionImplementation": "Προσθήκη - {implementationName}" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index fe68fe3e1..0093deca1 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -167,8 +167,8 @@ "AllResultsAreHiddenByTheAppliedFilter": "Todos los resultados están ocultos por el filtro aplicado", "AnalyseVideoFilesHelpText": "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.", "AnimeEpisodeTypeDescription": "Episodios lanzados usando un número de episodio absoluto", - "ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración", - "AppDataLocationHealthCheckMessage": "No será posible actualizar para prevenir la eliminación de AppData al Actualizar", + "ApiKeyValidationHealthCheckMessage": "Por favor actualiza tu clave API para que tenga de longitud al menos {length} caracteres. Puedes hacerlo en los ajustes o en el archivo de configuración", + "AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar", "Scheduled": "Programado", "Season": "Temporada", "Clone": "Clonar", @@ -308,7 +308,7 @@ "CountSeasons": "{count} Temporadas", "BranchUpdate": "Rama a usar para actualizar {appName}", "ChmodFolder": "Carpeta chmod", - "CheckDownloadClientForDetails": "Revisar cliente de descarpa para mas detalles", + "CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles", "ChooseAnotherFolder": "Elige otra Carpeta", "ClientPriority": "Prioridad del Cliente", "CloneIndexer": "Clonar Indexer", @@ -325,10 +325,10 @@ "ConnectSettings": "Conectar Ajustes", "CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'", "XmlRpcPath": "Ruta XML RPC", - "AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no aplicará si la condición {implementationName} coincide.", + "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", - "AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {0} será suficiente.", + "AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {implementationName} será suficiente.", "WeekColumnHeaderHelpText": "Mostrado sobre cada columna cuando la vista activa es semana", "WhyCantIFindMyShow": "Por que no puedo encontrar mi serie?", "WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?", @@ -357,7 +357,7 @@ "ChangeFileDate": "Cambiar fecha de archivo", "CertificateValidationHelpText": "Cambiar como es la validacion de la certificacion estricta de HTTPS. No cambiar a menos que entiendas las consecuencias.", "AddListExclusion": "Agregar Lista de Exclusión", - "AddedDate": "Agregado: {fecha}", + "AddedDate": "Agregado: {date}", "AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado", "AlternateTitles": "Titulos alternativos", "ChmodFolderHelpText": "Octal, aplicado durante la importación / cambio de nombre a carpetas y archivos multimedia (sin bits de ejecución)", @@ -369,7 +369,7 @@ "AirsTbaOn": "A anunciar en {networkLabel}", "AllFiles": "Todos los archivos", "Any": "Cualquiera", - "AirsTomorrowOn": "Mañana a las {hora} en {networkLabel}", + "AirsTomorrowOn": "Mañana a las {time} en {networkLabel}", "AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes, necesitará recargar {appName} ", "AddListExclusionSeriesHelpText": "Evitar que las series sean agregadas a {appName} por las listas", "CalendarLegendEpisodeDownloadedTooltip": "El episodio fue descargado y ordenado", @@ -616,7 +616,7 @@ "DownloadClientRTorrentSettingsUrlPath": "Ruta de la 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.", + "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}", @@ -825,7 +825,7 @@ "Existing": "Existentes", "ExportCustomFormat": "Exportar formato personalizado", "EpisodeFilesLoadError": "No se puede cargar los archivos de episodios", - "EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadCliente}", + "EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadClient}", "EpisodeInfo": "Información del episodio", "EpisodeMissingAbsoluteNumber": "El episodio no tiene un número de episodio absoluto", "EpisodeTitleRequired": "Título del episodio requerido", @@ -873,7 +873,7 @@ "FilterNotInLast": "no en el último", "Group": "Grupo", "ImportListSearchForMissingEpisodes": "Buscar episodios faltantes", - "EnableProfileHelpText": "Señalar para habilitar el perfil de lanzamiento", + "EnableProfileHelpText": "Marcar para habilitar el perfil de lanzamiento", "EnableRssHelpText": "Se usará cuando {appName} busque periódicamente lanzamientos vía Sincronización RSS", "EndedSeriesDescription": "No se esperan episodios o temporadas adicionales", "EpisodeFileDeleted": "Archivo de episodio eliminado", @@ -1059,7 +1059,7 @@ "ICalTagsSeriesHelpText": "El feed solo contendrá series con al menos una etiqueta coincidente", "IconForCutoffUnmet": "Icono para Umbrales no alcanzados", "IconForCutoffUnmetHelpText": "Mostrar icono para archivos cuando el umbral no haya sido alcanzado", - "EpisodeCount": "Número de episodios", + "EpisodeCount": "Recuento de episodios", "IndexerSettings": "Ajustes de Indexador", "AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, inténtelo de nuevo.", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexers capaces de RSS están temporalmente desactivados debido a errores recientes con el indexer", @@ -1177,7 +1177,7 @@ "LogFiles": "Archivos de Registro", "LogLevel": "Nivel de Registro", "LogLevelTraceHelpTextWarning": "El registro de seguimiento sólo debe activarse temporalmente", - "LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. ej. 'episodio.s02e15.bluray.mkv'.", + "LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. P. ej. `episodio.s02e15.bluray.mkv`", "ListSyncLevelHelpText": "Las series de la biblioteca se gestionarán en función de su selección si se caen o no aparecen en su(s) lista(s)", "LogOnly": "Sólo Registro", "LongDateFormat": "Formato de Fecha Larga", @@ -1335,7 +1335,7 @@ "Monitor": "Monitorizar", "MonitorAllEpisodes": "Todos los episodios", "MonitorAllSeasons": "Todas las temporadas", - "NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeSet}, asegúrate de que tu script maneja esto correctamente", + "NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeTest}, asegúrate de que tu script maneja esto correctamente", "NotificationsDiscordSettingsAvatar": "Avatar", "NotificationsDiscordSettingsAvatarHelpText": "Cambia el avatar que es usado para mensajes desde esta integración", "NotificationsAppriseSettingsNotificationType": "Tipo de notificación de Apprise", @@ -1351,7 +1351,7 @@ "NotificationsDiscordSettingsAuthorHelpText": "Sobrescribe el autor incrustado que se muestra para esta notificación. En blanco es el nombre de la instancia", "MonitorNewItems": "Monitorizar nuevos elementos", "MonitoredEpisodesHelpText": "Descargar episodios monitorizados en estas series", - "NegateHelpText": "Si se elige, el formato personalizado no se aplica si coincide la condición {implementationName}.", + "NegateHelpText": "Si se marca, el formato personalizado no se aplica si coincide la condición {implementationName}.", "NotificationsCustomScriptSettingsName": "Script personalizado", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar la monitorización de temporada", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincroniza la monitorización de temporada de la instancia de {appName}, si se habilita 'Monitorizar' será ignorado", @@ -1361,5 +1361,701 @@ "MoreDetails": "Más detalles", "MoreInfo": "Más información", "NoEpisodesInThisSeason": "No hay episodios en esta temporada", - "NoLinks": "No hay enlaces" + "NoLinks": "No hay enlaces", + "OrganizeSelectedSeriesModalAlert": "Consejo: Para previsualizar un renombrado, selecciona \"Cancelar\", entonces selecciona cualquier título de serie y usa este icono:", + "OrganizeSelectedSeriesModalConfirmation": "¿Estás seguro que quieres organizar todos los archivos en las {count} series seleccionadas?", + "Password": "Contraseña", + "Permissions": "Permisos", + "Port": "Puerto", + "RecyclingBinCleanup": "Limpieza de la papelera de reciclaje", + "ReleaseSceneIndicatorSourceMessage": "Los lanzamientos {message} existen con numeración ambigua, no se pudo identificar de forma fiable el episodio.", + "SeriesTitle": "Título de serie", + "ShowEpisodes": "Mostrar episodios", + "ShowBanners": "Mostrar banners", + "ShowSeriesTitleHelpText": "Muestra el título de serie bajo el póster", + "SkipFreeSpaceCheck": "Saltar comprobación de espacio libre", + "OneSeason": "1 temporada", + "OnlyTorrent": "Solo torrent", + "OpenBrowserOnStart": "Abrir navegador al inicio", + "OnlyUsenet": "Solo Usenet", + "OverrideAndAddToDownloadQueue": "Sobrescribe y añade a la cola de descarga", + "Table": "Tabla", + "TagsLoadError": "No se pudo cargar Etiquetas", + "OverviewOptions": "Opciones de vista general", + "Umask775Description": "{octal} - Usuario y grupo escriben, Otros leen", + "PendingChangesStayReview": "Quedarse y revisar cambios", + "PendingDownloadClientUnavailable": "Pendiente - El cliente de descarga no está disponible", + "PostImportCategory": "Categoría de post-importación", + "PreferUsenet": "Preferir usenet", + "PreviousAiringDate": "Emisiones anteriores: {date}", + "Profiles": "Perfiles", + "PrioritySettings": "Prioridad: {priority}", + "Ok": "Ok", + "PrefixedRange": "Rango prefijado", + "Qualities": "Calidades", + "PublishedDate": "Fecha de publicación", + "QualitySettings": "Opciones de calidad", + "QualitySettingsSummary": "Tamaños de calidad y nombrado", + "RecentChanges": "Cambios recientes", + "MountSeriesHealthCheckMessage": "El montaje que contiene una ruta de series se monta en solo lectura: ", + "NotificationsEmailSettingsBccAddress": "Dirección(es) BCC", + "NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc", + "NotificationsEmailSettingsName": "E-mail", + "NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario", + "NotificationsEmbySettingsSendNotificationsHelpText": "Hacer que MediaBrowser envíe notificaciones a los proveedores configurados", + "NotificationsGotifySettingsAppToken": "Token de app", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje", + "NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo", + "NotificationsJoinSettingsDeviceNamesHelpText": "Lista separada por coma de nombres de dispositivo completos o parciales a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.", + "NotificationsJoinSettingsNotificationPriority": "Prioridad de notificación", + "NotificationsNtfySettingsClickUrlHelpText": "Enlace opcional cuando el usuario hace clic en la notificación", + "NotificationsNtfySettingsPasswordHelpText": "Contraseña opcional", + "NotificationsNtfySettingsTagsEmojis": "Etiquetas y emojis de Ntfy", + "NotificationsNtfySettingsServerUrlHelpText": "Deja en blanco para usar el servidor público ({url})", + "NotificationsNtfySettingsTopicsHelpText": "Lista de temas a la que enviar notificaciones", + "NotificationsPushBulletSettingSenderIdHelpText": "La ID del dispositivo desde la que enviar notificaciones, usa device_iden en la URL del dispositivo en pushbullet.com (deja en blanco para enviarla por ti mismo)", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Lista de etiquetas de canal a las que enviar notificaciones", + "NotificationsSettingsUpdateMapPathsFromHelpText": "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')", + "NotificationsSettingsUpdateMapPathsFrom": "Mapear rutas desde", + "NotificationsTagsSeriesHelpText": "Envía notificaciones solo para series con al menos una etiqueta coincidente", + "NotificationsTraktSettingsRefreshToken": "Refrescar token", + "OnEpisodeFileDelete": "Al borrar un archivo de episodio", + "OnGrab": "Al capturar", + "OneMinute": "1 minuto", + "Or": "o", + "OrganizeSelectedSeriesModalHeader": "Organizar series seleccionadas", + "Original": "Original", + "OriginalLanguage": "Idioma original", + "OverrideGrabNoLanguage": "Al menos un idioma debe ser seleccionado", + "ParseModalHelpTextDetails": "{appName} intentará analizar el título y te mostrará detalles sobre ello", + "Parse": "Analizar", + "Path": "Ruta", + "PortNumber": "Número de puerto", + "PosterSize": "Tamaño de póster", + "Posters": "Pósteres", + "PreviewRename": "Previsualizar renombrado", + "PreferredProtocol": "Protocolo preferido", + "ProcessingFolders": "Procesando carpetas", + "Proper": "Proper", + "ProxyFailedToTestHealthCheckMessage": "Fallo al probar el proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Fallo al probar el proxy. Código de estado: {statusCode}", + "ProxyType": "Tipo de proxy", + "QualityLimitsSeriesRuntimeHelpText": "Los límites son automáticamente ajustados para las series en tiempo de ejecución y el número de episodios en el archivo.", + "Range": "Rango", + "RecycleBinUnableToWriteHealthCheckMessage": "No se pudo escribir en la carpeta configurada de la papelera de reciclaje: {path}. Asegúrate de que la ruta existe y es modificable por el usuario que ejecuta {appName}", + "RecyclingBinHelpText": "Los archivos irán aquí cuando se borren en lugar de ser borrados permanentemente", + "RelativePath": "Ruta relativa", + "RegularExpressionsCanBeTested": "Las expresiones regulares pueden ser probadas [aquí]({url}).", + "ReleaseGroup": "Grupo de lanzamiento", + "ReleaseGroups": "Grupos de lanzamiento", + "ReleaseProfilesLoadError": "No se pudo cargar los perfiles de lanzamiento", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} falló al importar (un) episodio(s). Comprueba tus registros para más detalles.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero este directorio no parece existir. Posiblemente mapeo de ruta remota perdido.", + "RemoveQueueItem": "Eliminar - {sourceTitle}", + "RemoveFailed": "Fallo al eliminar", + "ResetQualityDefinitions": "Restablecer definiciones de calidad", + "Scene": "Escena", + "RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)", + "SceneNumberNotVerified": "El número de escena no ha sido verificado aún", + "SearchForAllMissingEpisodes": "Buscar todos los episodios perdidos", + "SeasonInformation": "Información de temporada", + "SeasonNumber": "Número de temporada", + "SeasonCount": "Recuento de temporada", + "SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga", + "SelectEpisodes": "Seleccionar episodio(s)", + "SeriesDetailsGoTo": "Ir a {title}", + "SeriesTypes": "Tipos de serie", + "SeriesTypesHelpText": "El tipo de serie es usado para renombrar, analizar y buscar", + "SingleEpisodeInvalidFormat": "Episodio individual: Formato inválido", + "SslCertPasswordHelpText": "Contraseña para el archivo pfx", + "SslPort": "Puerto SSL", + "StandardEpisodeFormat": "Formato de episodio estándar", + "StartProcessing": "Iniciar procesamiento", + "SupportedListsMoreInfo": "Para más información en las listas individuales, haz clic en los botones de más información.", + "TagDetails": "Detalles de etiqueta - {label}", + "Total": "Total", + "True": "Verdadero", + "Umask770Description": "{octal} - Usuario y grupo escriben", + "UsenetBlackholeNzbFolder": "Carpeta Nzb", + "UsenetDelay": "Retraso de usenet", + "UsenetBlackhole": "Blackhole de usenet", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} reportó archivos en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones de tu cliente de descarga.", + "RemoveFilter": "Eliminar filtro", + "RemoveQueueItemConfirmation": "¿Estás seguro que quieres eliminar '{sourceTitle}' de la cola?", + "RemoveRootFolder": "Eliminar la carpeta raíz", + "RemoveSelectedItem": "Eliminar elemento seleccionado", + "RemoveTagsAutomaticallyHelpText": "Eliminar etiquetas automáticamente si las condiciones no se cumplen", + "RemovedFromTaskQueue": "Eliminar de la cola de tareas", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Las series {series} fueron eliminadas de TheTVDB", + "RenameFiles": "Renombrar archivos", + "ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?", + "ResetDefinitions": "Restablecer definiciones", + "ResetDefinitionTitlesHelpText": "Restablecer títulos de definición también como valores", + "ResetQualityDefinitionsMessageText": "¿Estás seguro que quieres restablecer las definiciones de calidad?", + "RestartNow": "Reiniciar ahora", + "RestartRequiredToApplyChanges": "{appName} requiere reiniciar para aplicar cambios. ¿Quieres reiniciar ahora?", + "RestartSonarr": "Reiniciar {appName}", + "RestoreBackup": "Restaurar copia de seguridad", + "Result": "Resultado", + "RetryingDownloadOn": "Reintentar descarga en {date} a las {time}", + "Rss": "RSS", + "SaveChanges": "Guardar cambios", + "SceneNumbering": "Numeración de escena", + "Script": "Script", + "Search": "Buscar", + "SearchForMonitoredEpisodesSeason": "Buscar episodios monitorizados en esta temporada", + "SearchForQuery": "Buscar {query}", + "SeasonFolder": "Carpeta de temporada", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} episodios descargados", + "SelectDropdown": "Seleccionar...", + "SelectLanguageModalTitle": "{modalTitle} - Seleccionar idioma", + "SelectLanguages": "Seleccionar idiomas", + "SelectReleaseGroup": "Seleccionar grupo de lanzamiento", + "SeriesDetailsNoEpisodeFiles": "Sin archivos de episodio", + "SeriesFolderImportedTooltip": "Episodio importado de la carpeta de serie", + "SeriesIsMonitored": "La serie está monitorizada", + "SeriesLoadError": "No se pudo cargar la serie", + "SeriesIsUnmonitored": "La serie no está monitorizada", + "SetPermissionsLinuxHelpTextWarning": "Si no estás seguro qué configuraciones hacer, no las cambies.", + "SetPermissionsLinuxHelpText": "¿Debería ejecutarse chmod cuando los archivos son importados/renombrados?", + "SetReleaseGroup": "Establecer grupo de lanzamiento", + "ShowEpisodeInformationHelpText": "Muestra el título y número de episodio", + "ShowMonitoredHelpText": "Muestra el estado monitorizado bajo el póster", + "ShowQualityProfile": "Mostrar perfil de calidad", + "ShowQualityProfileHelpText": "Muestra el perfil de calidad bajo el póster", + "ShowRelativeDates": "Mostrar fechas relativas", + "OnImport": "Al importar", + "Other": "Otro", + "ShowRelativeDatesHelpText": "Muestra fechas absolutas o relativas (Hoy/Ayer/etc)", + "Proxy": "Proxy", + "ShowSearch": "Mostrar búsqueda", + "ShowSearchHelpText": "Muestra el botón de búsqueda al pasar por encima", + "ShowSeasonCount": "Muestra el recuento de temporada", + "ShowAdvanced": "Mostrar avanzado", + "Socks4": "Socks4", + "Socks5": "Socks5 (Soporta TOR)", + "ShowTitle": "Mostrar título", + "Unknown": "Desconocido", + "Sort": "Ordenar", + "SourcePath": "Ruta de la fuente", + "SourceRelativePath": "Ruta relativa de la fuente", + "Special": "Especial", + "SourceTitle": "Título de la fuente", + "SpecialEpisode": "Episodio especial", + "Specials": "Especiales", + "SpecialsFolderFormat": "Formato de carpeta de los especiales", + "SslCertPassword": "Contraseña de certificado SSL", + "SupportedCustomConditions": "{appName} soporta condiciones personalizadas para las siguientes propiedades de lanzamiento.", + "SupportedDownloadClients": "{appName} soporta muchos torrent populares y clientes de descarga de usenet.", + "SupportedIndexers": "{appName} soporta cualquier indexador que use el estándar Newznab, así como otros indexadores listados a continuación.", + "OnSeriesDelete": "Al borrar series", + "OnRename": "Al renombrar", + "OutputPath": "Ruta de salida", + "PreferAndUpgrade": "Preferir y actualizar", + "Presets": "Preajustes", + "ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "QueueLoadError": "Fallo al cargar la cola", + "ReadTheWikiForMoreInformation": "Lee la Wiki para más información", + "RecyclingBinCleanupHelpText": "Establece a 0 para deshabilitar la limpieza automática", + "RegularExpressionsTutorialLink": "Más detalles de las expresiones regulares pueden ser encontradas [aquí]({url}).", + "RejectionCount": "Recuento de rechazos", + "RemotePathMappingFileRemovedHealthCheckMessage": "El fichero {path} ha sido eliminado durante el proceso.", + "RemotePathMappings": "Mapeos de ruta remota", + "RemovedSeriesSingleRemovedHealthCheckMessage": "La serie {series} fue eliminada de TheTVDB", + "ReplaceWithDash": "Reemplazar con guion", + "ReplaceWithSpaceDash": "Reemplazar por barra espaciadora", + "ReplaceWithSpaceDashSpace": "Reemplazar por espacio en la barra espaciadora", + "ScriptPath": "Ruta del script", + "SeasonDetails": "Detalles de temporada", + "SecretToken": "Token secreto", + "SelectQuality": "Seleccionar calidad", + "SelectLanguage": "Seleccionar idioma", + "SelectSeason": "Seleccionar temporada", + "SeriesIndexFooterMissingUnmonitored": "Episodios perdidos (Serie no monitorizada)", + "ShowEpisodeInformation": "Mostrar información de episodio", + "ShowPath": "Mostrar ruta", + "ShowNetwork": "Mostrar red", + "TvdbIdExcludeHelpText": "La ID de TVDB de la serie a excluir", + "UpdateSonarrDirectlyLoadError": "No se pudo actualizar {appName} directamente,", + "UpgradesAllowedHelpText": "Si se deshabilita las calidades no serán actualizadas", + "WithFiles": "Con archivos", + "SystemTimeHealthCheckMessage": "La hora del sistema está desfasada más de 1 día. Las tareas programadas pueden no ejecutarse correctamente hasta que la hora sea corregida", + "TableColumns": "Columnas", + "TableColumnsHelpText": "Elige qué columnas son visibles en qué orden aparecen", + "TablePageSize": "Tamaño de página", + "TablePageSizeHelpText": "Número de elementos a mostrar en cada página", + "TablePageSizeMinimum": "El tamaño de página debe ser al menos {minimumValue}", + "TablePageSizeMaximum": "El tamaño de página no debe exceder {maximumValue}", + "TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada", + "TagsSettingsSummary": "Vea todas las etiquetas y cómo se usan. Las etiquetas sin usar pueden ser eliminadas", + "TaskUserAgentTooltip": "User-Agent proporcionado por la aplicación que llamó a la API", + "Test": "Prueba", + "TestAllIndexers": "Probar todos los indexadores", + "TestAllLists": "Probar todas las listas", + "TestParsing": "Probar análisis", + "ThemeHelpText": "Cambiar el tema de la interfaz de la aplicación, el tema 'Auto' usará el tema de tu sistema para establecer el modo luminoso u oscuro. Inspirado por Theme.Park", + "TimeLeft": "Tiempo restante", + "ToggleMonitoredSeriesUnmonitored ": "No se puede conmutar el estado monitorizado cuando la serie no está monitorizada", + "Tomorrow": "Mañana", + "TorrentBlackhole": "Blackhole de torrent", + "TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet", + "TorrentBlackholeSaveMagnetFilesExtension": "Guardar extensión de archivos magnet", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Solo lectura", + "TorrentBlackholeTorrentFolder": "Carpeta de torrent", + "TorrentDelayHelpText": "Retraso en minutos a esperar antes de capturar un torrent", + "TorrentDelayTime": "Retraso torrent: {torrentDelay}", + "Umask755Description": "{octal} - Usuario escribe, Todos los demás leen", + "Umask777Description": "{octal} - Todos escriben", + "UnableToLoadAutoTagging": "No se pudo cargar el etiquetado automático", + "UnableToLoadBackups": "No se pudo cargar las copias de seguridad", + "Ungroup": "Sin agrupar", + "UnknownDownloadState": "Estado de descarga desconocido: {state}", + "Unlimited": "Ilimitado", + "UnmappedFilesOnly": "Solo archivos sin mapear", + "UnmonitorDeletedEpisodes": "Dejar de monitorizar episodios borrados", + "UnmonitoredOnly": "Solo sin monitorizar", + "UnsavedChanges": "Cambios sin guardar", + "UnselectAll": "Desmarcar todo", + "Upcoming": "Próximamente", + "UpcomingSeriesDescription": "Series que han sido anunciadas pero aún no hay fecha de emisión exacta", + "ReleaseSceneIndicatorUnknownSeries": "Episodio o serie desconocido.", + "RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.", + "RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto", + "SelectFolder": "Seleccionar carpeta", + "TestAllClients": "Probar todos los clientes", + "UpdateFiltered": "Actualizar filtrados", + "SeriesEditor": "Editor de serie", + "Updates": "Actualizaciones", + "NotificationsKodiSettingsDisplayTimeHelpText": "Durante cuánto tiempo serán mostradas las notificaciones (en segundos)", + "NotificationsNtfySettingsUsernameHelpText": "Usuario opcional", + "NotificationsSimplepushSettingsEvent": "Evento", + "NotificationsSimplepushSettingsEventHelpText": "Personaliza el comportamiento de las notificaciones push", + "NotificationsTwitterSettingsConsumerSecret": "Secreto de consumidor", + "NotificationsTelegramSettingsSendSilently": "Enviar de forma silenciosa", + "NotificationsValidationInvalidHttpCredentials": "Credenciales de autenticación HTTP inválidas: {exceptionMessage}", + "OnEpisodeFileDeleteForUpgrade": "Al borrar un archivo de episodio para actualización", + "OnHealthIssue": "Al haber un problema de salud", + "Organize": "Organizar", + "OrganizeRenamingDisabled": "El renombrado está deshabilitado, nada que renombrar", + "OrganizeNothingToRename": "¡Éxito! Mi trabajo está hecho, no hay archivos que renombrar.", + "OrganizeRelativePaths": "Todas las rutas son relativas a: `{path}`", + "Pending": "Pendiente", + "QualityDefinitions": "Definiciones de calidad", + "RecyclingBin": "Papelera de reciclaje", + "ReleaseProfileIndexerHelpTextWarning": "Usar un indexador específico con perfiles de lanzamientos puede conllevar que lanzamientos duplicados sean capturados", + "ReleaseTitle": "Título de lanzamiento", + "RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente", + "Remove": "Eliminar", + "RetentionHelpText": "Solo usenet: Establece a cero para establecer una retención ilimitada", + "SelectIndexerFlags": "Seleccionar banderas del indexador", + "SelectSeasonModalTitle": "{modalTitle} - Seleccionar temporada", + "SeriesFinale": "Final de serie", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}).", + "SetIndexerFlags": "Establecer banderas del indexador", + "SkipRedownload": "Saltar redescarga", + "ShowMonitored": "Mostrar monitorizado", + "Space": "Espacio", + "TimeFormat": "Formato de hora", + "UiSettings": "Opciones de interfaz", + "Umask": "UMask", + "UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' no es modificable por el usuario '{userName}'.", + "UsenetDelayHelpText": "Retraso en minutos a esperar antes de capturar un lanzamiento desde usenet", + "PartialSeason": "Temporada parcial", + "RemoveSelectedItemQueueMessageText": "¿Estás seguro que quieres eliminar 1 elemento de la cola?", + "SceneInformation": "Información de escena", + "UpgradeUntilThisQualityIsMetOrExceeded": "Actualizar hasta que esta calidad sea alcanzada o excedida", + "Uppercase": "Mayúsculas", + "SeriesDetailsRuntime": "{runtime} minutos", + "ShowBannersHelpText": "Muestra banners en lugar de títulos", + "SslCertPathHelpText": "Ruta al archivo pfx", + "Umask750Description": "{octal} - Usuario escribe, Grupo lee", + "UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío", + "UpdateAll": "Actualizar todo", + "ConnectionSettingsUrlBaseHelpText": "Añade un prefijo a la url {connectionName}, como {url}", + "UsenetDelayTime": "Retraso de usenet: {usenetDelay}", + "UsenetDisabled": "Usenet deshabilitado", + "Username": "Usuario", + "UtcAirDate": "Fecha de emisión UTC", + "Version": "Versión", + "WaitingToImport": "Esperar para importar", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al capturar'", + "NotificationsNtfyValidationAuthorizationRequired": "Se requiere autorización", + "NotificationsNtfySettingsClickUrl": "URL al hacer clic", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Tu clave API de tu perfil", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Autenticar con Plex.tv", + "NotificationsPushcutSettingsNotificationNameHelpText": "Nombre de notificación de la pestaña Notificaciones de la aplicación Pushcut", + "NotificationsPlexValidationNoTvLibraryFound": "Al menos se requiere una biblioteca de TV", + "NotificationsPushBulletSettingSenderId": "ID del remitente", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID de grupo / Número de teléfono del receptor", + "NotificationsSettingsWebhookMethod": "Método", + "NotificationsSettingsUseSslHelpText": "Conectar a {serviceName} sobre HTTPS en vez de HTTP", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Secreto de consumidor de una aplicación de Twitter", + "NotificationsValidationInvalidUsernamePassword": "Usuario o contraseña inválido", + "NotificationsSlackSettingsWebhookUrlHelpText": "URL de canal webhook de Slack", + "PackageVersion": "Versión del paquete", + "NotificationsValidationUnableToConnectToApi": "No se pudo conectar a la API de {service}. La conexión al servidor falló: ({responseCode}) {exceptionMessage}", + "PosterOptions": "Opciones de póster", + "PreferTorrent": "Preferir torrent", + "PreviewRenameSeason": "Previsualizar renombrado para esta temporada", + "PreviousAiring": "Emisiones anteriores", + "RemoveFromDownloadClient": "Eliminar del cliente de descarga", + "RemovingTag": "Eliminando etiqueta", + "Required": "Solicitado", + "Reorder": "Reordenar", + "SceneInfo": "Información de escena", + "RootFolderMissingHealthCheckMessage": "Carpeta raíz perdida: {rootFolderPath}", + "SearchAll": "Buscar todo", + "SelectAll": "Seleccionar todo", + "SeriesIndexFooterEnded": "FInalizado (Todos los episodios descargados)", + "ShowDateAdded": "Mostrar fecha de adición", + "UnmonitorDeletedEpisodesHelpText": "Los episodios borrados del disco son dejados de monitorizar automáticamente en {appName}", + "UnmonitorSelected": "Dejar de monitorizar seleccionados", + "UpdateSelected": "Actualizar seleccionados", + "UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz '{uiFolder}' no es modificable por el usuario '{userName}'.", + "UpgradeUntil": "Actualizar hasta", + "UpdaterLogFiles": "Actualizador de archivos de registro", + "UseSeasonFolder": "Usar carpeta de temporada", + "UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar", + "View": "Vista", + "VisitTheWikiForMoreDetails": "Visita la wiki para más detalles: ", + "WaitingToProcess": "Esperar al proceso", + "Week": "Semana", + "WeekColumnHeader": "Cabecera de columna de semana", + "Release": "Lanzamiento", + "RemoveSelectedItems": "Eliminar elementos seleccionados", + "RemoveSelectedItemsQueueMessageText": "¿Estás seguro que quieres eliminar {selectedCount} elementos de la cola?", + "RootFolderSelectFreeSpace": "{freeSpace} libres", + "RootFolderPath": "Ruta de carpeta raíz", + "RssSyncInterval": "Intervalo de sincronización RSS", + "SingleEpisode": "Episodio individual", + "ShowUnknownSeriesItems": "Mostrar elementos de serie desconocidos", + "NotificationsGotifySettingIncludeSeriesPoster": "Incluir poster de serie", + "NotificationsKodiSettingsCleanLibraryHelpText": "Limpia la biblioteca después de actualizar", + "NotificationsKodiSettingsCleanLibrary": "Limpiar biblioteca", + "NotificationsKodiSettingsGuiNotification": "Notificación de interfaz gráfica", + "NotificationsKodiSettingsUpdateLibraryHelpText": "¿Actualiza la biblioteca durante Importar y renombrar?", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Habilitar el uso del endpoint de UE de MailGun", + "NotificationsMailgunSettingsUseEuEndpoint": "Usar el endpoint de la UE", + "NotificationsNtfySettingsAccessToken": "Token de acceso", + "NotificationsNtfySettingsAccessTokenHelpText": "Autorización opcional basada en token. Tiene prioridad sobre usuario/contraseña", + "NotificationsNtfySettingsTagsEmojisHelpText": "Lista opcional de etiquetas o emojis para usar", + "NotificationsNtfySettingsTopics": "Temas", + "NotificationsPushBulletSettingsDeviceIds": "IDs de dispositivo", + "NotificationsPushBulletSettingsAccessToken": "Token de acceso", + "NotificationsPushBulletSettingsChannelTags": "Etiquetas de canal", + "NotificationsPushcutSettingsTimeSensitive": "Sensible al tiempo", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Habilitar para marcas la notificación como \"Sensible al tiempo\"", + "NotificationsPushoverSettingsDevices": "Dispositivos", + "NotificationsPushoverSettingsDevicesHelpText": "Lista de nombres de dispositivo (deja en blanco para enviar a todos los dispositivos)", + "NotificationsPushoverSettingsExpireHelpText": "Tiempo máximo para reintentar las alertas de emergencia, máximo 86400 segundos", + "NotificationsPushoverSettingsRetry": "Reintentar", + "NotificationsPushoverSettingsSound": "Sonido", + "NotificationsPushoverSettingsUserKey": "Clave de usuario", + "NotificationsPushoverSettingsSoundHelpText": "Sonido de notificación, deja en blanco para usar el predeterminado", + "NotificationsSettingsWebhookMethodHelpText": "Qué método HTTP utilizar para enviar al servicio web", + "NotificationsSettingsWebhookUrl": "URL del webhook", + "NotificationsSignalSettingsGroupIdPhoneNumber": "ID de grupo / Número de teléfono", + "NotificationsSignalSettingsPasswordHelpText": "Contraseña usada para autenticar solicitudes hacia signal-api", + "NotificationsSignalSettingsSenderNumber": "Número del emisor", + "NotificationsSignalSettingsUsernameHelpText": "Usuario usado para autenticar solicitudes hacia signal-api", + "NotificationsSignalValidationSslRequired": "Se requiere SSL", + "NotificationsSimplepushSettingsKey": "Clave", + "NotificationsSlackSettingsChannel": "Canal", + "NotificationsSlackSettingsIconHelpText": "Cambia el icono usado para mensajes publicados a Slack (emoji o URL)", + "NotificationsSlackSettingsUsernameHelpText": "Usuario para publicar a Slack", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Envía el mensaje de forma silenciosa. Los usuarios recibirán una notificación sin sonido", + "NotificationsTelegramSettingsTopicId": "ID de tema", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Autenticar con Trakt", + "NotificationsTraktSettingsExpires": "Caduca", + "NotificationsValidationUnableToConnectToService": "No se pudo conectar a {serviceName}", + "NotificationsValidationUnableToSendTestMessage": "No se pudo enviar un mensaje de prueba: {exceptionMessage}", + "NzbgetHistoryItemMessage": "Estado de PAR: {parStatus} - Estado de desempaquetado: {unpackStatus} - Estado de movido: {moveStatus} - Estado de script: {scriptStatus} - Estado de borrado: {deleteStatus} - Estado de marcado: {markStatus}", + "OpenSeries": "Abrir serie", + "OrganizeLoadError": "Error cargando vistas previas", + "OrganizeNamingPattern": "Patrón de nombrado: `{episodeFormat}`", + "OverrideGrabNoSeries": "La serie debe ser seleccionada", + "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "PendingChangesDiscardChanges": "Descartar cambios y salir", + "Period": "Periodo", + "PendingChangesMessage": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir de esta página?", + "PreviouslyInstalled": "Previamente instalado", + "ProtocolHelpText": "Elige qué protocolo(s) usar y cuál se prefiere cuando se elige entre lanzamientos equivalentes", + "ProgressBarProgress": "Barra de progreso al {progress}%", + "ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios", + "ProxyResolveIpHealthCheckMessage": "Fallo al resolver la dirección IP para el host proxy configurado {proxyHostName}", + "ProxyUsernameHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "QualityProfile": "Perfil de calidad", + "QualityDefinitionsLoadError": "No se pudo cargar las definiciones de calidad", + "QualityProfiles": "Perfiles de calidad", + "QualityProfilesLoadError": "No se pudo cargar los perfiles de calidad", + "QueueFilterHasNoItems": "Seleccionado filtro de cola que no tiene elementos", + "QuickSearch": "Búsqueda rápida", + "Real": "Real", + "Reason": "Razón", + "RegularExpression": "Expresión regular", + "ReleaseHash": "Hash de lanzamiento", + "Rejections": "Rechazos", + "RecyclingBinCleanupHelpTextWarning": "Los archivos en la papelera de reciclaje anteriores al número de días seleccionado serán limpiados automáticamente", + "ReleaseProfiles": "Perfiles de lanzamiento", + "ReleaseRejected": "Lanzamiento rechazado", + "ReleaseSceneIndicatorAssumingScene": "Asumiendo numeración de escena.", + "ReleaseSceneIndicatorAssumingTvdb": "Asumiendo numeración de TVDB.", + "ReleaseSceneIndicatorUnknownMessage": "La numeración varía para este episodio y el lanzamiento no coincide con ningún mapeo conocido.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} puede ver pero no acceder al episodio descargado {path}. Probablemente error de permisos.", + "RemotePathMappingRemotePathHelpText": "Ruta raíz al directorio al que accede el cliente de descarga", + "RemoveFailedDownloads": "Eliminar descargas fallidas", + "RemoveSelected": "Eliminar seleccionado", + "RenameEpisodesHelpText": "{appName} usará el nombre de archivo existente si el renombrado está deshabilitado", + "RenameEpisodes": "Renombrar episodios", + "RestrictionsLoadError": "No se pudo cargar Restricciones", + "SearchForMissing": "Buscar perdidos", + "SeasonFinale": "Final de temporada", + "SearchSelected": "Buscar seleccionados", + "SeasonFolderFormat": "Formato de carpeta de temporada", + "SendAnonymousUsageData": "Enviar datos de uso anónimos", + "SeriesDetailsOneEpisodeFile": "1 archivo de episodio", + "SeriesFolderFormatHelpText": "Usado cuando se añade una nueva serie o se mueve la serie a través del editor de serie", + "SeriesID": "ID de serie", + "SetPermissions": "Establecer permisos", + "SetReleaseGroupModalTitle": "{modalTitle} - Establecer grupo de lanzamiento", + "SetTags": "Establecer etiquetas", + "ShowPreviousAiring": "Mostrar emisión anterior", + "ShowSizeOnDisk": "Mostrar tamaño en disco", + "SizeOnDisk": "Tamaño en disco", + "SizeLimit": "Límite de tamaño", + "SkipRedownloadHelpText": "Evita que {appName} intente descargar un lanzamiento alternativo para este elemento", + "Small": "Pequeño", + "SomeResultsAreHiddenByTheAppliedFilter": "Algunos resultados están ocultos por el filtro aplicado", + "SonarrTags": "Etiquetas de {appName}", + "Standard": "Estándar", + "StandardEpisodeTypeFormat": "Temporada y número de episodios ({format})", + "StandardEpisodeTypeDescription": "Episodios lanzados con patrón SxxEyy", + "SubtitleLanguages": "Idiomas de subtítulo", + "SupportedAutoTaggingProperties": "{appName} soporta las siguientes propiedades para reglas de etiquetado automáticas", + "SupportedIndexersMoreInfo": "Para más información en los indexadores individuales, haz clic en los botones de más información.", + "SupportedListsSeries": "{appName} soporta múltiples listas para importar series en la base de datos.", + "TableOptions": "Opciones de tabla", + "TableOptionsButton": "Botón de opciones de tabla", + "Today": "Hoy", + "Titles": "Títulos", + "ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar", + "TotalFileSize": "Tamaño total de archivo", + "UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización", + "UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado", + "UrlBase": "URL base", + "UseSsl": "Usar SSL", + "Usenet": "Usenet", + "VersionNumber": "Versión {version}", + "OnManualInteractionRequired": "Cuando se requiera interacción manual", + "OnLatestVersion": "La última versión de {appName} ya está instalada", + "OnUpgrade": "Al actualizar", + "RootFolders": "Carpetas raíz", + "SeasonPremiere": "Estreno de temporada", + "UnableToUpdateSonarrDirectly": "No se pudo actualizar {appName} directamente,", + "UnmappedFolders": "Carpetas sin mapear", + "QualitiesLoadError": "No se pudo cargar las calidades", + "SeasonNumberToken": "Temporada {seasonNumber}", + "PreferredSize": "Tamaño preferido", + "TypeOfList": "Lista {typeOfList}", + "UiSettingsLoadError": "No se pudo cargar las opciones de interfaz", + "UpdateMonitoring": "Actualizar monitorizando", + "ReleaseType": "Tipo de lanzamiento", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones del cliente de descarga.", + "RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas desde el historial del cliente de descarga", + "RemoveFromQueue": "Eliminar de la cola", + "RemoveMultipleFromDownloadClientHint": "Elimina descargas y archivos del cliente de descarga", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará las descargas y los archivos del cliente de descarga.", + "RemoveTagsAutomatically": "Eliminar etiquetas automáticamente", + "ReplaceIllegalCharactersHelpText": "Reemplaza los caracteres ilegales. Si no está marcado, {appName} los eliminará en su lugar", + "ResetAPIKey": "Restablecer clave API", + "RootFolder": "Carpeta raíz", + "RootFolderMultipleMissingHealthCheckMessage": "Múltiples carpetas raíz están perdidas: {rootFolderPaths}", + "RestartReloadNote": "Nota: {appName} se reiniciará automáticamente y recargará la interfaz durante el proceso de restauración.", + "RestartRequiredWindowsService": "Dependiendo de qué usuario esté ejecutando el servicio {appName}, puede ser necesario reiniciar {appName} como administrador antes de que el servicio se inicie automáticamente.", + "SeasonPremieresOnly": "Solo estrenos de temporada", + "SeasonPassTruncated": "Solo se muestran las últimas 25 temporadas, ve a detalles para ver todas las temporadas", + "SelectFolderModalTitle": "{modalTitle} - Seleccionar carpeta", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio", + "SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)", + "SetIndexerFlagsModalTitle": "{modalTitle} - Establecer banderas del indexador", + "ShortDateFormat": "Formato de fecha breve", + "ShowUnknownSeriesItemsHelpText": "Muestra elementos sin una serie en la cola, esto incluiría series eliminadas, películas o cualquier cosa más en la categoría de {appName}", + "ShownClickToHide": "Mostrado, haz clic para ocultar", + "SkipFreeSpaceCheckWhenImportingHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz durante la importación de archivo", + "SmartReplace": "Reemplazo inteligente", + "SupportedDownloadClientsMoreInfo": "Para más información en los clientes de descarga individuales, haz clic en los botones de más información.", + "SupportedImportListsMoreInfo": "Para más información de los listas de importación individuales, haz clic en los botones de más información.", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "En lugar de mover archivos esto indicará a {appName} que copie o enlace (dependiendo de los ajustes/configuración del sistema)", + "TorrentDelay": "Retraso de torrent", + "ToggleMonitoredToUnmonitored": "Monitorizado, haz clic para dejar de monitorizar", + "TorrentBlackholeSaveMagnetFilesHelpText": "Guarda el enlace magnet si no hay ningún archivo .torrent disponible (útil solo si el cliente de descarga soporta magnets guardados en un archivo)", + "UiLanguage": "Idioma de interfaz", + "UiLanguageHelpText": "Idioma que {appName} usará en la interfaz", + "UiSettingsSummary": "Opciones de calendario, fecha y color alterado", + "UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones", + "TotalRecords": "Total de registros: {totalRecords}", + "WantMoreControlAddACustomFormat": "¿Quieres más control sobre qué descargas son preferidas? Añade un [formato personalizado](/opciones/formatospersonalizados)", + "OrganizeModalHeader": "Organizar y renombrar", + "RemoveCompleted": "Eliminar completado", + "OpenBrowserOnStartHelpText": " Abre un navegador web y navega a la página de inicio de {appName} al iniciar la aplicación.", + "SslCertPath": "Ruta del certificado SSL", + "StartImport": "Iniciar importación", + "OptionalName": "Nombre opcional", + "RemotePath": "Ruta remota", + "SeriesPremiere": "Estreno de serie", + "SeriesMatchType": "Tipo de emparejamiento de series", + "SeriesMonitoring": "Monitorización de serie", + "Tba": "TBA", + "TorrentsDisabled": "Torrents deshabilitados", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.", + "ReplaceIllegalCharacters": "Reemplazar caracteres ilegales", + "ResetTitles": "Restablecer títulos", + "SmartReplaceHint": "Raya o barra espaciadora según el nombre", + "SelectEpisodesModalTitle": "{modalTitle} - Seleccionar episodio(s)", + "DownloadClientDelugeSettingsDirectory": "Directorio de descarga", + "DownloadClientDelugeSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Deluge", + "UnmonitorSpecialsEpisodesDescription": "Dejar de monitorizar todos los episodios especiales sin cambiar el estado monitorizado de otros episodios", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicación opcional a la que mover las descargas completadas, dejar en blanco para usar la ubicación predeterminada de Deluge", + "DownloadClientDelugeSettingsDirectoryCompleted": "Directorio al que mover cuando se complete", + "NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord", + "NotificationsEmailSettingsCcAddress": "Dirección(es) CC", + "NotificationsEmbySettingsSendNotifications": "Enviar notificaciones", + "NotificationsEmbySettingsUpdateLibraryHelpText": "¿Actualiza biblioteca en importar, renombrar o borrar?", + "NotificationsJoinSettingsDeviceIdsHelpText": "En desuso, usar Nombres de dispositivo en su lugar. Lista separada por coma de los IDs de dispositivo a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.", + "NotificationsPushoverSettingsExpire": "Caduca", + "NotificationsMailgunSettingsSenderDomain": "Dominio del remitente", + "NotificationsNtfySettingsServerUrl": "URL del servidor", + "PreferProtocol": "Preferir {preferredProtocol}", + "ProfilesSettingsSummary": "Perfiles de calidad, de retraso de idioma y de lanzamiento", + "QualitiesHelpText": "Calidades superiores en la lista son más preferibles. Calidades dentro del mismo grupo son iguales. Comprobar solo calidades que se busquen", + "RssIsNotSupportedWithThisIndexer": "RSS no está soportado con este indexador", + "Repack": "Reempaquetar", + "NotificationsGotifySettingsPriorityHelpText": "Prioridad de la notificación", + "NotificationsGotifySettingsServer": "Servidor Gotify", + "NotificationsPlexSettingsAuthToken": "Token de autenticación", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Llamada synoindex en localhost para actualizar un archivo de biblioteca", + "Overview": "Vista general", + "UseSeasonFolderHelpText": "Ordenar episodios en carpetas de temporada", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir dentro del contenedor. Revisa tus mapeos de ruta remotos y opciones de volumen del contenedor.", + "Retention": "Retención", + "NotificationsDiscordSettingsOnManualInteractionFields": "Campos durante la interacción manual", + "NotificationsDiscordSettingsOnGrabFields": "Campos al capturar", + "NotificationsDiscordSettingsOnImportFields": "Campos al importar", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al importar'", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'durante la interacción manual'", + "NotificationsEmailSettingsCcAddressHelpText": "Lista separada por coma de destinatarios de e-mail cc", + "NotificationsEmailSettingsFromAddress": "De dirección", + "NotificationsKodiSettingAlwaysUpdateHelpText": "¿Actualiza la biblioteca incluso cuando un video se esté reproduciendo?", + "NotificationsKodiSettingsDisplayTime": "Tiempo de visualización", + "NotificationsLoadError": "No se pudo cargar las notificaciones", + "NotificationsMailgunSettingsApiKeyHelpText": "La clave API generada desde MailGun", + "NotificationsSendGridSettingsApiKeyHelpText": "La clave API generada por SendGrid", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Clave de consumidor de una aplicación de Twitter", + "NotificationsTwitterSettingsDirectMessage": "Mensaje directo", + "NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público", + "OnApplicationUpdate": "Al actualizar la aplicación", + "OnSeriesAdd": "Al añadir series", + "OnlyForBulkSeasonReleases": "Solo para lanzamientos de temporada a granel", + "OrganizeModalHeaderSeason": "Organizar y renombrar - {season}", + "OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado", + "OverrideGrabNoQuality": "La calidad debe ser seleccionada", + "NotificationsValidationInvalidAuthenticationToken": "Token de autenticación inválido", + "NotificationsValidationUnableToConnect": "No se pudo conectar: {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "No se pudo enviar un mensaje de prueba. Respuesta de la API: {error}", + "OverrideGrabModalTitle": "Sobrescribe y captura - {title}", + "ReleaseProfileTagSeriesHelpText": "Los perfiles de lanzamientos se aplicarán a series con al menos una etiqueta coincidente. Deja en blanco para aplicar a todas las series", + "ReleaseSceneIndicatorMappedNotRequested": "El episodio mapeado no fue solicitado en esta búsqueda.", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} puede ver pero no acceder al directorio de descarga {downloadPath}. Probablemente error de permisos.", + "RemotePathMappingHostHelpText": "El mismo host que especificaste para el cliente de descarga remoto", + "ParseModalUnableToParse": "No se pudo analizar el título proporcionado, por favor inténtalo de nuevo.", + "Preferred": "Preferido", + "Priority": "Prioridad", + "QualityProfileInUseSeriesListCollection": "No se puede borrar un perfil de calidad que está asignado a una serie, lista o colección", + "ReleaseProfile": "Perfil de lanzamiento", + "ReleaseProfileIndexerHelpText": "Especifica a qué indexador se aplica el perfil", + "RequiredHelpText": "Esta condición {implementationName} debe coincidir para el formato personalizado para aplicar. De otro modo una coincidencia sencilla {implementationName} es suficiente.", + "RemotePathMappingsLoadError": "No se pudo cargar los mapeos de ruta remota", + "RestartLater": "Reiniciaré más tarde", + "RootFoldersLoadError": "No se pudo cargar las carpetas raíz", + "RssSync": "Sincronización RSS", + "RssSyncIntervalHelpTextWarning": "Esto se aplicará a todos los indexadores, por favor sigue las reglas establecidas por ellos", + "Score": "Puntuación", + "SearchFailedError": "La búsqueda falló, por favor inténtalo de nuevo más tarde.", + "SearchForMonitoredEpisodes": "Buscar episodios monitorizados", + "SearchIsNotSupportedWithThisIndexer": "La búsqueda no está soportada con este indexador", + "SearchMonitored": "Buscar monitorizados", + "SeasonPack": "Pack de temporada", + "SeriesCannotBeFound": "Lo siento, esta serie no puede ser encontrada.", + "SeriesEditRootFolderHelpText": "Mover series a la misma carpeta raíz se puede usar para renombrar carpetas de series para coincidir el título actualizado o el formato de nombrado", + "SeriesFolderFormat": "Formato de carpeta de serie", + "SeriesIndexFooterDownloading": "Descargando (Uno o más episodios)", + "SeriesIndexFooterMissingMonitored": "Episodios perdidos (Serie monitorizada)", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Total: {totalEpisodeCount}, Descargando: {downloadingCount})", + "UpgradesAllowed": "Actualizaciones permitidas", + "VideoCodec": "Códec de vídeo", + "SeriesTitleToExcludeHelpText": "El nombre de la serie a excluir", + "Shutdown": "Apagar", + "TestAll": "Probar todo", + "UseProxy": "Usar proxy", + "Repeat": "Repetir", + "Replace": "Reemplazar", + "RemoveCompletedDownloadsHelpText": "Elimina las descargas importadas desde el historial del cliente de descarga", + "RemoveQueueItemRemovalMethod": "Método de eliminación", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.", + "RemoveCompletedDownloads": "Eliminar descargas completadas", + "RemoveFromDownloadClientHint": "Elimina la descarga y archivo(s) del cliente de descarga", + "EpisodeRequested": "Episodio requerido", + "NotificationsEmailSettingsServer": "Servidor", + "NotificationsEmailSettingsServerHelpText": "Nombre de host o IP del servidor de e-mail", + "NotificationsGotifySettingsAppTokenHelpText": "El token de aplicación generado por Gotify", + "NotificationsGotifySettingsServerHelpText": "URL de servidor de Gotify, incluyendo http(s):// y puerto si es necesario", + "NotificationsJoinSettingsDeviceIds": "IDs de dispositivo", + "NotificationsJoinValidationInvalidDeviceId": "Los IDs de dispositivo parecen inválidos.", + "NotificationsKodiSettingAlwaysUpdate": "Actualizar siempre", + "NotificationsPushcutSettingsApiKeyHelpText": "Las claves API pueden ser gestionadas en la vista Cuenta de la aplicación Pushcut", + "NotificationsPushcutSettingsNotificationName": "Nombre de notificación", + "NotificationsPushoverSettingsRetryHelpText": "Intervalo para reintentar las alertas de emergencia, mínimo 30 segundos", + "NotificationsSettingsUpdateLibrary": "Actualizar biblioteca", + "NotificationsSettingsUpdateMapPathsTo": "Mapear rutas a", + "NotificationsSignalSettingsSenderNumberHelpText": "Número de teléfono del emisor registrado en signal-api", + "NotificationsSlackSettingsChannelHelpText": "Sobrescribe el canal predeterminado para el webhook entrante (#otro-canal)", + "NotificationsSlackSettingsIcon": "Icono", + "NotificationsSynologyValidationInvalidOs": "Debe ser un Synology", + "NotificationsSynologyValidationTestFailed": "No es Synology o synoindex no está disponible", + "NotificationsTelegramSettingsBotToken": "Token de bot", + "NotificationsTelegramSettingsChatId": "ID de chat", + "NotificationsTelegramSettingsTopicIdHelpText": "Especifica una ID de tema para enviar notificaciones a ese tema. Deja en blanco para usar el tema general (solo supergrupos)", + "NotificationsTraktSettingsAccessToken": "Token de acceso", + "NotificationsTraktSettingsAuthUser": "Autenticar usuario", + "NotificationsTwitterSettingsAccessToken": "Token de acceso", + "NotificationsTwitterSettingsAccessTokenSecret": "Token secreto de acceso", + "NotificationsTwitterSettingsConsumerKey": "Clave de consumidor", + "NotificationsTwitterSettingsMention": "Mención", + "NotificationsTwitterSettingsMentionHelpText": "Menciona este usuario en tweets enviados", + "NotificationsValidationInvalidAccessToken": "Token de acceso inválido", + "NotificationsValidationInvalidApiKey": "Clave API inválida", + "ParseModalErrorParsing": "Error analizando, por favor inténtalo de nuevo.", + "ParseModalHelpText": "Introduce un título de lanzamiento en la entrada anterior", + "SearchByTvdbId": "También puedes buscar usando la ID de TVDB de un show. P. ej. tvdb:71663", + "SearchForAllMissingEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios perdidos?", + "SeriesType": "Tipo de serie", + "TagCannotBeDeletedWhileInUse": "La etiqueta no puede ser borrada mientras esté en uso", + "UnmonitorSpecialEpisodes": "Dejar de monitorizar especiales", + "UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' está en una carpeta de translocalización de la aplicación.", + "Yesterday": "Ayer", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará la descarga y el archivo(s) del cliente de descarga.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir. Probablemente mapeo de ruta remota perdido o incorrecto.", + "RemotePathMappingsInfo": "Los mapeos de ruta remota son muy raramente solicitados, si {appName} y tu cliente de descarga están en el mismo sistema es mejor coincidir sus rutas. Para más información mira la [wiki]({wikiLink})", + "UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización", + "NotificationsTelegramSettingsChatIdHelpText": "Debes comenzar una conversación con el bot o añádelo a tu grupo para recibir mensajes", + "NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por coma de destinatarios de e-mail", + "NotificationsTwitterSettingsConnectToTwitter": "Conectar a Twitter / X", + "NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}", + "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')" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index af390db5a..c00cd1c8a 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -2,7 +2,7 @@ "RecycleBinUnableToWriteHealthCheckMessage": "Määritettyyn roskakorikansioon ei voida tallentaa: {path}. Varmista että sijainti on olemassa ja että sovelluksen suorittavalla käyttäjällä on siihen kirjoitusoikeus.", "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} näkee ladatun jakson \"{path}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "Added": "Lisäysaika", - "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää.", + "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää", "DownloadClientSortingHealthCheckMessage": "Lataustyökalun \"{downloadClientName}\" {sortingMode} on kytketty käyttöön {appName}in kategorialle ja tuontiongelmien välttämiseksi se tulisi poistaa käytöstä.", "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS-synkronointia varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName} ei kaappaa uusia julkaisuja automaattisesti.", "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalihaulle ei ole määritetty tietolähteitä, eikä se sen vuoksi löydä tuloksia.", @@ -15,7 +15,7 @@ "GrabId": "Kaappauksen tunniste", "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", "BrowserReloadRequired": "Käyttöönotto vaatii selaimen sivupäivityksen.", - "CustomFormatHelpText": "Julkaisut pisteytetään niitä vastaavien mukautettujen muotojen pisteiden yhteenlaskun summalla. Julkaisu kaapataan, jos se parantaa pisteytystä nykyisellä tai sitä paremmalla laadulla.", + "CustomFormatHelpText": "Julkaisut pisteytetään niitä vastaavien mukautettujen muotojen pisteiden yhteenlaskun summalla. {appName} tallentaa julkaisun, jos se parantaa arvosanaa nykyisellä laadulla tai parempaa.", "RemotePathMappingHostHelpText": "Sama osoite, joka on määritty etälataustyökalulle.", "AudioLanguages": "Äänen kielet", "Grabbed": "Kaapattu", @@ -25,7 +25,6 @@ "OriginalLanguage": "Alkuperäinen kieli", "ProxyResolveIpHealthCheckMessage": "Määritetyn välityspalvelimen \"{proxyHostName}\" IP-osoitteen selvitys epäonnistui.", "SetPermissionsLinuxHelpText": "Tulisiko chmod suorittaa, kun tiedostoja tuodaan/nimetään uudelleen?", - "UrlBaseHelpText": "Lisää {appName}in URL-osoitteeseen jälkiliitteen, esim. \"http://[osoite]:[portti]/[URL-perusta]\". Oletusarvo on tyhjä.", "SetPermissionsLinuxHelpTextWarning": "Jollet ole varma mitä nämä asetukset tekevät, älä muuta niitä.", "ClickToChangeLanguage": "Vaihda kieli painamalla tästä", "EnableColorImpairedModeHelpText": "Vaihtoehtoinen tyyli, joka auttaa erottamaan värikoodatut tiedot paremmin.", @@ -47,7 +46,7 @@ "AutomaticUpdatesDisabledDocker": "Automaattisia päivityksiä ei tueta suoraan käytettäessä Dockerin päivitysmekanismia. Docker-säiliö on päivitettävä {appName}in ulkopuolella tai päivitys on suoritettava komentosarjalla.", "AddListExclusionSeriesHelpText": "Estä {appName}ia lisäämästä sarjaa listoilta.", "AppUpdated": "{appName} on päivitetty", - "AuthenticationMethodHelpText": "Vaadi {appName}in käyttöön käyttäjätunnus ja salasana.", + "AuthenticationMethodHelpText": "Vaadi {appName}in käyttöön käyttäjätunnus ja salasana", "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja se on käynnistettävä uudelleen.", "DeleteTag": "Poista tunniste", "AppUpdatedVersion": "{appName} on päivitetty versioon {version} ja muutosten käyttöönottamiseksi se on käynnistettävä uudelleen. ", @@ -164,7 +163,7 @@ "OnGrab": "Kun julkaisu kaapataan", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} ei voinut lisätä tunnistetta qBittorrentiin.", "SeriesFolderFormat": "Sarjakansioiden kaava", - "TagDetails": "Tunnisteen \"{0}\" tiedot", + "TagDetails": "Tunnisteen \"{label}\" tiedot", "DownloadClientStatusSingleClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", "DownloadClientValidationCategoryMissing": "Kategoriaa ei ole olemassa", "EditSelectedDownloadClients": "Muokkaa valittuja lataustyökaluja", @@ -397,7 +396,7 @@ "DelayProfilesLoadError": "Virhe ladattaessa viiveprofiileja", "DeleteDownloadClient": "Poista lataustyökalu", "DeleteBackupMessageText": "Haluatko varmasti poistaa varmuuskopion \"{name}\"?", - "DeleteIndexerMessageText": "Haluatko varmasti poistaa tietolähteen \"{0}\"?", + "DeleteIndexerMessageText": "Haluatko varmasti poistaa tietolähteen '{name}'?", "DeleteRootFolderMessageText": "Haluatko varmasti poistaa juurikansion \"{path}\"?", "DeleteReleaseProfileMessageText": "Haluatko varmasti poistaa julkaisuprofiilin \"{name}\"?", "DeleteSelectedIndexers": "Poista tietoläh(de/teet)", @@ -504,7 +503,7 @@ "RefreshAndScan": "Päivitä ja tarkista", "Refresh": "Päivitä", "ReleaseProfilesLoadError": "Virhe ladattaessa julkaisuprofiileita", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Etälataustyökalu \"{0}\" tallentaa lataukset kohteeseen \"{1}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} ei voinut lisätä Label-tunnistetta {clientName}en.", "DownloadClientDelugeValidationLabelPluginInactive": "Label-tunnistelisäosa ei ole käytössä.", "AddConditionImplementation": "Lisätään ehtoa - {implementationName}", @@ -526,7 +525,7 @@ "ResetTitles": "Palauta nimet", "RestartLater": "Käynnistän uudelleen myöhemmin", "RestartReloadNote": "Huomioi: {appName} käynnistyy palautusprosessin aikana automaattisesti uudelleen.", - "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen.", + "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii in uudelleenkäynnistyksen.", "Runtime": "Kesto", "Season": "Kausi", "SeasonFolder": "Kausikohtaiset kansiot", @@ -612,7 +611,7 @@ "EditSeriesModalHeader": "Muokataan - {title}", "EnableInteractiveSearch": "Käytä manuaalihakuun", "EnableRssHelpText": "Käytetään {appName}in etsiessä julkaisuja ajoitetusti RSS-synkronoinnilla.", - "EnableSslHelpText": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", + "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeInfo": "Jakson tiedot", "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja", @@ -706,7 +705,7 @@ "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", "RemotePathMappings": "Etäsijaintien kohdistukset", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Etälataustyökalu \"{0}\" ilmoitti tiedostosijainniksi \"{1}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", @@ -817,7 +816,7 @@ "AnimeEpisodeTypeFormat": "Absoluuttinen jaksonumerointi ({format})", "AnimeEpisodeTypeDescription": "Jaksot julkaistaan absoluuttisella numeroinnilla.", "CalendarLegendEpisodeDownloadedTooltip": "Jakso on ladattu ja lajiteltu", - "BranchUpdate": "{appName}in versiopäivityksiin käytettävä kehityshaara.", + "BranchUpdate": "{appName}in versiopäivityksiin käytettävä kehityshaara", "CollapseMultipleEpisodesHelpText": "Tiivistä useat samana päivänä esitettävät jaksot.", "CalendarLegendSeriesFinaleTooltip": "Sarjan tai kauden päätösjakso", "CalendarLegendSeriesPremiereTooltip": "Sarjan tai kauden pilottijakso", @@ -841,7 +840,7 @@ "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa lataustyökalun \"{name}\"?", "DeleteSelectedDownloadClients": "Poista lataustyökalu(t)", "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) tietoläh(teen/dettä)?", - "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{customFormatName}\"?", + "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{name}\"?", "DeleteRemotePathMapping": "Poista etäsijainnin kohdistus", "DeleteSelectedImportLists": "Poista tuontilista(t)", "DetailedProgressBar": "Yksityiskohtainen tilapalkki", @@ -900,7 +899,7 @@ "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "HardlinkCopyFiles": "Hardlink/tiedostojen kopiointi", "ExternalUpdater": "{appName} on määritetty käyttämään ulkoista päivitysratkaisua.", - "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnista mihin sarjalle ja jaksolle julkaisu kuuluu, eikä sen automaattinen tuonti onnistu. Haluatko kaapata julkaisun \"{0}\"?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnista mihin sarjalle ja jaksolle julkaisu kuuluu, eikä sen automaattinen tuonti onnistu. Haluatko kaapata julkaisun \"{title}\"?", "FailedToUpdateSettings": "Asetusten päivitys epäonnistui", "Forums": "Keskustelualue", "ErrorLoadingPage": "Virhe ladattaessa sivua", @@ -946,7 +945,7 @@ "QualityCutoffNotMet": "Laadun katkaisutasoa ei ole saavutettu", "ProtocolHelpText": "Valitse käytettävä(t) protokolla(t) ja mitä käytetään ensisijaisesti valittaessa muutoin tasaveroisista julkaisuista.", "QualityDefinitionsLoadError": "Virhe ladattaessa laatumäärityksiä", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{0}\" tallentaa lataukset kohteeseen \"{1}\", mutta se ei ole kelvollinen {2}-sijainti. Tarkista lataustyökalun asetukset.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista lataustyökalun asetukset.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista lataustyökalun asetukset.", "RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon lataustyökalukohtaisiin asetuksiin.", "QualityProfile": "Laatuprofiili", @@ -1084,7 +1083,7 @@ "UtcAirDate": "UTC-esitysaika", "FileManagement": "Tiedostojen hallinta", "InteractiveImportNoEpisode": "Jokaiselle valitulle tiedostolle on valittava ainakin yksi jakso.", - "ApiKeyValidationHealthCheckMessage": "Muuta rajapinnan (API) avain ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa.", + "ApiKeyValidationHealthCheckMessage": "Muuta rajapinnan (API) avain ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa", "Conditions": "Ehdot", "MinimumCustomFormatScore": "Mukautetun muodon vähimmäispisteytys", "Period": "Piste", @@ -1122,7 +1121,7 @@ "RecyclingBinCleanup": "Roskakorin tyhjennys", "RecyclingBinCleanupHelpText": "Arvo \"0\" (nolla) poistaa automaattisen tyhjennyksen käytöstä.", "ReleaseSceneIndicatorAssumingScene": "Oletetuksena kohtausnumerointi.", - "ConditionUsingRegularExpressions": "Ehto vastaa säännöllisiä lausekkeita. Huomioi, että merkeillä \"\\^$.|?*+()[{\" on erityismerkityksiä ja ne on erotettava \"\\\"-merkillä.", + "ConditionUsingRegularExpressions": "Ehto vastaa säännöllisiä lausekkeita. Huomioi, että merkeillä `\\^$.|?*+()[{`on erityismerkityksiä ja ne on erotettava `\\`-merkillä", "CreateGroup": "Luo ryhmä", "Custom": "Mukautettu", "CustomFormatJson": "Mukautetun muodon JSON-koodi", @@ -1202,7 +1201,7 @@ "CountSelectedFile": "{selectedCount} tiedosto on valittu", "SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava", "Underscore": "Alaviiva", - "AllSeriesInRootFolderHaveBeenImported": "Kaikki sijainnin {path} sisältämät sarjat on tuotu.", + "AllSeriesInRootFolderHaveBeenImported": "Kaikki sijainnin {path} sisältämät sarjat on tuotu", "AlreadyInYourLibrary": "Kohde on jo krijastossasi", "Analytics": "Analytiikka", "AuthenticationRequired": "Vaadi tunnistautuminen", @@ -1226,7 +1225,7 @@ "DestinationRelativePath": "Kohde suhteessa polkuun", "Disabled": "Ei käytössä", "Dates": "Päiväykset", - "DeleteAutoTagHelpText": "Haluatko varmasti poistaa automaattitunnisteen '\"0}\"?", + "DeleteAutoTagHelpText": "Haluatko varmasti poistaa automaattitunnisteen '{name}'?", "DeleteAutoTag": "Poista automaattitunniste", "DotNetVersion": ".NET", "DownloadClientPneumaticSettingsStrmFolder": "Strm-kansio", @@ -1619,7 +1618,7 @@ "NotificationsPushBulletSettingsDeviceIds": "Laite-ID:t", "NotificationsKodiSettingsDisplayTime": "Näytä aika", "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", - "NotificationsSettingsUseSslHelpText": "Muodosta yhteys SSL-protokollan välityksellä.", + "NotificationsSettingsUseSslHelpText": "Muodosta yhteys sovellukseen {serviceName} SSL-protokollan välityksellä.", "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", "NotificationsSettingsUpdateMapPathsTo": "Kohdista sijainnit kohteeseen", "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 65046c53b..438e7cafe 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -109,13 +109,13 @@ "AutoRedownloadFailedHelpText": "Recherche automatique et tentative de téléchargement d'une version différente", "AutoTaggingLoadError": "Impossible de charger le balisage automatique", "AuthenticationRequiredWarning": "Pour empêcher l'accès à distance sans authentification, {appName} exige désormais que l'authentification soit activée. Vous pouvez éventuellement désactiver l'authentification pour les adresses locales.", - "BackupFolderHelpText": "Les chemins d'accès relatifs se trouvent dans le répertoire AppData de Sonarr", + "BackupFolderHelpText": "Les chemins d'accès relatifs se trouvent dans le répertoire AppData de {appName}", "AirDate": "Date de diffusion", "AllTitles": "Tous les titres", "AutoAdd": "Ajout automatique", "AutoTagging": "Balisage automatique", - "AutoTaggingNegateHelpText": "Si cette case est cochée, la règle de marquage automatique ne s'appliquera pas si la condition {implementationName} est remplie.", - "AutoTaggingRequiredHelpText": "Cette condition {implementationName} doit être remplie pour que la règle de marquage automatique s'applique. Dans le cas contraire, une seule correspondance {implementationName} suffit.", + "AutoTaggingNegateHelpText": "Si cette case est cochée, la règle de marquage automatique ne s'appliquera pas si cette condition {implementationName} correspond.", + "AutoTaggingRequiredHelpText": "Cette condition {implementationName} doit correspondre pour que la règle de marquage automatique s'applique. Sinon, une seule correspondance {implementationName} suffit.", "AllResultsAreHiddenByTheAppliedFilter": "Tous les résultats sont masqués par le filtre appliqué", "ApplyTagsHelpTextReplace": "Remplacer : remplace les étiquettes par les étiquettes renseignées (ne pas renseigner d'étiquette pour toutes les effacer)", "Agenda": "Agenda", @@ -150,30 +150,30 @@ "AnimeEpisodeTypeDescription": "Episodes diffusés en utilisant un numéro d'épisode absolu", "Any": "Tous", "AppUpdated": "{appName} mis à jour", - "AddListExclusionSeriesHelpText": "Empêcher les séries d'être ajoutées à Sonarr par des listes", + "AddListExclusionSeriesHelpText": "Empêcher les séries d'être ajoutées à {appName} par des listes", "AllSeriesAreHiddenByTheAppliedFilter": "Tous les résultats sont masqués par le filtre appliqué", - "AnalyseVideoFilesHelpText": "Extraire des fichiers des informations vidéo telles que la résolution, la durée d'exécution et le codec. Pour ce faire, Sonarr doit lire des parties du fichier, ce qui peut entraîner une activité élevée du disque ou du réseau pendant les analyses.", - "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs aux serveurs de Sonarr. Cela inclut des informations sur votre navigateur, les pages de l'interface Web de Sonarr que vous utilisez, les rapports d'erreurs ainsi que le système d'exploitation et la version d'exécution. Nous utiliserons ces informations pour prioriser les fonctionnalités et les corrections de bugs.", + "AnalyseVideoFilesHelpText": "Extraire des fichiers des informations vidéo telles que la résolution, la durée d'exécution et le codec. Pour ce faire, {appName} doit lire des parties du fichier, ce qui peut entraîner une activité élevée du disque ou du réseau pendant les analyses.", + "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs aux serveurs de {appName}. Cela inclut des informations sur votre navigateur, les pages de l'interface Web de {appName} que vous utilisez, les rapports d'erreurs ainsi que le système d'exploitation et la version d'exécution. Nous utiliserons ces informations pour prioriser les fonctionnalités et les corrections de bugs.", "AuthenticationMethodHelpTextWarning": "Veuillez choisir une méthode d'authentification valide", "AuthenticationRequiredHelpText": "Modifier les demandes pour lesquelles l'authentification est requise. Ne rien modifier si vous n'en comprenez pas les risques.", "AutomaticUpdatesDisabledDocker": "Les mises à jour automatiques ne sont pas directement prises en charge lors de l'utilisation du mécanisme de mise à jour de Docker. Vous devrez mettre à jour l'image du conteneur en dehors de {appName} ou utiliser un script", "BackupRetentionHelpText": "Les sauvegardes automatiques plus anciennes que la période de rétention seront nettoyées automatiquement", "QualityProfile": "Profil de qualité", - "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "Sonarr peut voir mais ne peut pas accéder à l'épisode téléchargé {path}. Probablement une erreur de permissions.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} peut voir mais ne peut pas accéder à l'épisode téléchargé {path}. Probablement une erreur de permissions.", "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement {downloadClientName} place les téléchargements dans {path}, mais ce répertoire ne semble pas exister dans le conteneur. Vérifiez vos mappages de chemins d'accès distants et les paramètres de volume du conteneur.", "BlocklistReleases": "Publications de la liste de blocage", "BindAddress": "Adresse de liaison", "BackupsLoadError": "Impossible de charger les sauvegardes", "BuiltIn": "Intégré", "BrowserReloadRequired": "Rechargement du navigateur requis", - "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score est supérieur au format personnalisé", + "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score du format personnalisé est supérieur", "CheckDownloadClientForDetails": "Pour plus de détails, consultez le client de téléchargement", "ChooseAnotherFolder": "Sélectionnez un autre dossier", "BlocklistLoadError": "Impossible de charger la liste de blocage", - "BranchUpdate": "Branche à utiliser pour mettre à jour Sonarr", + "BranchUpdate": "Branche à utiliser pour mettre à jour {appName}", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Score minimum pour le format personnalisé", "CalendarLoadError": "Impossible de charger le calendrier", - "BypassDelayIfAboveCustomFormatScoreHelpText": "Ignorer lorsque la version a un score supérieur au score minimum configuré pour le format personnalisé", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Activer le contournement lorsque la libération a un score supérieur au score minimum configuré pour le format personnalisé", "CertificateValidationHelpText": "Modifier le niveau de rigueur de la validation de la certification HTTPS. Ne pas modifier si vous ne maîtrisez pas les risques.", "Certification": "Certification", "ChangeFileDateHelpText": "Modifier la date du fichier lors de l'importation/la réanalyse", @@ -208,7 +208,7 @@ "RemotePathMappingBadDockerPathHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement {downloadClientName} place les téléchargements dans {path} mais ce n'est pas un chemin {osName} valide. Revoyez vos mappages de chemins d'accès distants et les paramètres du client de téléchargement.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Le client de téléchargement local {downloadClientName} a signalé des fichiers dans {path}, mais il ne s'agit pas d'un chemin {osName} valide. Vérifiez les paramètres de votre client de téléchargement.", "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Le client de téléchargement distant {downloadClientName} a signalé des fichiers dans {path}, mais il ne s'agit pas d'un chemin {osName} valide. Revoyez vos mappages de chemins d'accès distants et les paramètres du client de téléchargement.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "Sonarr peut voir mais ne peut pas accéder au répertoire de téléchargement {downloadPath}. Il s'agit probablement d'une erreur de permissions.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} peut voir mais ne peut pas accéder au répertoire de téléchargement {downloadPath}. Il s'agit probablement d'une erreur de permissions.", "Path": "Chemin", "QueueIsEmpty": "La file d'attente est vide", "Warn": "Avertissement", @@ -243,7 +243,7 @@ "Edit": "Modifier", "RemoveSelectedItem": "Supprimer l'élément sélectionné", "SubtitleLanguages": "Langues des sous-titres", - "Clone": "Cloner", + "Clone": "Dupliquer", "ColonReplacementFormatHelpText": "Changer la manière dont {appName} remplace les « deux-points »", "DefaultCase": "Casse par défaut", "Delete": "Supprimer", @@ -304,7 +304,7 @@ "NoIndexersFound": "Aucun indexeur n'a été trouvé", "Profiles": "Profils", "Dash": "Tiret", - "DelayProfileProtocol": "Protocole : {preferredProtocol}", + "DelayProfileProtocol": "Protocole: {preferredProtocol}", "DeleteBackupMessageText": "Voulez-vous supprimer la sauvegarde « {name} » ?", "DeleteConditionMessageText": "Voulez-vous vraiment supprimer la condition « {name} » ?", "DeleteCondition": "Supprimer la condition", @@ -322,7 +322,7 @@ "Host": "Hôte", "ICalIncludeUnmonitoredEpisodesHelpText": "Inclure les épisodes non surveillés dans le flux iCal", "RenameEpisodesHelpText": "{appName} utilisera le nom de fichier existant si le changement de nom est désactivé", - "RestartRequiredToApplyChanges": "{appName} nécessite un redémarrage pour appliquer les modifications. Voulez-vous redémarrer maintenant ?", + "RestartRequiredToApplyChanges": "{appName} nécessite un redémarrage pour appliquer les changements, voulez-vous redémarrer maintenant ?", "OrganizeRenamingDisabled": "Le renommage est désactivé, rien à renommer", "PendingChangesStayReview": "Rester et vérifier les modifications", "PendingChangesMessage": "Vous avez des modifications non sauvegardées, voulez-vous vraiment quitter cette page ?", @@ -378,7 +378,7 @@ "OneSeason": "1 saison", "Ok": "OK", "PendingChangesDiscardChanges": "Abandonner les modifications et quitter", - "PreferProtocol": "Préféré {preferredProtocol}", + "PreferProtocol": "Préférer {preferredProtocol}", "Refresh": "Rafraîchir", "PrefixedRange": "Plage préfixée", "PreferredProtocol": "Protocole préféré", @@ -451,7 +451,7 @@ "PrioritySettings": "Priorité : {priority}", "ImportExistingSeries": "Importer une série existante", "RootFolderSelectFreeSpace": "{freeSpace} Libre", - "WantMoreControlAddACustomFormat": "Vous voulez plus de contrôle sur les téléchargements préférés ? Ajouter un [Format Personnalisé](/settings/customformats)", + "WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)", "RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?", "UpdateAll": "Tout actualiser", "EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif", @@ -576,7 +576,7 @@ "MaximumSize": "Taille maximum", "Mechanism": "Mécanisme", "MediaInfo": "Informations médias", - "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages supporte un suffixe `:EN+DE` vous permettant de filtrer les langues incluses dans le nom de fichier. Utilisez `-DE` pour exclure des langues spécifiques. L'ajout de `+` (par exemple `:EN+`) affichera `[EN]`/`[EN+--]`/`[--]` en fonction des langues exclues. Par exemple `{MediaInfo Full:EN+DE}`.", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages supporte un suffixe `:EN+DE` vous permettant de filtrer les langues incluses dans le nom de fichier. Utilisez `-DE` pour exclure des langues spécifiques. En ajoutant `+` (par exemple `:EN+`), vous obtiendrez `[EN]`/`[EN+--]`/`[--]` en fonction des langues exclues. Par exemple `{MediaInfo Full:EN+DE}`.", "MetadataProvidedBy": "Les métadonnées sont fournies par {provider}", "MetadataSettings": "Paramètres des métadonnées", "MetadataSettingsSeriesSummary": "Créez des fichiers de métadonnées lorsque les épisodes sont importés ou que les séries sont actualisées", @@ -601,14 +601,14 @@ "MustNotContainHelpText": "La version sera rejetée si elle contient un ou plusieurs termes (insensible à la casse)", "NamingSettings": "Paramètres de dénomination", "Negate": "Nier", - "NegateHelpText": "Si cette case est cochée, le format personnalisé ne s'appliquera pas si cette condition {implementationName} correspond.", + "NegateHelpText": "Si coché, le format personnalisé ne s'appliquera pas si cette condition {implementationName} correspond.", "Negated": "Nier", "Network": "Réseau", "Never": "Jamais", "New": "Nouveau", "NextExecution": "Prochaine exécution", "NoChange": "Pas de changement", - "NoDelay": "Sans délais", + "NoDelay": "Pas de délai", "NoEpisodeHistory": "Pas d'historique des épisodes", "NoEpisodesInThisSeason": "Aucun épisode dans cette saison", "NoEventsFound": "Aucun événement trouvé", @@ -621,7 +621,7 @@ "Organize": "Organiser", "OrganizeLoadError": "Erreur lors du chargement des aperçus", "OrganizeModalHeader": "Organiser et renommer", - "OrganizeModalHeaderSeason": "Organiser et renommer – {saison}", + "OrganizeModalHeaderSeason": "Organiser et renommer – {season}", "OrganizeSelectedSeriesModalAlert": "Astuce : Pour prévisualiser un changement de nom, sélectionnez \"Annuler\", puis sélectionnez n'importe quel titre de série et utilisez cette icône :", "OrganizeSelectedSeriesModalConfirmation": "Voulez-vous vraiment organiser tous les fichiers des {count} séries sélectionnées ?", "OrganizeSelectedSeriesModalHeader": "Organiser les séries sélectionnées", @@ -651,10 +651,10 @@ "RelativePath": "Chemin relatif", "Release": "Version", "ReleaseGroup": "Groupe de versions", - "ReleaseGroups": "Groupes de versions", + "ReleaseGroups": "Groupes de version", "ReleaseHash": "Somme de contrôle de la version", "ReleaseProfile": "Profil de version", - "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut conduire à la saisie de versions en double", + "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double.", "ReleaseProfiles": "Profils de version", "ReleaseProfilesLoadError": "Impossible de charger les profils de version", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Le client de téléchargement {downloadClientName} place les téléchargements dans {path} mais {appName} ne peut pas voir ce répertoire. Vous devrez peut-être ajuster les autorisations du dossier.", @@ -934,7 +934,7 @@ "OnGrab": "À saisir", "OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse", "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici]({url}).", - "ReleaseProfileIndexerHelpText": "Spécifiez à quel indexeur le profil s'applique", + "ReleaseProfileIndexerHelpText": "Spécifier l'indexeur auquel le profil s'applique", "RemotePathMappings": "Mappages de chemins distants", "RescanAfterRefreshHelpTextWarning": "{appName} ne détectera pas automatiquement les modifications apportées aux fichiers lorsqu'il n'est pas défini sur 'Toujours'", "SingleEpisode": "Épisode unique", @@ -988,13 +988,13 @@ "Min": "Min", "MinimumAge": "Âge minimum", "MinimumAgeHelpText": "Usenet uniquement : âge minimum en minutes des NZB avant leur saisie. Utilisez-le pour donner aux nouvelles versions le temps de se propager à votre fournisseur Usenet.", - "MinutesSixty": "60 Minutes : {sixty}", + "MinutesSixty": "60 Minutes : {sixty}", "MonitoredOnly": "Surveillé uniquement", "MoveSeriesFoldersDontMoveFiles": "Non, je déplacerai les fichiers moi-même", "MoveSeriesFoldersMoveFiles": "Oui, déplacez les fichiers", "MoveSeriesFoldersToNewPath": "Souhaitez-vous déplacer les fichiers de la série de « {originalPath} » vers « {destinationPath} » ?", - "MoveSeriesFoldersToRootFolder": "Souhaitez-vous déplacer les dossiers de la série vers « {DestinationRootFolder} » ?", - "MustContainHelpText": "Le communiqué doit contenir au moins un de ces termes (insensible à la casse)", + "MoveSeriesFoldersToRootFolder": "Souhaitez-vous déplacer les dossiers de la série vers « {destinationRootFolder} » ?", + "MustContainHelpText": "La version doit contenir au moins l'un des termes suivants (insensible à la casse)", "MustNotContain": "Ne doit pas contenir", "NamingSettingsLoadError": "Impossible de charger les paramètres de dénomination", "NoEpisodeInformation": "Aucune information sur l'épisode n'est disponible.", @@ -1054,7 +1054,7 @@ "Level": "Niveau", "LibraryImport": "Importer biblio.", "ListExclusionsLoadError": "Impossible de charger les exclusions de liste", - "ListQualityProfileHelpText": "Les éléments de la liste des profils de qualité seront ajoutés avec", + "ListQualityProfileHelpText": "Les éléments de la liste du profil de qualité seront ajoutés avec", "ListTagsHelpText": "Balises qui seront ajoutées lors de l'importation à partir de cette liste", "LocalAirDate": "Date de diffusion locale", "Location": "Emplacement", @@ -1067,7 +1067,7 @@ "MultiEpisode": "Multi-épisode", "MultiEpisodeInvalidFormat": "Épisode multiple : format invalide", "NoEpisodeOverview": "Aucun aperçu des épisodes", - "OneMinute": "1 Minute", + "OneMinute": "1 minute", "OriginalLanguage": "Langue originale", "Port": "Port", "PreferTorrent": "Préféré Torrent", @@ -1108,7 +1108,7 @@ "LibraryImportTipsSeriesUseRootFolder": "Pointez {appName} vers le dossier contenant toutes vos émissions de télévision, pas une en particulier. par exemple. \"`{goodFolderExample}`\" et non \"`{badFolderExample}`\". De plus, chaque série doit se trouver dans son propre dossier dans le dossier racine/bibliothèque.", "Links": "Liens", "ListOptionsLoadError": "Impossible de charger les options de la liste", - "ListRootFolderHelpText": "Les éléments de la liste du dossier racine seront ajoutés à", + "ListRootFolderHelpText": "Les éléments de la liste du dossier racine seront ajoutés à la liste des dossiers racine", "MinutesThirty": "30 Minutes : {thirty}", "Missing": "Manquant", "MissingEpisodes": "Épisodes manquants", @@ -1152,11 +1152,11 @@ "RemovedFromTaskQueue": "Supprimé de la file d'attente des tâches", "RemovedSeriesSingleRemovedHealthCheckMessage": "La série {series} a été supprimée de TheTVDB", "Reorder": "Réorganiser", - "Repack": "Remballer", + "Repack": "Repack", "RequiredHelpText": "Cette condition {implementationName} doit correspondre pour que le format personnalisé s'applique. Sinon, une seule correspondance {implementationName} suffit.", "RescanSeriesFolderAfterRefresh": "Réanalyser le dossier de la série après l'actualisation", "ResetAPIKey": "Réinitialiser la clé API", - "RestartRequiredWindowsService": "Selon l'utilisateur qui exécute le service {appName}, vous devrez peut-être redémarrer {appName} en tant qu'administrateur une fois avant que le service ne démarre automatiquement.", + "RestartRequiredWindowsService": "En fonction de l'utilisateur qui exécute le service {appName}, vous devrez peut-être redémarrer {appName} en tant qu'administrateur une fois avant que le service ne démarre automatiquement.", "Restore": "Restaurer", "RestoreBackup": "Restaurer la sauvegarde", "RestrictionsLoadError": "Impossible de charger les restrictions", @@ -1174,7 +1174,7 @@ "StartImport": "Démarrer l'importation", "Started": "Démarré", "StartupDirectory": "Répertoire de démarrage", - "SupportedAutoTaggingProperties": "{appName} prend en charge les propriétés suivantes pour les règles de marquage automatique", + "SupportedAutoTaggingProperties": "{appName} prend en charge les propriétés suivantes pour les règles d'étiquetage automatique", "ToggleUnmonitoredToMonitored": "Non surveillé, cliquez pour surveiller", "Torrents": "Torrents", "Total": "Total", @@ -1195,7 +1195,7 @@ "InstanceName": "Nom de l'instance", "InteractiveImportLoadError": "Impossible de charger les éléments d'importation manuelle", "InteractiveImportNoEpisode": "Un ou plusieurs épisodes doivent être choisis pour chaque fichier sélectionné", - "MappedNetworkDrivesWindowsService": "Les lecteurs réseau mappés ne sont pas disponibles lors de l'exécution en tant que service Windows, consultez la [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote -serveur) pour plus d'informations.", + "MappedNetworkDrivesWindowsService": "Les lecteurs réseau mappés ne sont pas disponibles lors de l'exécution en tant que service Windows, consultez la [FAQ]({url}) pour plus d'informations.", "SelectReleaseGroup": "Sélectionnez un groupe de versions", "Tomorrow": "Demain", "OverrideGrabNoSeries": "La série doit être sélectionnée", @@ -1206,7 +1206,7 @@ "ReleaseSceneIndicatorAssumingScene": "En supposant la numérotation des scènes.", "ReleaseSceneIndicatorAssumingTvdb": "En supposant la numérotation TVDB.", "ReleaseSceneIndicatorMappedNotRequested": "L'épisode mappé n'a pas été demandé dans cette recherche.", - "SearchForQuery": "Rechercher {requête}", + "SearchForQuery": "Rechercher {query}", "View": "Vues", "HardlinkCopyFiles": "Lien physique/Copie de fichiers", "Health": "Santé", @@ -1251,7 +1251,7 @@ "DelayProfileSeriesTagsHelpText": "S'applique aux séries avec au moins une balise correspondante", "DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}", "DeletedReasonManual": "Le fichier a été supprimé via l'interface utilisateur", - "DeleteRemotePathMapping": "Supprimer le mappage de chemin distant", + "DeleteRemotePathMapping": "Supprimer la correspondance de chemin distant", "DestinationPath": "Chemin de destination", "DestinationRelativePath": "Chemin relatif de destination", "DownloadClientRootFolderHealthCheckMessage": "Le client de téléchargement {downloadClientName} place les téléchargements dans le dossier racine {rootFolderPath}. Vous ne devez pas télécharger vers un dossier racine.", @@ -1263,7 +1263,7 @@ "EditCustomFormat": "Modifier le format personnalisé", "Downloading": "Téléchargement", "EditListExclusion": "Modifier l'exclusion de liste", - "EditMetadata": "Modifier les métadonnées {metadataType}", + "EditMetadata": "Modifier {metadataType} Métadonnée", "EnableAutomaticAdd": "Activer l'ajout automatique", "EpisodeFileDeleted": "Fichier de l'épisode supprimé", "EpisodeFileDeletedTooltip": "Fichier de l'épisode supprimé", @@ -1277,7 +1277,7 @@ "Component": "Composant", "Condition": "Condition", "Connections": "Connexions", - "ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs multimédias et scripts personnalisés", + "ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs de médias et scripts personnalisés", "CopyToClipboard": "Copier dans le presse-papier", "CreateEmptySeriesFolders": "Créer des dossiers de séries vides", "Custom": "Customisé", @@ -1292,7 +1292,7 @@ "DeleteImportListExclusionMessageText": "Êtes-vous sûr de vouloir supprimer cette exclusion de la liste d'importation ?", "DeleteQualityProfile": "Supprimer le profil de qualité", "DeleteReleaseProfile": "Supprimer le profil de version", - "DeleteRemotePathMappingMessageText": "Êtes-vous sûr de vouloir supprimer ce mappage de chemin distant ?", + "DeleteRemotePathMappingMessageText": "Êtes-vous sûr de vouloir supprimer cette correspondance de chemin distant ?", "DoNotPrefer": "Ne préfère pas", "DoNotUpgradeAutomatically": "Ne pas mettre à niveau automatiquement", "DownloadClient": "Client de téléchargement", @@ -1340,17 +1340,17 @@ "Database": "Base de données", "Dates": "Dates", "CustomFormatJson": "Format personnalisé JSON", - "DelayMinutes": "{delay} Minutes", + "DelayMinutes": "{delay} minutes", "DelayProfile": "Profil de retard", "DeleteDelayProfile": "Supprimer le profil de retard", "DeleteDelayProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de retard ?", "DeleteEpisodeFile": "Supprimer le fichier de l'épisode", - "DeleteEpisodeFileMessage": "Supprimer le fichier de l'épisode ?", + "DeleteEpisodeFileMessage": "Supprimer le fichier de l'épisode '{path}'?", "DeleteEpisodeFromDisk": "Supprimer l'épisode du disque", - "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer cette exclusion de la liste d'importation ?", + "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer la liste « {name} » ?", "DeleteSelectedEpisodeFiles": "Supprimer les fichiers d'épisode sélectionnés", "DeleteSelectedEpisodeFilesHelpText": "Êtes-vous sûr de vouloir supprimer les fichiers d'épisode sélectionnés ?", - "DeleteSpecificationHelpText": "Êtes-vous sûr de vouloir supprimer la spécification « {name} » ?", + "DeleteSpecificationHelpText": "Êtes-vous sûr de vouloir supprimer la spécification '{name}' ?", "DeleteTag": "Supprimer l'étiquette", "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de téléchargement indisponibles en raison d'échecs : {downloadClientNames}", "DownloadClientStatusAllClientHealthCheckMessage": "Tous les clients de téléchargement sont indisponibles en raison d'échecs", @@ -1390,7 +1390,7 @@ "CustomFilters": "Filtres personnalisés", "CustomFormat": "Format personnalisé", "CustomFormatHelpText": "{appName} attribue un score pour chaque release en additionnant les scores des formats personnalisés correspondants. Si une nouvelle release permet d'améliorer le score, pour une qualité identique ou supérieure, alors {appName} la téléchargera.", - "CustomFormatUnknownCondition": "Condition de format personnalisé inconnue '{implémentation}'", + "CustomFormatUnknownCondition": "Condition de format personnalisé inconnue '{implementation}'", "CustomFormatUnknownConditionOption": "Option inconnue '{key}' pour la condition '{implementation}'", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Le client de téléchargement {downloadClientName} est configuré pour supprimer les téléchargements terminés. Cela peut entraîner la suppression des téléchargements de votre client avant que {appName} puisse les importer.", "DownloadFailed": "Échec du téléchargement", @@ -1401,7 +1401,7 @@ "EnableHelpText": "Activer la création de fichiers de métadonnées pour ce type de métadonnées", "EnableInteractiveSearchHelpText": "Sera utilisé lorsque la recherche interactive est utilisée", "EnableInteractiveSearchHelpTextWarning": "La recherche n'est pas prise en charge avec cet indexeur", - "EnableProfileHelpText": "Cochez pour activer le profil de version", + "EnableProfileHelpText": "Vérifier pour activer le profil de version", "EnableRss": "Activer RSS", "Ended": "Terminé", "EndedOnly": "Terminé seulement", @@ -1444,15 +1444,15 @@ "DeleteEmptySeriesFoldersHelpText": "Supprimez les dossiers de séries et de saisons vides lors de l'analyse du disque et lorsque les fichiers d'épisode sont supprimés", "DeleteEpisodesFiles": "Supprimer {episodeFileCount} fichiers d'épisode", "DeleteEpisodesFilesHelpText": "Supprimer les fichiers d'épisode et le dossier de série", - "DeleteQualityProfileMessageText": "Êtes-vous sûr de vouloir supprimer le profil de qualité « {name} » ?", - "DeleteReleaseProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de version « {name} » ?", + "DeleteQualityProfileMessageText": "Êtes-vous sûr de vouloir supprimer le profil de qualité \"{name}\" ?", + "DeleteReleaseProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de version '{name}' ?", "DeleteSelectedSeries": "Supprimer la série sélectionnée", "DeleteSeriesFolder": "Supprimer le dossier de série", "DeleteSeriesFolderCountConfirmation": "Voulez-vous vraiment supprimer {count} séries sélectionnées ?", "DeleteSeriesFolderCountWithFilesConfirmation": "Voulez-vous vraiment supprimer {count} séries sélectionnées et tous les contenus ?", - "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} fichiers d'épisode totalisant {taille}", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} fichiers d'épisode totalisant {size}", "DeleteSeriesFoldersHelpText": "Supprimez les dossiers de séries et tout leur contenu", - "DeleteSeriesModalHeader": "Supprimer - {titre}", + "DeleteSeriesModalHeader": "Supprimer - {title}", "DeletedReasonUpgrade": "Le fichier a été supprimé pour importer une mise à niveau", "DeletedSeriesDescription": "La série a été supprimée de TheTVDB", "DetailedProgressBar": "Barre de progression détaillée", @@ -1464,7 +1464,7 @@ "DownloadClientSettings": "Télécharger les paramètres client", "EditRestriction": "Modifier la restriction", "EditSelectedSeries": "Modifier la série sélectionnée", - "EditSeriesModalHeader": "Modifier - {titre}", + "EditSeriesModalHeader": "Modifier - {title}", "Enable": "Activer", "Error": "Erreur", "ErrorLoadingContents": "Erreur lors du chargement du contenu", @@ -1486,13 +1486,13 @@ "IndexerValidationCloudFlareCaptchaRequired": "Site protégé par le CAPTCHA CloudFlare. Un jeton CAPTCHA valide est nécessaire.", "IndexerValidationTestAbortedDueToError": "Le test a été abandonné à cause d'un erreur : {exceptionMessage}", "TorrentBlackholeSaveMagnetFilesReadOnly": "Lecture seule", - "DownloadClientFloodSettingsPostImportTagsHelpText": "Ajouter les étiquettes après qu'un téléchargement est importé.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Ajoute des balises après l'importation d'un téléchargement.", "DownloadClientFreeboxSettingsAppId": "ID de l'application", - "DownloadClientFreeboxSettingsAppIdHelpText": "L'ID de l'application donné lors de la création de l'accès à l'API Freebox (c'est-à-dire « app_id »)", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Cette option exige au moins la version 16.0 de NzbGet", - "DownloadStationStatusExtracting": "Extraction : {progress} %", - "IndexerHDBitsSettingsCodecsHelpText": "Si non renseigné, toutes les options sont utilisées.", - "IndexerHDBitsSettingsMediumsHelpText": "Si non renseigné, toutes les options sont utilisées.", + "DownloadClientFreeboxSettingsAppIdHelpText": "L'ID de l'application donné lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_id')", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Cette option nécessite au moins la version 16.0 de NzbGet", + "DownloadStationStatusExtracting": "Extraction : {progress}%", + "IndexerHDBitsSettingsCodecsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", + "IndexerHDBitsSettingsMediumsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", "IndexerSettingsAdditionalParametersNyaa": "Paramètres supplémentaires", "IndexerSettingsAnimeCategories": "Catégories Anime", "IndexerSettingsApiUrl": "URL de l'API", @@ -1506,25 +1506,25 @@ "IndexerValidationInvalidApiKey": "Clé API invalide", "IndexerValidationUnableToConnect": "Impossible de se connecter à l'indexeur : {exceptionMessage}. Vérifiez le journal pour plus de détails sur cette erreur", "IndexerValidationRequestLimitReached": "Limite de requêtes atteinte : {exceptionMessage}", - "IndexerValidationUnableToConnectHttpError": "Impossible de se connecter à l'indexeur, vérifiez vos paramètres DNS est assurez-vous que l'IPv6 fonctionne ou est désactivé.", - "TorrentBlackholeSaveMagnetFiles": "Sauvegarder les fichiers Magnet", + "IndexerValidationUnableToConnectHttpError": "Impossible de se connecter à l'indexeur, vérifiez vos paramètres DNS est assurez-vous que l'IPv6 fonctionne ou est désactivé. {exceptionMessage}.", + "TorrentBlackholeSaveMagnetFiles": "Enregistrer les fichiers magnétiques", "Category": "Catégorie", - "Destination": "Destination", - "Directory": "Répertoire", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL du JSON de Deluge, voir {url}", - "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} n'a pas pu ajouter les étiquettes à {clientName}.", + "Destination": "Cible", + "Directory": "Dossier", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL json du déluge, voir {url}", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} n'a pas pu ajouter le libellé à {clientName}.", "DownloadClientDelugeTorrentStateError": "Deluge signale une erreur", - "DownloadClientDelugeValidationLabelPluginFailure": "La configuration des étiquettes a échoué", + "DownloadClientDelugeValidationLabelPluginFailure": "La configuration de l'étiquette a échoué", "DownloadClientDownloadStationValidationFolderMissing": "Le dossier n'existe pas", - "DownloadClientDownloadStationValidationNoDefaultDestination": "Aucune destination par défaut", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Pas de destination par défaut", "DownloadClientDownloadStationValidationSharedFolderMissing": "Le dossier partagé n'existe pas", - "DownloadClientFloodSettingsAdditionalTags": "Étiquettes supplémentaires", - "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation n'a pas de dossier partagé avec le nom « {sharedFolder} », êtes-vous sûr de l'avoir correctement indiqué ?", - "DownloadClientFreeboxApiError": "L'API Freebox a retourné l'erreur : {errorDescription}", + "DownloadClientFloodSettingsAdditionalTags": "Étiquette supplémentaire", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Le poste de travail n'a pas de dossier partagé portant le nom '{sharedFolder}', êtes-vous sûr de l'avoir spécifié correctement ?", + "DownloadClientFreeboxApiError": "L'API Freebox a renvoyé une erreur : {errorDescription}", "DownloadClientFreeboxNotLoggedIn": "Non connecté", - "DownloadClientFreeboxSettingsHostHelpText": "Nom de l'hôte ou adresse IP de l'hôte de la Freebox, par défaut à « {url} » (ne fonctionne que si sur le même réseau)", - "DownloadClientFreeboxUnableToReachFreeboxApi": "Impossible de contacter l'API Freebox. Vérifiez le paramètre « URL de l'API » pour l'URL de base et la version.", - "DownloadClientVuzeValidationErrorVersion": "Version du protocole non pris en charge, utilisez Vuze 5.0.0.0 ou version ultérieure avec le plugin Vuze Web Remote.", + "DownloadClientFreeboxSettingsHostHelpText": "Nom d'hôte ou adresse IP de la Freebox, par défaut '{url}' (ne fonctionnera que si elle est sur le même réseau)", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Impossible d'accéder à l'API Freebox. Vérifiez le paramètre 'API URL' pour l'URL de base et la version.", + "DownloadClientVuzeValidationErrorVersion": "Version du protocole non prise en charge, utilisez Vuze 5.0.0.0 ou une version plus récente avec le plugin Vuze Web Remote.", "IndexerHDBitsSettingsCodecs": "Codecs", "IndexerHDBitsSettingsCategories": "Catégories", "IndexerSettingsAdditionalParameters": "Paramètres supplémentaires", @@ -1532,15 +1532,15 @@ "IndexerValidationFeedNotSupported": "Le flux de l'indexeur n'est pas pris en charge : {exceptionMessage}", "IndexerValidationUnableToConnectInvalidCredentials": "Impossible de se connecter à l'indexeur, identifiants invalides. {exceptionMessage}.", "IndexerHDBitsSettingsCategoriesHelpText": "Si non renseigné, toutes les options sont utilisées.", - "IndexerSettingsSeedTimeHelpText": "Le temps qu'un torrent doit être seedé avant d'arrêter, laissez vide pour utiliser la valeur du client de téléchargement par défaut", - "DownloadClientFloodSettingsUrlBaseHelpText": "Ajoute un préfixe à l'API Flood, tel que {url}", + "IndexerSettingsSeedTimeHelpText": "Durée pendant laquelle un torrent doit être envoyé avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement", + "DownloadClientFloodSettingsUrlBaseHelpText": "Ajoute d'un préfixe à l'API Flood, tel que {url}", "DownloadClientFreeboxAuthenticationError": "L'authentification à l'API Freebox a échoué. Raison : {errorDescription}", - "DownloadClientFreeboxSettingsApiUrl": "URL de l'API", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Définissez l'URL de base de l'API Freebox avec la version de l'API, par ex. « {url} » est par défaut à « {defaultApiUrl} »", - "DownloadClientFreeboxSettingsAppToken": "Jeton de l'application", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Le jeton de l'application récupéré lors de la création de l'accès à l'API Freebox (c'est-à-dire « app_token »)", - "DownloadClientFreeboxSettingsPortHelpText": "Port utilisé pour accéder à l'interface de la Freebox, par défaut à « {port} »", - "DownloadClientFreeboxUnableToReachFreebox": "Impossible de contacter l'API Freebox. Vérifiez les paramètres « Hôte », « Port » ou « Utiliser SSL ». (Erreur : {exceptionMessage})", + "DownloadClientFreeboxSettingsApiUrl": "URL DE L'API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Définir l'URL de base de l'API Freebox avec la version de l'API, par exemple '{url}', par défaut '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppToken": "Jeton d'application", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Le jeton de l'application récupéré lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_token')", + "DownloadClientFreeboxSettingsPortHelpText": "Port utilisé pour accéder à l'interface de la Freebox, la valeur par défaut est '{port}'", + "DownloadClientFreeboxUnableToReachFreebox": "Impossible d'accéder à l'API Freebox. Vérifiez les paramètres 'Host', 'Port' ou 'Use SSL'. (Erreur : {exceptionMessage})", "MonitorAllSeasons": "Toutes les saisons", "MonitorAllSeasonsDescription": "Surveiller automatiquement toutes les nouvelles saisons", "MonitorLastSeason": "Dernière saison", @@ -1555,81 +1555,81 @@ "PasswordConfirmation": "Confirmation du mot de passe", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmer le nouveau mot de passe", "MonitorNoNewSeasons": "Aucune nouvelle saison", - "DownloadClientFloodSettingsTagsHelpText": "Étiquettes initiales d'un téléchargement. Pour être reconnu, un téléchargement doit avoir toutes les étiquettes initiales. Cela évite les conflits avec des téléchargements non liés.", + "DownloadClientFloodSettingsTagsHelpText": "Étiquettes initiales d'un téléchargement. Pour être reconnu, un téléchargement doit avoir toutes les étiquettes initiales. Cela permet d'éviter les conflits avec des téléchargements non apparentés.", "DownloadClientQbittorrentValidationCategoryAddFailure": "La configuration de la catégorie a échoué", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Les catégories ne sont pas prises en charge avant la version 3.3.0 de qBittorrent. Veuillez effectuer une mise à niveau ou réessayez avec une catégorie vide.", - "DownloadClientRTorrentProviderMessage": "rTorrent ne mettra pas en pause les torrents lorsqu'ils atteindront les critères de partage. {appName} se chargera de la suppression automatique des torrents en fonction des critères de partage actuels dans Paramètres -> Indexeurs uniquement lorsque la suppression des téléchargements terminés est activée. Après l'importation, il définira également {importedView} en tant que vue rTorrent, qui peut être utilisée dans les scripts rTorrent pour personnaliser le comportement.", - "DownloadClientSettingsCategorySubFolderHelpText": "L'ajout d'une catégorie spécifique à {appName} évite les conflits avec des téléchargements non liés à {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée. Cela crée un sous-répertoire [catégorie] dans le répertoire de sortie.", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Les catégories ne sont pas prises en charge avant la version 3.3.0 de qBittorrent. Veuillez effectuer une mise à niveau ou réessayer avec une catégorie vide.", + "DownloadClientRTorrentProviderMessage": "rTorrent ne mettra pas les torrents en pause lorsqu'ils répondent aux critères d'ensemencement. {appName} traitera la suppression automatique des torrents en fonction des critères d'ensemencement actuels dans Paramètres->Indexeurs uniquement lorsque l'option Supprimer terminé est activée. Après l'importation, il définira également {importedView} comme une vue rTorrent, qui peut être utilisée dans les scripts rTorrent pour personnaliser le comportement.", + "DownloadClientSettingsCategorySubFolderHelpText": "L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée. Crée un sous-répertoire [catégorie] dans le répertoire de sortie.", "IndexerValidationQuerySeasonEpisodesNotSupported": "L'indexeur ne prend pas en charge la requête actuelle. Vérifiez si les catégories et/ou la recherche de saisons/épisodes sont prises en charge. Consultez le journal pour plus de détails.", "MonitorNewItems": "Surveiller les nouveaux éléments", "UsenetBlackholeNzbFolder": "Dossier Nzb", - "IndexerSettingsApiPath": "Chemin de l'API", - "IndexerSettingsSeedTime": "Temps de partage", - "IndexerSettingsSeedRatio": "Ratio de partage", - "IndexerSettingsSeedRatioHelpText": "Le ratio que doit atteindre un torrent avant de s'arrêter, laisser vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles de l'indexeur", + "IndexerSettingsApiPath": "Chemin d'accès à l'API", + "IndexerSettingsSeedTime": "Temps d'envoie", + "IndexerSettingsSeedRatio": "Ratio d'envoie", + "IndexerSettingsSeedRatioHelpText": "Le ratio qu'un torrent doit atteindre avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles des indexeurs", "IndexerValidationNoRssFeedQueryAvailable": "Aucune requête de flux RSS disponible. Cela peut être un problème avec l'indexeur ou vos paramètres de catégorie de l'indexeur.", "IndexerValidationSearchParametersNotSupported": "L'indexeur ne prend pas en charge les paramètres de recherche requis", "IndexerValidationUnableToConnectResolutionFailure": "Impossible de se connecter à l'indexeur : échec de la connexion. Vérifiez votre connexion au serveur de l'indexeur et le DNS. {exceptionMessage}.", - "TorrentBlackhole": "Torrent Blackhole", + "TorrentBlackhole": "Trou noir des torrents", "UseSsl": "Utiliser SSL", "UsenetBlackhole": "Usenet Blackhole", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Vous devez vous connecter à votre Diskstation en tant que {username} et le configurer manuellement dans les paramètres de DownloadStation sous BT/HTTP/FTP/NZB -> Emplacement.", - "DownloadClientFloodSettingsPostImportTags": "Étiquettes après importation", - "DownloadClientFloodSettingsRemovalInfo": "{appName} se chargera de la suppression automatique des torrents en fonction des critères de partage actuels dans Paramètres -> Indexeurs", - "DownloadClientFloodSettingsStartOnAdd": "Commencer lors de l'ajout", - "DownloadClientNzbVortexMultipleFilesMessage": "Le téléchargement contient plusieurs fichiers et n'est pas dans un dossier de tâche : {outputPath}", - "DownloadClientNzbgetValidationKeepHistoryOverMax": "Le paramètre 'KeepHistory' de NzbGet devrait être inférieur à 25000", - "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Le paramètre 'KeepHistory' de NzbGet est défini trop élevé.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Vous devez vous connecter à votre poste de travail en tant que {username} et le configurer manuellement dans les paramètres de la DownloadStation sous BT/HTTP/FTP/NZB -> Location.", + "DownloadClientFloodSettingsPostImportTags": "Balises post-importation", + "DownloadClientFloodSettingsRemovalInfo": "{appName} gérera la suppression automatique des torrents sur la base des critères de semences actuels dans Paramètres -> Indexeurs", + "DownloadClientFloodSettingsStartOnAdd": "Démarrer l'ajout", + "DownloadClientNzbVortexMultipleFilesMessage": "Le téléchargement contient plusieurs fichiers et ne se trouve pas dans un dossier de travail : {outputPath}", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "Le paramètre KeepHistory de NzbGet doit être inférieur à 25000", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Le paramètre KeepHistory de NzbGet est trop élevé.", "DownloadClientPneumaticSettingsNzbFolder": "Dossier Nzb", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Ce dossier devra être accessible depuis XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Dossier Strm", - "DownloadClientPneumaticSettingsStrmFolderHelpText": "Les fichiers .strm dans ce dossier seront importés par Drone", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Télécharger d'abord les premières et les dernières pièces (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "État initial pour les torrents ajoutés à qBittorrent. Notez que les torrents forcés ne tiennent pas compte des restrictions de partage", - "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent ne peut pas résoudre le lien magnétique avec la DHT désactivée", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Les fichiers .strm contenus dans ce dossier seront importés par drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Télécharger d'abord le premier et le dernier morceau (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "État initial des torrents ajoutés à qBittorrent. Notez que les torrents forcés ne respectent pas les restrictions relatives aux seeds", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent ne peut pas résoudre le lien magnet lorsque le DHT est désactivé", "DownloadClientQbittorrentTorrentStateError": "qBittorrent signale une erreur", - "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} n'a pas réussi à ajouter l'étiquette à qBittorrent.", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} n'a pas pu ajouter l'étiquette à qBittorrent.", "DownloadClientQbittorrentValidationCategoryRecommended": "La catégorie est recommandée", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} ne tentera pas d'importer les téléchargements terminés sans catégorie.", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} n'essaiera pas d'importer des téléchargements terminés sans catégorie.", "DownloadClientQbittorrentValidationCategoryUnsupported": "La catégorie n'est pas prise en charge", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "La mise en file d'attente n'est pas activée", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "La mise en file d'attente des torrents n'est pas activée dans vos paramètres de qBittorrent. Activez-la dans qBittorrent ou sélectionnez 'Dernier' comme priorité.", - "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent est configuré pour supprimer les torrents lorsqu'ils atteignent leur limite de partage (Share Ratio Limit)", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ne pourra pas effectuer le traitement des téléchargements terminés tel que configuré. Vous pouvez résoudre ce problème dans qBittorrent ('Outils -> Options...' dans le menu) en modifiant 'Options -> BitTorrent -> Limitation du ratio de partage' de 'Les supprimer' à 'Les mettre en pause'", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "L'activation ajoutera les torrents et les liens magnétiques à rTorrent dans un état arrêté. Cela peut endommager les fichiers magnétiques.", - "DownloadClientRTorrentSettingsDirectoryHelpText": "Emplacement optionnel pour placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de rTorrent", - "DownloadClientRTorrentSettingsUrlPath": "Chemin d'URL", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Chemin vers le point de terminaison XMLRPC, voir {url}. Il s'agit généralement de RPC2 ou [chemin vers ruTorrent]{url2} lors de l'utilisation de ruTorrent.", - "DownloadClientSabnzbdValidationCheckBeforeDownload": "Désactivez l'option 'Vérifier avant le téléchargement' dans Sabnzbd", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "L'utilisation de 'Vérifier avant le téléchargement' affecte la capacité de {appName} à suivre les nouveaux téléchargements. Sabnzbd recommande également 'Abandonner les tâches qui ne peuvent pas être terminées', car c'est plus efficace.", - "DownloadClientSabnzbdValidationDevelopVersion": "Version de développement de Sabnzbd, en supposant la version 3.0.0 ou supérieure.", - "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} pourrait ne pas être en mesure de prendre en charge les nouvelles fonctionnalités ajoutées à SABnzbd lors de l'exécution de versions de développement.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Mise en file d'attente non activée", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "La mise en file d'attente des torrents n'est pas activée dans les paramètres de qBittorrent. Activez-la dans qBittorrent ou sélectionnez 'Dernier' comme priorité.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent est configuré pour supprimer les torrents lorsqu'ils atteignent leur limite de ratio de partage", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ne pourra pas effectuer le traitement des téléchargements terminés tel que configuré. Vous pouvez résoudre ce problème dans qBittorrent ('Outils -> Options...' dans le menu) en remplaçant 'Options -> BitTorrent -> Limitation du ratio de partageg' de 'Les supprimer' par 'Les mettre en pause'", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "L'activation ajoutera des torrents et des magnets à rTorrent dans un état d'arrêt. Cela peut endommager les fichiers magnétiques.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Emplacement facultatif dans lequel placer les téléchargements. Laisser vide pour utiliser l'emplacement par défaut de rTorrent", + "DownloadClientRTorrentSettingsUrlPath": "Chemin d'url", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Chemin d'accès au point de terminaison XMLRPC, voir {url}. Il s'agit généralement de RPC2 ou de [chemin vers ruTorrent]{url2} lors de l'utilisation de ruTorrent.", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Désactiver l'option 'Vérifier avant de télécharger' dans Sabnbzd", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "L'utilisation de 'Vérifier avant de télécharger' affecte la capacité de {appName} à suivre les nouveaux téléchargements. Sabnzbd recommande également 'Abandonner les tâches qui ne peuvent pas être achevées', car c'est plus efficace.", + "DownloadClientSabnzbdValidationDevelopVersion": "Version de développement de Sabnzbd, en supposant une version 3.0.0 ou supérieure.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} peut ne pas être en mesure de prendre en charge les nouvelles fonctionnalités ajoutées à SABnzbd lors de l'exécution de versions développées.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Désactiver le tri par date", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Vous devez désactiver le tri par date pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Vous devez désactiver le tri par date pour la catégorie {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour résoudre ce problème.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Désactiver le tri des films", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Vous devez désactiver le tri des films pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", - "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Désactiver le tri des émissions de télévision", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Vous devez désactiver le tri des émissions de télévision pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", - "DownloadClientSabnzbdValidationEnableJobFolders": "Activer les dossiers de tâches", - "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} préfère que chaque téléchargement ait son propre dossier. En ajoutant un astérisque (*) au dossier/chemin, Sabnzbd ne créera pas ces dossiers de tâches. Rendez-vous dans Sabnzbd pour le résoudre.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Vous devez désactiver le tri des films pour la catégorie utilisée par {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour y remédier.", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Désactiver le tri des téléviseurs", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Vous devez désactiver le tri TV pour la catégorie {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour y remédier.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Activer les dossiers de travail", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} préfère que chaque téléchargement ait un dossier séparé. Avec * ajouté au dossier/chemin, Sabnzbd ne créera pas ces dossiers de travail. Allez sur Sabnzbd pour résoudre ce problème.", "DownloadClientSabnzbdValidationUnknownVersion": "Version inconnue : {rawVersion}", - "DownloadClientSettingsCategoryHelpText": "Ajouter une catégorie spécifique à {appName} évite les conflits avec des téléchargements non liés à {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée.", - "DownloadClientSettingsPostImportCategoryHelpText": "Catégorie à définir pour {appName} après avoir importé le téléchargement. {appName} ne supprimera pas les torrents de cette catégorie même si le partage est terminé. Laissez vide pour conserver la même catégorie.", - "DownloadClientSettingsDestinationHelpText": "Spécifie manuellement la destination du téléchargement, laissez vide pour utiliser la destination par défaut", - "DownloadClientValidationCategoryMissingDetail": "La catégorie que vous avez entrée n'existe pas dans {clientName}. Créez-la d'abord dans {clientName}.", + "DownloadClientSettingsCategoryHelpText": "L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée.", + "DownloadClientSettingsPostImportCategoryHelpText": "Catégorie que {appName} doit définir après avoir importé le téléchargement. {appName} ne supprimera pas les torrents de cette catégorie même si l'ensemencement est terminé. Laisser vide pour conserver la même catégorie.", + "DownloadClientSettingsDestinationHelpText": "Spécifie manuellement la destination du téléchargement, laisser vide pour utiliser la destination par défaut", + "DownloadClientValidationCategoryMissingDetail": "La catégorie que vous avez saisie n'existe pas dans {clientName}. Créez-la d'abord dans {clientName}.", "DownloadClientValidationErrorVersion": "La version de {clientName} doit être au moins {requiredVersion}. La version rapportée est {reportedVersion}", - "DownloadClientValidationGroupMissingDetail": "Le groupe que vous avez entré n'existe pas dans {clientName}. Créez-le d'abord dans {clientName}.", + "DownloadClientValidationGroupMissingDetail": "Le groupe que vous avez saisi n'existe pas dans {clientName}. Créez-le d'abord dans {clientName}.", "DownloadClientValidationSslConnectFailure": "Impossible de se connecter via SSL", "DownloadClientValidationUnableToConnect": "Impossible de se connecter à {clientName}", "IndexerIPTorrentsSettingsFeedUrlHelpText": "URL complète du flux RSS généré par IPTorrents, en utilisant uniquement les catégories que vous avez sélectionnées (HD, SD, x264, etc...)", - "IndexerHDBitsSettingsMediums": "Type de médias", + "IndexerHDBitsSettingsMediums": "Supports", "IndexerSettingsAdditionalNewznabParametersHelpText": "Veuillez noter que si vous modifiez la catégorie, vous devrez ajouter des règles requises/restrictives concernant les sous-groupes pour éviter les sorties en langues étrangères.", "IndexerSettingsAllowZeroSize": "Autoriser la taille zéro", "IndexerSettingsAllowZeroSizeHelpText": "L'activation de cette option vous permettra d'utiliser des flux qui ne spécifient pas la taille de la version, mais soyez prudent, les vérifications liées à la taille ne seront pas effectuées.", "IndexerSettingsAnimeCategoriesHelpText": "Liste déroulante, laissez vide pour désactiver les animes", "IndexerSettingsAnimeStandardFormatSearch": "Recherche au format standard pour les animes", "IndexerSettingsAnimeStandardFormatSearchHelpText": "Rechercher également les animes en utilisant la numérotation standard", - "IndexerSettingsApiPathHelpText": "Chemin vers l'API, généralement {url}", + "IndexerSettingsApiPathHelpText": "Chemin d'accès à l'api, généralement {url}", "IndexerSettingsApiUrlHelpText": "Ne le modifiez pas à moins de savoir ce que vous faites, car votre clé API sera envoyée à cet hôte.", "IndexerSettingsCategoriesHelpText": "Liste déroulante, laissez vide pour désactiver les émissions standard/quotidiennes", "IndexerSettingsCookieHelpText": "Si votre site nécessite un cookie de connexion pour accéder au flux RSS, vous devrez le récupérer via un navigateur.", @@ -1641,63 +1641,63 @@ "IndexerValidationJackettAllNotSupportedHelpText": "L'endpoint 'all' de Jackett n'est pas pris en charge, veuillez ajouter les indexeurs individuellement", "IndexerValidationUnableToConnectServerUnavailable": "Impossible de se connecter à l'indexeur, le serveur de l'indexeur est indisponible. Réessayez plus tard. {exceptionMessage}.", "IndexerValidationUnableToConnectTimeout": "Impossible de se connecter à l'indexeur, peut-être en raison d'un délai d'attente. Réessayez ou vérifiez vos paramètres réseau. {exceptionMessage}.", - "NzbgetHistoryItemMessage": "État PAR : {parStatus} - État de décompression : {unpackStatus} - État de déplacement : {moveStatus} - État du script : {scriptStatus} - État de suppression : {deleteStatus} - État de marquage : {markStatus}", - "TorrentBlackholeSaveMagnetFilesHelpText": "Enregistrer le lien magnétique s'il n'y a pas de fichier .torrent disponible (utile uniquement si le client de téléchargement prend en charge les liens magnétiques enregistrés dans un fichier)", - "PostImportCategory": "Catégorie après importation", + "NzbgetHistoryItemMessage": "Statut PAR : {parStatus} - Unpack Status : {unpackStatus} - Move Status : {moveStatus} - Statut du script : {scriptStatus} - Supprimer l'état : {deleteStatus} - Mark Status : {markStatus}", + "TorrentBlackholeSaveMagnetFilesHelpText": "Enregistrer le lien magnétique si aucun fichier .torrent n'est disponible (utile uniquement si le client de téléchargement prend en charge les liens magnétiques enregistrés dans un fichier)", + "PostImportCategory": "Catégorie après l'importation", "BlackholeFolderHelpText": "Dossier dans lequel {appName} stockera le fichier {extension}", - "BlackholeWatchFolder": "Dossier de surveillance", - "BlackholeWatchFolderHelpText": "Dossier à partir duquel {appName} devrait importer les téléchargements terminés", - "DownloadClientDelugeValidationLabelPluginInactive": "Plugin d'étiquetage non activé", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Vous devez avoir le plugin d'étiquetage activé dans {clientName} pour utiliser les catégories.", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "Dossier partagé facultatif dans lequel placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de Download Station", - "DownloadClientDownloadStationValidationApiVersion": "Version de l'API de Download Station non prise en charge, elle devrait être au moins {requiredVersion}. Elle prend en charge de {minVersion} à {maxVersion}", - "DownloadClientDownloadStationValidationFolderMissingDetail": "Le dossier '{downloadDir}' n'existe pas, il doit être créé manuellement à l'intérieur du Dossier Partagé '{sharedFolder}'.", - "DownloadClientDownloadStationProviderMessage": "{appName} ne peut pas se connecter à Download Station si l'authentification à deux facteurs est activée sur votre compte DSM", - "DownloadClientFloodSettingsAdditionalTagsHelpText": "Ajoute des propriétés des médias en tant qu'étiquettes. Les indices sont des exemples.", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Le paramètre 'KeepHistory' de NzbGet est réglé sur 0, ce qui empêche {appName} de voir les téléchargements terminés.", - "DownloadClientNzbgetValidationKeepHistoryZero": "Le paramètre 'KeepHistory' de NzbGet devrait être supérieur à 0", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Premier et dernier prénom", - "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Télécharger dans l'ordre séquentiel (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Utiliser une connexion sécurisée. Consultez Options -> Interface Web -> 'Utiliser HTTPS au lieu de HTTP' dans qBittorrent.", + "BlackholeWatchFolder": "Dossier surveillé", + "BlackholeWatchFolderHelpText": "Dossier à partir duquel {appName} doit importer les téléchargements terminés", + "DownloadClientDelugeValidationLabelPluginInactive": "Plugin d'étiquette non activé", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Vous devez avoir activé le plug-in Label dans {clientName} pour utiliser les catégories.", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Dossier partagé dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement par défaut de Download Station", + "DownloadClientDownloadStationValidationApiVersion": "La version de l'API de la station de téléchargement n'est pas prise en charge, elle doit être au moins {requiredVersion}. Elle est prise en charge de {minVersion} à {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "Le dossier '{downloadDir}' n'existe pas, il doit être créé manuellement dans le dossier partagé '{sharedFolder}'.", + "DownloadClientDownloadStationProviderMessage": "{appName} ne parvient pas à se connecter à Download Station si l'authentification à 2 facteurs est activée sur votre compte DSM", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Ajoute les propriétés des médias sous forme d'étiquette. Les conseils sont des exemples.", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Le paramètre KeepHistory de NzbGet est fixé à 0, ce qui empêche {appName} de voir les téléchargements terminés.", + "DownloadClientNzbgetValidationKeepHistoryZero": "Le paramètre KeepHistory de NzbGet doit être supérieur à 0", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Premier et dernier premiers", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Téléchargement dans l'ordre séquentiel (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Utilisez une connexion sécurisée. Voir Options -> UI Web -> 'Utiliser HTTPS au lieu de HTTP' dans qBittorrent.", "DownloadClientQbittorrentSettingsSequentialOrder": "Ordre séquentiel", - "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent est en train de télécharger les métadonnées", - "DownloadClientQbittorrentTorrentStatePathError": "Impossible d'importer. Le chemin correspond au répertoire de téléchargement de base du client, il est possible que 'Conserver le dossier de niveau supérieur' soit désactivé pour ce torrent ou que 'Disposition du contenu du torrent' ne soit pas définie sur 'Original' ou 'Créer un sous-dossier' ?", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent télécharge des métadonnées", + "DownloadClientQbittorrentTorrentStatePathError": "Impossible d'importer. Le chemin d'accès correspond au répertoire de téléchargement de la base du client, il est possible que l'option 'Conserver le dossier de premier niveau' soit désactivée pour ce torrent ou que l'option 'Disposition du contenu du torrent' ne soit PAS réglée sur 'Original' ou 'Créer un sous-dossier' ?", "DownloadClientQbittorrentTorrentStateStalled": "Le téléchargement est bloqué sans aucune connexion", "DownloadClientQbittorrentTorrentStateUnknown": "État de téléchargement inconnu : {state}", - "DownloadClientRTorrentSettingsAddStopped": "Ajouter Arrêté", - "DownloadClientSettingsAddPaused": "Ajouter en pause", - "DownloadClientSettingsOlderPriority": "Priorité inférieure", + "DownloadClientRTorrentSettingsAddStopped": "Ajout arrêté", + "DownloadClientSettingsAddPaused": "Ajout en pause", + "DownloadClientSettingsOlderPriority": "Priorité plus ancienne", "DownloadClientSettingsRecentPriority": "Priorité récente", "DownloadClientSettingsInitialState": "État initial", "DownloadClientSettingsInitialStateHelpText": "État initial pour les torrents ajoutés à {clientName}", - "DownloadClientSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL de {clientName}, comme {url}", + "DownloadClientSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url {clientName}, tel que {url}", "DownloadClientSettingsUseSslHelpText": "Utiliser une connexion sécurisée lors de la connexion à {clientName}", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Emplacement facultatif pour placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de Transmission", - "DownloadClientTransmissionSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL RPC de {clientName}, par exemple {url}, par défaut '{defaultUrl}'", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement de transmission par défaut", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url rpc de {clientName}, par exemple {url}, la valeur par défaut étant '{defaultUrl}'", "DownloadClientUTorrentTorrentStateError": "uTorrent signale une erreur", "DownloadClientValidationApiKeyIncorrect": "Clé API incorrecte", "DownloadClientValidationApiKeyRequired": "Clé API requise", "DownloadClientValidationAuthenticationFailure": "Échec de l'authentification", - "DownloadClientValidationAuthenticationFailureDetail": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe. Assurez-vous également que l'hôte sur lequel {appName} s'exécute n'est pas bloqué pour l'accès à {clientName} en raison de limitations de liste blanche (WhiteList) dans la configuration de {clientName}.", + "DownloadClientValidationAuthenticationFailureDetail": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe. Vérifiez également que l'hôte qui exécute {appName} n'est pas empêché d'accéder à {clientName} par des limitations de la liste blanche dans la configuration de {clientName}.", "DownloadClientValidationCategoryMissing": "La catégorie n'existe pas", "DownloadClientValidationGroupMissing": "Le groupe n'existe pas", "DownloadClientValidationTestNzbs": "Échec de l'obtention de la liste des NZB : {exceptionMessage}", - "DownloadClientValidationSslConnectFailureDetail": "{appName} ne peut pas se connecter à {clientName} en utilisant SSL. Ce problème pourrait être lié à l'ordinateur. Veuillez essayer de configurer à la fois {appName} et {clientName} pour ne pas utiliser SSL.", + "DownloadClientValidationSslConnectFailureDetail": "{appName} ne parvient pas à se connecter à {clientName} en utilisant SSL. Ce problème peut être lié à l'ordinateur. Veuillez essayer de configurer {appName} et {clientName} pour qu'ils n'utilisent pas SSL.", "DownloadClientValidationTestTorrents": "Échec de l'obtention de la liste des torrents : {exceptionMessage}", "DownloadClientValidationUnknownException": "Exception inconnue : {exception}", - "DownloadClientValidationVerifySsl": "Vérifiez les paramètres SSL", + "DownloadClientValidationVerifySsl": "Vérifier les paramètres SSL", "DownloadClientValidationUnableToConnectDetail": "Veuillez vérifier le nom d'hôte et le port.", - "DownloadClientValidationVerifySslDetail": "Veuillez vérifier votre configuration SSL à la fois sur {clientName} et {appName}", + "DownloadClientValidationVerifySslDetail": "Veuillez vérifier votre configuration SSL sur {clientName} et {appName}", "UnknownDownloadState": "État de téléchargement inconnu : {state}", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Priorité à utiliser lors de la récupération des épisodes diffusés il y a plus de 14 jours", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Priorité à utiliser lors de la récupération des épisodes diffusés au cours des 14 derniers jours", "MonitorNewSeasonsHelpText": "Quelles nouvelles saisons doivent être surveillées automatiquement", "MonitorNoNewSeasonsDescription": "Ne pas surveiller automatiquement de nouvelles saisons", - "TorrentBlackholeSaveMagnetFilesExtension": "Enregistrer les extensions de fichiers magnet", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension à utiliser pour les liens magnétiques, par défaut '.magnet'", - "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Au lieu de déplacer les fichiers, cela indiquera à {appName} de copier ou de créer un lien (en fonction des paramètres/configuration système)", + "TorrentBlackholeSaveMagnetFilesExtension": "Sauvegarde des fichiers magnétiques Extension", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension à utiliser pour les liens magnétiques, la valeur par défaut est '.magnet'", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Au lieu de déplacer les fichiers, cela demandera à {appName} de les copier ou de les relier (en fonction des paramètres/de la configuration du système)", "TorrentBlackholeTorrentFolder": "Dossier Torrent", - "XmlRpcPath": "Chemin XML RPC", + "XmlRpcPath": "Chemin d'accès XML RPC", "AddRootFolderError": "Impossible d'ajouter le dossier racine", "NotificationsAppriseSettingsConfigurationKey": "Clé de configuration Apprise", "NotificationsCustomScriptSettingsProviderMessage": "Tester va exécuter le script avec le type d'événement définit sur {eventTypeTest}, assurez-vous que votre script le gère correctement", @@ -1792,7 +1792,7 @@ "NotificationsSettingsUpdateMapPathsFrom": "Mapper les chemins depuis", "NotificationsSettingsUpdateLibrary": "Mettre à jour la bibliothèque", "NotificationsSendGridSettingsApiKeyHelpText": "La clé API générée par SendGrid", - "NotificationsSettingsUpdateMapPathsToHelpText": "Chemin {serviceName}, utilisé pour modifier les chemins des séries quand {serviceName} voit un chemin d'emplacement de bibliothèque différemment de {appName} (nécessite « Mise à jour bibliothèque »)", + "NotificationsSettingsUpdateMapPathsToHelpText": "Chemin {serviceName}, utilisé pour modifier les chemins des séries quand {serviceName} voit un chemin d'emplacement de bibliothèque différemment de {appName} (nécessite 'Mise à jour bibliothèque')", "NotificationsSettingsUpdateMapPathsTo": "Mapper les chemins vers", "NotificationsSignalSettingsUsernameHelpText": "Nom d'utilisateur utilisé pour authentifier les requêtes vers signal-api", "NotificationsSlackSettingsIcon": "Icône", @@ -1872,7 +1872,7 @@ "AutoTaggingSpecificationMaximumYear": "Année maximum", "AutoTaggingSpecificationMinimumYear": "Année minimum", "AutoTaggingSpecificationOriginalLanguage": "Langue", - "AutoTaggingSpecificationQualityProfile": "Profil de Qualité", + "AutoTaggingSpecificationQualityProfile": "Profil de qualité", "AutoTaggingSpecificationRootFolder": "Dossier Racine", "AutoTaggingSpecificationSeriesType": "Type de série", "AutoTaggingSpecificationStatus": "État", @@ -1883,19 +1883,19 @@ "CustomFormatsSpecificationReleaseGroup": "Groupe de versions", "CustomFormatsSpecificationResolution": "Résolution", "CustomFormatsSpecificationSource": "Source", - "ImportListsAniListSettingsAuthenticateWithAniList": "Connection avec AniList", - "ImportListsAniListSettingsImportCancelled": "Importation annulé", - "ImportListsAniListSettingsImportCancelledHelpText": "Media : La série est annulé", - "ImportListsAniListSettingsImportCompleted": "Importation terminé", + "ImportListsAniListSettingsAuthenticateWithAniList": "S'authentifier avec AniList", + "ImportListsAniListSettingsImportCancelled": "Importation annulée", + "ImportListsAniListSettingsImportCancelledHelpText": "Médias : La série est annulée", + "ImportListsAniListSettingsImportCompleted": "Importation terminée", "ImportListsAniListSettingsImportFinished": "Importation terminée", - "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur de la liste à importer", - "ImportListsCustomListSettingsName": "Liste personnalisé", + "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur pour la liste à importer", + "ImportListsCustomListSettingsName": "Liste personnalisée", "ImportListsCustomListValidationAuthenticationFailure": "Échec de l'authentification", - "ImportListsPlexSettingsAuthenticateWithPlex": "Se connecter avec Plex.tv", - "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", + "ImportListsPlexSettingsAuthenticateWithPlex": "S'authentifier avec Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Liste de surveillance Plex", "ImportListsSettingsAccessToken": "Jeton d'accès", "ImportListsSettingsRefreshToken": "Jeton d'actualisation", - "ImportListsSimklSettingsAuthenticatewithSimkl": "Se connecter avec Simkl", + "ImportListsSimklSettingsAuthenticatewithSimkl": "S'authentifier avec Simkl", "ImportListsSonarrSettingsFullUrl": "URL complète", "DownloadClientPriorityHelpText": "Priorité du client de téléchargement de 1 (la plus haute) à 50 (la plus faible). Par défaut : 1. Le Round-Robin est utilisé pour les clients ayant la même priorité.", "CustomFormatsSpecificationRegularExpressionHelpText": "Format personnalisé RegEx est insensible à la casse", @@ -1923,5 +1923,139 @@ "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Si un torrent est bloqué par le hachage, il peut ne pas être correctement rejeté pendant le RSS/recherche pour certains indexeurs. L'activation de cette fonction permet de le rejeter après que le torrent a été saisi, mais avant qu'il ne soit envoyé au client.", "DownloadClientAriaSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement par défaut Aria2", "AddDelayProfileError": "Impossible d'ajouter un nouveau profil de délai, veuillez réessayer.", - "BlocklistReleaseHelpText": "Bloque le téléchargement de cette version par {appName} via RSS ou Recherche automatique" + "BlocklistReleaseHelpText": "Empêche cette version d'être téléchargée par {appName} via RSS ou la recherche automatique", + "NotificationsEmailSettingsUseEncryptionHelpText": "Préférer utiliser le cryptage s'il est configuré sur le serveur, toujours utiliser le cryptage via SSL (port 465 uniquement) ou StartTLS (tout autre port) ou ne jamais utiliser le cryptage", + "LabelIsRequired": "L'étiquette est requise", + "NotificationsEmailSettingsUseEncryption": "Utiliser le cryptage", + "ConnectionSettingsUrlBaseHelpText": "Ajoute un préfixe l'url de {connectionName}, comme {url}", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Destination pour les téléchargements terminés (facultative), laissez ce champ vide pour utiliser le répertoire par défaut de Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Emplacement dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement Deluge par défaut", + "DownloadClientDelugeSettingsDirectory": "Dossier de téléchargement", + "DownloadClientDelugeSettingsDirectoryCompleted": "Dossier de déplacement une fois terminé", + "ClickToChangeIndexerFlags": "Cliquer pour changer les attributs de l'indexer", + "CustomFormatsSpecificationFlag": "Attribut", + "CustomFilter": "Filtre personnalisé", + "ImportListsTraktSettingsAuthenticateWithTrakt": "S'authentifier avec Trakt", + "SelectIndexerFlags": "Sélectionner les drapeaux de l'indexeur", + "SetIndexerFlags": "Définir les drapeaux de l'indexeur", + "SetIndexerFlagsModalTitle": "{modalTitle} - Définir les drapeaux de l'indexeur", + "KeepAndTagSeries": "Conserver et étiqueter les séries", + "KeepAndUnmonitorSeries": "Série Garder et ne pas surveiller", + "CustomFormatsSpecificationMaximumSizeHelpText": "La version doit être inférieur ou égal à cette taille", + "ImportListsAniListSettingsImportDroppedHelpText": "Liste : Abandonné", + "ImportListsAniListSettingsImportPlanningHelpText": "Liste : Planification à suivre", + "ImportListsAniListSettingsImportPlanning": "Planification des importations", + "ImportListsAniListSettingsImportReleasing": "Importation de la diffusion", + "ImportListsAniListSettingsImportWatching": "Importation de surveillance", + "ImportListsAniListSettingsImportWatchingHelpText": "Liste : En cours de visionnage", + "ImportListsCustomListSettingsUrlHelpText": "L'URL de la liste des séries", + "ImportListsCustomListValidationConnectionError": "Impossible d'envoyer une requête à cette URL. StatusCode : {exceptionStatusCode}", + "ImportListsPlexSettingsWatchlistRSSName": "Liste de surveillance de Plex RSS", + "ImportListsSettingsAuthUser": "Utilisateur Auth", + "ImportListsSettingsExpires": "Expiration", + "ImportListsSimklSettingsListType": "Type de liste", + "ImportListsSimklSettingsName": "Liste de surveillance des utilisateurs de Simkl", + "ImportListsSimklSettingsListTypeHelpText": "Type de liste à partir de laquelle vous cherchez à importer", + "ImportListsSimklSettingsUserListTypeHold": "Tenir", + "ImportListsSimklSettingsUserListTypeDropped": "Abandonné", + "ImportListsSonarrSettingsApiKeyHelpText": "Clé API de l'instance {appName} à importer depuis", + "ImportListsSonarrSettingsRootFoldersHelpText": "Dossiers racine de l'instance source à partir de laquelle l'importation doit être effectuée", + "ImportListsSonarrSettingsTagsHelpText": "Tags de l'instance source à importer", + "ImportListsTraktSettingsAdditionalParameters": "Paramètres supplémentaires", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Paramètres supplémentaires de l'API Trakt", + "ImportListsTraktSettingsLimitHelpText": "Limiter le nombre de séries à obtenir", + "ImportListsTraktSettingsListType": "Type de liste", + "ImportListsTraktSettingsListTypeHelpText": "Type de liste à partir de laquelle vous cherchez à importer", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Spectacles recommandés par mois", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Spectacles recommandés par année", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Spectacles les plus regardés par semaine", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Spectacles les plus regardés par mois", + "ImportListsTraktSettingsUserListTypeCollection": "Liste des collections d'utilisateurs", + "ImportListsTraktSettingsUserListName": "Utilisateur de Trakt", + "ImportListsTraktSettingsUserListTypeWatch": "Liste de surveillance des utilisateurs", + "ImportListsTraktSettingsUserListUsernameHelpText": "Nom d'utilisateur pour la liste à importer (laisser vide pour utiliser Auth User)", + "ImportListsTraktSettingsUsernameHelpText": "Nom d'utilisateur pour la liste à importer", + "ImportListsTraktSettingsWatchedListFilter": "Filtre de liste surveillée", + "ImportListsTraktSettingsWatchedListTypeAll": "Tous", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% regardé", + "MetadataSettingsSeasonImages": "Images de la saison", + "MetadataSettingsEpisodeMetadataImageThumbs": "Métadonnées des épisodes Vignettes des images", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Inclure des balises d'image dans <filename>.nfo (nécessite 'Episode Metadata')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo avec les métadonnées complètes de la série", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Inclure l'URL de la série TheTVDB dans tvshow.nfo (peut être combiné avec 'Métadonnées de la série')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Inclure l'élément JSON du guide d'épisode dans tvshow.nfo (nécessite 'Métadonnées de la série')", + "MetadataSettingsEpisodeImages": "Images de l'épisode", + "MetadataSettingsEpisodeMetadata": "Métadonnées d'épisode", + "NotificationsSettingsUpdateMapPathsFromHelpText": "Chemin d'accès {appName}, utilisé pour modifier les chemins d'accès aux séries lorsque {serviceName} voit l'emplacement du chemin d'accès à la bibliothèque différemment de {appName} (Nécessite 'Mettre à jour la bibliothèque')", + "ReleaseType": "Type de version", + "ImportListsSimklSettingsUserListTypeWatching": "Regarder", + "ImportListsTraktSettingsLimit": "Limite", + "ImportListsSimklSettingsUserListTypeCompleted": "Terminé", + "ImportListsAniListSettingsImportReleasingHelpText": "Médias : Diffusion actuelle de nouveaux épisodes", + "ImportListsAniListSettingsImportRepeating": "Importation répétée", + "ImportListsAniListSettingsImportRepeatingHelpText": "Liste : En cours de relecture", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Plan de surveillance", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, y compris le port, de l'instance {appName} à importer depuis", + "ImportListsTraktSettingsUserListTypeWatched": "Liste des utilisateurs surveillés", + "Label": "Étiquette", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Guide des épisodes de métadonnées de séries", + "MetadataSettingsSeriesMetadata": "Métadonnées de la série", + "MetadataSettingsSeriesImages": "Série Images", + "CleanLibraryLevel": "Nettoyer le niveau de la bibliothèque", + "EpisodeRequested": "Épisode demandé", + "CustomFormatsSpecificationMinimumSizeHelpText": "La version doit être supérieure à cette taille", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Médias : La diffusion n'a pas encore commencé", + "ImportListsAniListSettingsImportPaused": "Importation en pause", + "ImportListsAniListSettingsImportPausedHelpText": "Liste : En attente", + "ImportListsCustomListSettingsUrl": "URL de la liste", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Toutes les listes requièrent une interaction manuelle en raison de la possibilité de recherches partielles", + "ImportListsAniListSettingsImportCompletedHelpText": "Liste : Surveillance achevée", + "ImportListsAniListSettingsImportDropped": "Importation abandonnée", + "ImportListsAniListSettingsImportFinishedHelpText": "Médias : Tous les épisodes ont été diffusés", + "ImportListsAniListSettingsImportHiatus": "Hiatus d'importation", + "ImportListsAniListSettingsImportHiatusHelpText": "Médias : Série en hiatus", + "ImportListsAniListSettingsImportNotYetReleased": "Importation non encore diffusée", + "ImportListsSimklSettingsShowType": "Type de spectacle", + "ImportListsSimklSettingsShowTypeHelpText": "Type de spectacle que vous souhaitez importer", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Profils de qualité de l'instance source à importer", + "ImportListsSonarrValidationInvalidUrl": "L'URL de {appName} n'est pas valide, vous manque-t-il une base d'URL ?", + "ImportListsSettingsRssUrl": "URL RSS", + "ImportListsImdbSettingsListId": "ID de la liste", + "ImportListsImdbSettingsListIdHelpText": "ID de la liste IMDb (par exemple ls12345678)", + "ImportListsTraktSettingsGenres": "Genres", + "ImportListsTraktSettingsGenresHelpText": "Filtrer les séries par Trakt Genre Slug (séparées par des virgules) Uniquement pour les listes populaires", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Spectacles recommandés par semaine", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Les émissions les plus regardées de tous les temps", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Spectacles les plus regardés par année", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Spectacles en vogue", + "ImportListsTraktSettingsPopularName": "Liste populaire de Trakt", + "ImportListsTraktSettingsRating": "Evaluation", + "ImportListsTraktSettingsRatingHelpText": "Série de filtres par plage de valeurs nominales (0-100)", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Si le type de liste est surveillé, sélectionnez le type de série que vous souhaitez importer", + "ImportListsTraktSettingsWatchedListSorting": "Tri de la liste de surveillance", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Si le type de liste est surveillé, sélectionnez l'ordre de tri de la liste", + "ImportListsTraktSettingsListName": "Nom de la liste", + "ImportListsTraktSettingsListNameHelpText": "Nom de la liste à importer, la liste doit être publique ou vous devez avoir accès à la liste", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Spectacles attendus", + "ImportListsTraktSettingsPopularListTypePopularShows": "Spectacles populaires", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Spectacles recommandés de tous les temps", + "ImportListsTraktSettingsWatchedListTypeInProgress": "En cours", + "ImportListsTraktSettingsYears": "Années", + "ImportListsTraktSettingsYearsHelpText": "Filtrer les séries par année ou par plage d'années", + "ImportListsValidationUnableToConnectException": "Impossible de se connecter à la liste des importations : {exceptionMessage}. Consultez les logs entourant cette erreur pour plus de détails.", + "ImportListsValidationInvalidApiKey": "La clé API n'est pas valide", + "ImportListsValidationTestFailed": "Le test a été interrompu en raison d'une erreur : {exceptionMessage}", + "IndexerFlags": "Drapeaux de l'indexeur", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Suivi de la saison de synchronisation", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "La surveillance de la saison de synchronisation à partir de l’instance {appName} si l'option 'Monitor' est activée, elle sera ignorée", + "ListSyncTag": "Balise de synchronisation de liste", + "ListSyncTagHelpText": "Cette étiquette sera ajoutée lorsqu'une série tombera ou ne figurera plus sur votre (vos) liste(s)", + "LogOnly": "Log seulement", + "ListSyncLevelHelpText": "Les séries de la bibliothèque seront traitées en fonction de votre sélection si elles tombent ou ne figurent pas sur votre (vos) liste(s)", + "MetadataPlexSettingsSeriesPlexMatchFile": "Fichier de correspondance Plex série", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Crée un fichier .plexmatch dans le dossier de la série", + "MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série", + "NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise", + "DatabaseMigration": "Migration des bases de données", + "Filters": "Filtres" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index f8ca3b381..c3213f302 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -17,14 +17,14 @@ "RemoveSelectedItemsQueueMessageText": "Biztosan el akar távolítani {selectedCount} elemet a várólistáról?", "Required": "Kötelező", "Added": "Hozzáadva", - "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {hossz} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", + "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {length} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", "ApplyChanges": "Változások alkalmazása", "AppDataLocationHealthCheckMessage": "A frissítés nem lehetséges az alkalmazás adatok törlése nélkül", "AutomaticAdd": "Automatikus hozzáadás", "CountSeasons": "{count} Évad", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Nincs elérhető letöltési kliens", - "DownloadClientRootFolderHealthCheckMessage": "A letöltési kliens {downloadClientName} a letöltéseket a gyökérmappába helyezi. Ne tölts le közvetlenül a gyökérmappába.", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nem lehet kommunikálni a {downloadClientName} -val", + "DownloadClientRootFolderHealthCheckMessage": "A letöltési kliens {downloadClientName} a letöltéseket a gyökérmappába helyezi {rootFolderPath}. Ne tölts le közvetlenül a gyökérmappába.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nem lehet kommunikálni a {downloadClientName}. {errorMessage}", "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltési kliens elérhetetlen meghibásodások miatt", "EditSelectedDownloadClients": "Kijelölt letöltési kliensek szerkesztése", "EditSelectedImportLists": "Kijelölt importálási listák szerkesztése", @@ -36,7 +36,7 @@ "Ended": "Vége", "HideAdvanced": "Haladó elrejtése", "ImportListRootFolderMissingRootHealthCheckMessage": "Hiányzó gyökérmappa a/az {rootFolderInfo} importálási listához", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Több gyökérmappa hiányzik a/az {rootFoldersInfo} importálási listához", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Több gyökérmappa hiányzik a/az {rootFolderInfo} importálási listához", "Enabled": "Engedélyezés", "HiddenClickToShow": "Rejtett, kattints a felfedéshez", "ImportListStatusAllUnavailableHealthCheckMessage": "Minden lista elérhetetlen meghibásodások miatt", @@ -94,7 +94,7 @@ "SystemTimeHealthCheckMessage": "A rendszer idő több, mint 1 napot eltér az aktuális időtől. Előfordulhat, hogy az ütemezett feladatok nem futnak megfelelően, amíg az időt nem korrigálják", "Unmonitored": "Nem felügyelt", "UpdateStartupNotWritableHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{startupFolder}' nem írható a(z) '{userName}' felhasználó által.", - "UpdateStartupTranslocationHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{indítási mappa}' az App Translocation mappában található.", + "UpdateStartupTranslocationHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{startupFolder}' az App Translocation mappában található.", "UpdateAvailableHealthCheckMessage": "Új frissítés elérhető", "UpdateUiNotWritableHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a felhasználó '{userName}' nem rendelkezik írási jogosultsággal a(z) '{uiFolder}' felhasználói felület mappában.", "DownloadClientSortingHealthCheckMessage": "A(z) {downloadClientName} letöltési kliensben engedélyezve van a {sortingMode} rendezés a {appName} kategóriájához. Az import problémák elkerülése érdekében ki kell kapcsolnia a rendezést a letöltési kliensben.", @@ -891,7 +891,7 @@ "DeleteSelectedIndexersMessageText": "Biztosan törölni szeretne {count} kiválasztott indexelőt?", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Függetlenül attól, hogy a qBittorrent konfigurált tartalomelrendezését használja, az eredeti elrendezést a torrentből, vagy mindig hozzon létre egy almappát (qBittorrent 4.3.2)", "FormatAgeDay": "nap", - "FormatRuntimeMinutes": "{perc} p", + "FormatRuntimeMinutes": "{minutes} p", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "A(z) {downloadClientName} letöltési kliens úgy van beállítva, hogy eltávolítsa a befejezett letöltéseket. Ez azt eredményezheti, hogy a letöltések eltávolításra kerülnek az ügyfélprogramból, mielőtt a {appName} importálhatná őket.", "RecyclingBinCleanupHelpTextWarning": "A kiválasztott napoknál régebbi fájlok a lomtárban automatikusan törlődnek", "ReleaseProfileIndexerHelpTextWarning": "Egy adott indexelő kiadási profilokkal történő használata duplikált kiadások megragadásához vezethet", @@ -975,7 +975,6 @@ "DeleteSelectedDownloadClientsMessageText": "Biztosan törölni szeretné a kiválasztott {count} letöltési klienst?", "Tba": "TBA", "SpecialsFolderFormat": "Különleges mappa formátum", - "TablePageSizeMinimum": "A relatív elérési utak a(z) {appName} AppData könyvtárában találhatók", "TorrentDelay": "Torrent Késleltetés", "TorrentBlackhole": "Torrent Blackhole", "TorrentDelayHelpText": "Percek késése, hogy várjon, mielőtt megragad egy torrentet", @@ -1107,7 +1106,7 @@ "DownloadClientFloodSettingsStartOnAdd": "Kezdje a Hozzáadás lehetőséggel", "ImportExtraFiles": "Extra fájlok importálása", "ImportListExclusions": "Listakizárások importálása", - "BlocklistReleaseHelpText": "Letiltja ennek a kiadásnak a letöltését a(z) {app Name} által RSS-en vagy automatikus keresésen keresztül", + "BlocklistReleaseHelpText": "Letiltja ennek a kiadásnak a letöltését a(z) {appName} által RSS-en vagy automatikus keresésen keresztül", "CustomFormatUnknownCondition": "Ismeretlen egyéni formátum feltétele „{implementation}”", "AutoTaggingNegateHelpText": "Ha be van jelölve, az automatikus címkézési szabály nem érvényesül, ha ez a {implementationName} feltétel megfelel.", "CountSeriesSelected": "{count} sorozat kiválasztva", @@ -1380,7 +1379,7 @@ "ProcessingFolders": "Mappák feldolgozása", "ProgressBarProgress": "Haladásjelző sáv: {progress}%", "ProxyType": "Proxy típus", - "RegularExpressionsTutorialLink": "További részletek a reguláris kifejezésekről [itt](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsTutorialLink": "További részletek a reguláris kifejezésekről [itt]({url}).", "ReplaceIllegalCharacters": "Cserélje ki az illegális karaktereket", "ResetDefinitionTitlesHelpText": "A definíciócímek és értékek visszaállítása", "ResetDefinitions": "Definíciók visszaállítása", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 2b98de2c4..3cb19a328 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -227,7 +227,7 @@ "DeleteCondition": "Cancella Condizione", "DeleteEpisodeFromDisk": "Cancella episodio dal disco", "DeleteNotification": "Cancella Notifica", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}. {errorMessage}", "Connect": "Collegamento", "CustomFormatsSettings": "Formati Personalizzati Impostazioni", "Condition": "Condizione", diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index a719741c0..439c144d4 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -9,7 +9,7 @@ "NoHistory": "내역 없음", "SelectAll": "모두 선택", "View": "표시 변경", - "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다.", + "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다", "AddNew": "새로 추가하기", "History": "내역", "Sunday": "일요일" diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index ff993454a..082114cd8 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -38,9 +38,9 @@ "Date": "Datum", "About": "Over", "Actions": "Acties", - "AppDataDirectory": "AppData folder", + "AppDataDirectory": "AppData map", "AptUpdater": "Gebruik apt om de update te installeren", - "BackupNow": "Nu backup nemen", + "BackupNow": "Back-up nu maken", "BeforeUpdate": "Voor Update", "CancelPendingTask": "Ben je zeker dat je deze onafgewerkte taak wil annuleren?", "Clear": "Wis", @@ -170,5 +170,40 @@ "AutoTaggingSpecificationGenre": "Genre(s)", "BackupFolderHelpText": "Relatieve paden zullen t.o.v. de {appName} AppData map bekeken worden", "BindAddress": "Gebonden Adres", - "BindAddressHelpText": "Geldig IP-adres, localhost of '*' voor alle interfaces" + "BindAddressHelpText": "Geldig IP-adres, localhost of '*' voor alle interfaces", + "DelayMinutes": "{delay} minuten", + "FormatAgeMinutes": "minuten", + "EnableInteractiveSearchHelpText": "Zal worden gebruikt wanneer interactief zoeken wordt gebruikt", + "UsenetDelayTime": "Usenet-vertraging: {usenetDelay}", + "MinutesSixty": "60 Minuten: {sixty}", + "RemoveFromDownloadClient": "Verwijder uit download cliënt", + "RemoveSelectedItemsQueueMessageText": "Weet je zeker dat je {selectedCount} items van de wachtrij wilt verwijderen?", + "BlocklistLoadError": "Niet in staat om de blokkeerlijst te laden", + "CustomFormatUnknownConditionOption": "Onbekende optie '{key}' voor conditie '{implementation}'", + "ImportListsSettingsSummary": "Importeer van een andere {appName} of Trakt lijst en regel lijst uitzonderingen", + "TagDetails": "Tagdetails - {label}", + "RemoveSelectedItemQueueMessageText": "Weet je zeker dat je 1 item van de wachtrij wilt verwijderen?", + "EnableAutomaticSearch": "Schakel automatisch zoeken in", + "DeleteTagMessageText": "Weet je zeker dat je de tag '{label}' wil verwijderen?", + "DownloadWarning": "Download waarschuwing: {warningMessage}", + "EnableAutomaticAdd": "Schakel automatisch toevoegen in", + "EnableColorImpairedMode": "Schakel kleurenblindheid-modus in", + "EnableCompletedDownloadHandlingHelpText": "Importeer automatisch voltooide downloads vanuit de download cliënt", + "EnableColorImpairedModeHelpText": "Aangepaste stijl voor gebruikers die kleurenblind zijn om gemakkelijker kleurgecodeerde informatie te onderscheiden", + "EnableInteractiveSearch": "Schakel interactief zoeken in", + "TorrentDelayTime": "Torrent-vertraging: {torrentDelay}", + "WouldYouLikeToRestoreBackup": "Wilt u de back-up {name} herstellen?", + "DeleteNotificationMessageText": "Weet je zeker dat je de notificatie ‘{name}’ wil verwijderen?", + "PrioritySettings": "Prioriteit: {priority}", + "RssSync": "RSS Sync", + "Enable": "Inschakelen", + "EnableAutomaticSearchHelpText": "Zal worden gebruikt wanneer automatische zoekopdrachten worden uitgevoerd via de gebruikersinterface of door {appName}", + "AutomaticUpdatesDisabledDocker": "Automatische updates zijn niet ondersteund wanneer je het docker update mechanisme gebruikt. Je dient de container image up te daten buiten {appName} om of een script te gebruiken", + "ClearBlocklistMessageText": "Weet je zeker dat je de blokkeerlijst wil legen?", + "BlackholeFolderHelpText": "De map waarin {appName} het {extension} bestand opslaat", + "BlackholeWatchFolderHelpText": "De map waaruit {appName} de voltooide downloads dient te importeren", + "Category": "Categorie", + "BlocklistReleaseHelpText": "Voorkom dat deze release opnieuw wordt gedownload door {appName} door een RSS lijst of een automatische zoekopdracht", + "ChangeCategory": "Verander categorie", + "ChownGroup": "chown groep" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 8f2497675..95931000a 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -129,13 +129,13 @@ "DeleteDownloadClientMessageText": "Tem a certeza que quer eliminar o cliente de transferências \"{name}\"?", "DeleteNotificationMessageText": "Tem a certeza que quer eliminar a notificação \"{name}\"?", "EnableRss": "Activar RSS", - "DeleteSelectedDownloadClientsMessageText": "Tem a certeza de que pretende eliminar o(s) cliente(s) de transferência selecionado(s)?", + "DeleteSelectedDownloadClientsMessageText": "Tem a certeza de que pretende eliminar o(s) cliente(s) de {count} transferência selecionado(s)?", "MaintenanceRelease": "Versão de manutenção: reparações de erros e outras melhorias. Consulte o Histórico de Commits do Github para saber mais", "DeleteBackupMessageText": "Tem a certeza que quer eliminar a cópia de segurança \"{name}\"?", "Exception": "Exceção", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Pontuação Mínima do Formato Personalizado necessária para contornar o atraso do protocolo preferido", - "ConnectionLostReconnect": "O Radarr tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", - "ConnectionLostToBackend": "O Radarr perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLostReconnect": "O {appName} tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", + "ConnectionLostToBackend": "O {appName} perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", "CountIndexersSelected": "{count} indexador(es) selecionado(s)", "DeleteImportListMessageText": "Tem a certeza de que pretende eliminar a lista '{name}'?", "DeleteRootFolder": "Eliminar a Pasta Raiz", @@ -143,7 +143,7 @@ "EditSelectedDownloadClients": "Editar Clientes de Transferência Selecionados", "EditSelectedImportLists": "Editar Listas de Importação Selecionadas", "CloneAutoTag": "Clonar Etiqueta Automática", - "DeleteSelectedImportListsMessageText": "Tem a certeza de que pretende eliminar a(s) lista(s) de importação selecionada(s)?", + "DeleteSelectedImportListsMessageText": "Tem a certeza de que pretende eliminar a(s) lista(s) de {count} importação selecionada(s)?", "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação de formato personalizado", "CouldNotFindResults": "Nenhum resultado encontrado para \"{term}\"", "CountImportListsSelected": "{count} importar lista(s) selecionada(s)", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 940cb956f..0773f1dac 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -9,7 +9,7 @@ "Enabled": "Habilitado", "Ended": "Terminou", "HideAdvanced": "Ocultar opções avançadas", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão faltando nas listas de importação: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão ausentes nas listas de importação: {rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Todas as listas estão indisponíveis devido a falhas", "ImportMechanismHandlingDisabledHealthCheckMessage": "Ativar gerenciamento de download concluído", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", @@ -1327,7 +1327,7 @@ "MoveSeriesFoldersToRootFolder": "Gostaria de mover as pastas da série para '{destinationRootFolder}'?", "PreviewRename": "Prévia da Renomeação", "PreviewRenameSeason": "Prévia da Renomeação para esta temporada", - "PreviousAiringDate": "Exibição Anterior: {data}", + "PreviousAiringDate": "Exibição Anterior: {date}", "SeasonInformation": "Informações da Temporada", "SeasonDetails": "Detalhes da Temporada", "SelectAll": "Selecionar Tudo", @@ -1615,7 +1615,7 @@ "DownloadClientValidationTestTorrents": "Falha ao obter a lista de torrents: {exceptionMessage}", "DownloadClientValidationUnableToConnect": "Não foi possível conectar-se a {clientName}", "DownloadClientValidationUnableToConnectDetail": "Verifique o nome do host e a porta.", - "DownloadClientValidationUnknownException": "Exceção desconhecida: {exceção}", + "DownloadClientValidationUnknownException": "Exceção desconhecida: {exception}", "DownloadClientValidationVerifySsl": "Verifique as configurações de SSL", "DownloadClientValidationVerifySslDetail": "Verifique sua configuração SSL em {clientName} e {appName}", "DownloadClientVuzeValidationErrorVersion": "Versão do protocolo não suportada, use Vuze 5.0.0.0 ou superior com o plugin Vuze Web Remote.", @@ -1756,7 +1756,7 @@ "NotificationsKodiSettingsCleanLibraryHelpText": "Limpar biblioteca após atualização", "NotificationsKodiSettingsDisplayTime": "Tempo de Exibição", "NotificationsKodiSettingsGuiNotification": "Notificação GUI", - "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca ao Importar & Renomear?", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca em Importar e Renomear?", "NotificationsMailgunSettingsApiKeyHelpText": "A chave API gerada pelo MailGun", "NotificationsMailgunSettingsSenderDomain": "Domínio do Remetente", "NotificationsMailgunSettingsUseEuEndpoint": "Usar EU Endpoint", @@ -2052,5 +2052,10 @@ "Label": "Rótulo", "LabelIsRequired": "Rótulo é requerido", "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo a URL {connectionName}, como {url}", - "ReleaseType": "Tipo de Lançamento" + "ReleaseType": "Tipo de Lançamento", + "DownloadClientDelugeSettingsDirectory": "Diretório de Download", + "DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 0a28d0f4a..d6a639da8 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -20,7 +20,7 @@ "ApplyTagsHelpTextAdd": "Adăugare: adăugați etichetele la lista de etichete existentă", "ApplyTagsHelpTextReplace": "Înlocuire: înlocuiți etichetele cu etichetele introduse (nu introduceți etichete pentru a șterge toate etichetele)", "CancelPendingTask": "Sigur doriți să anulați această sarcină în așteptare?", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}. {errorMessage}", "CloneCustomFormat": "Clonați format personalizat", "Close": "Închide", "Delete": "Șterge", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 95e74778b..cf5197d5c 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -20,7 +20,7 @@ "HiddenClickToShow": "Скрыто, нажмите чтобы показать", "HideAdvanced": "Скрыть расширенные", "ImportListRootFolderMissingRootHealthCheckMessage": "Отсутствует корневая папка для импортирования списка(ов): {rootFolderInfo}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Все листы недоступны из-за ошибок", "ImportListStatusUnavailableHealthCheckMessage": "Листы недоступны из-за ошибок: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Включить обработку завершенной загрузки, если это возможно", @@ -210,5 +210,11 @@ "CustomFormatsSpecificationRegularExpression": "Регулярное выражение", "CustomFormatsSpecificationReleaseGroup": "Релиз группа", "CustomFormatsSpecificationResolution": "Разрешение", - "CustomFormatsSpecificationSource": "Источник" + "CustomFormatsSpecificationSource": "Источник", + "AddAutoTag": "Добавить автоматический тег", + "AddAutoTagError": "Не удалось добавить новый авто тег, пожалуйста повторите попытку.", + "AddListExclusionError": "Не удалось добавить новое исключение из списка. Повторите попытку.", + "AddImportListExclusionError": "Не удалось добавить новое исключение из списка импорта. Повторите попытку.", + "AddListExclusion": "Добавить исключение из списка", + "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку." } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 787097f4e..32a93393a 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -119,7 +119,7 @@ "CustomFormatsSpecificationRegularExpression": "Düzenli ifade", "AppDataDirectory": "Uygulama Veri Dizini", "ChownGroup": "Chown Grubu", - "ConditionUsingRegularExpressions": "Bu koşul Normal İfadeler kullanılarak eşleşir. `\\^$.|?*+()[{` karakterlerinin özel anlamlara sahip olduğunu ve `\\` ile kaçılması gerektiğini unutmayın.", + "ConditionUsingRegularExpressions": "Bu koşul Normal İfadeler kullanılarak eşleşir. `\\^$.|?*+()[{` karakterlerinin özel anlamlara sahip olduğunu ve `\\` ile kaçılması gerektiğini unutmayın", "BlackholeFolderHelpText": "{appName} uygulamasının {extension} dosyasını depolayacağı klasör", "BlackholeWatchFolder": "İzleme Klasörü", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimum Özel Format Puanı", @@ -136,7 +136,7 @@ "AuthenticationMethod": "Kimlik Doğrulama Yöntemi", "AuthenticationRequired": "Kimlik Doğrulama Gerekli", "AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.", - "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz.", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz", "ClearBlocklistMessageText": "Engellenenler listesindeki tüm öğeleri temizlemek istediğinizden emin misiniz?", "AutomaticUpdatesDisabledDocker": "Docker güncelleme mekanizması kullanıldığında otomatik güncellemeler doğrudan desteklenmez. Kapsayıcı görüntüsünü {appName} dışında güncellemeniz veya bir komut dosyası kullanmanız gerekecek", "ConnectionLostReconnect": "{appName} otomatik bağlanmayı deneyecek veya aşağıda yeniden yükle seçeneğini işaretleyebilirsiniz.", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 0c6733450..2083d9c24 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -62,7 +62,7 @@ "Metadata": "元数据", "CountSeasons": "第 {count} 季", "DownloadClientCheckNoneAvailableHealthCheckMessage": "无可用的下载客户端", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{downloadClientName}进行通讯", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{downloadClientName}进行通讯. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "下载客户端{downloadClientName}将下载内容放在根文件夹{rootFolderPath}中。您不应该下载到根文件夹。", "DownloadClientSortingHealthCheckMessage": "下载客户端{downloadClientName}已为{appName}的分类启用{sortingMode}排序。您应该在下载客户端中禁用排序,以避免导入问题。", "DownloadClientStatusAllClientHealthCheckMessage": "所有下载客户端都不可用", @@ -71,7 +71,7 @@ "Enabled": "已启用", "Ended": "已完结", "ImportListRootFolderMissingRootHealthCheckMessage": "在导入列表中缺少根目录文件夹:{rootFolderInfo}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "所有的列表因错误不可用", "ImportListStatusUnavailableHealthCheckMessage": "列表因错误不可用:{importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "如果可能,启用完整的下载处理(不支持多台计算机)", @@ -276,7 +276,7 @@ "RemotePathMappingGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}将文件下载在{path}中,但{appName}无法找到此目录。您可能需要调整文件夹的权限。", "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "本地下载客户端{downloadClientName}将文件下载在{path}中,但这不是有效的{osName}路径。查看您的下载客户端设置。", "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "远程下载客户端{downloadClientName}报告了{path}中的文件,但此目录似乎不存在。可能缺少远程路径映射。", - "IRCLinkText": "#Libera上的{appName}", + "IRCLinkText": "#sonarr - Libera", "LiberaWebchat": "Libera聊天", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "您正在使用Docker;下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}中的路径。查看Docker路径映射并更新下载客户端设置。", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}报告的文件在{path},但{appName}无法查看此目录。您可能需要调整文件夹的权限。", @@ -825,7 +825,7 @@ "SelectEpisodes": "选择剧集", "SelectSeason": "选择季", "LibraryImportTipsQualityInEpisodeFilename": "确保您的文件在其文件名中包含质量。例如:`episode.s02e15.bluray.mkv`", - "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExamp}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", + "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExample}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", "ListQualityProfileHelpText": "质量配置列表项将添加", "SeriesIndexFooterMissingMonitored": "缺失集(剧集已监控)", "SeriesIsMonitored": "剧集被监控", @@ -907,7 +907,7 @@ "TorrentDelay": "Torrent延时", "ToggleUnmonitoredToMonitored": "未监控,单击进行监控", "Upcoming": "即将播出", - "ProgressBarProgress": "进度栏位于{Progress}%", + "ProgressBarProgress": "进度栏位于{progress}%", "Usenet": "Usenet", "Week": "周", "Standard": "标准", @@ -966,7 +966,7 @@ "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答]({url})获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", - "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", + "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", "Max": "最大的", "MaximumSingleEpisodeAgeHelpText": "在整季搜索期间,当该季的最后一集比此设置旧时,只允许获取整季包。仅限标准剧集。填写 0 可禁用此设置。", "MaximumSize": "最大文件体积", @@ -994,7 +994,7 @@ "MoreDetails": "更多详细信息", "More": "更多", "MoveAutomatically": "自动移动", - "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{originalPath}' ?", + "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{destinationPath}' ?", "MoveSeriesFoldersMoveFiles": "是,移动文件", "MultiEpisode": "多集", "MultiEpisodeStyle": "多集风格", @@ -1585,7 +1585,7 @@ "IndexerSettingsSeedRatioHelpText": "种子在停止之前应达到的比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", "IndexerValidationTestAbortedDueToError": "测试因错误而中止:{exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "索引器不支持所需的搜索参数", - "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {异常消息}。", + "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {exceptionMessage}。", "IndexerValidationUnableToConnectInvalidCredentials": "无法连接到索引器,凭据无效。{exceptionMessage}。", "IndexerValidationUnableToConnectResolutionFailure": "与索引器连接失败。 请检查与索引器服务器和 DNS 的连接。{exceptionMessage}。", "IndexerValidationUnableToConnectServerUnavailable": "无法连接到索引器,索引器的服务器不可用。 请稍后再试。{exceptionMessage}。", @@ -1601,7 +1601,7 @@ "DownloadClientFloodSettingsPostImportTagsHelpText": "导入下载后附加标签。", "DownloadClientFloodSettingsStartOnAdd": "添加并开始", "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。 要被识别,下载必须具有所有初始标签。 这可以避免与不相关的下载发生冲突。", - "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因:{错误描述}", + "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因 {errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "API 地址", "DownloadClientFreeboxSettingsAppToken": "App Token", "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查“主机名”、“端口”或“使用 SSL”的设置(错误: {exceptionMessage})", From 61a7515041b75fa7809b0d55c051ce707d62aca0 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Fri, 8 Mar 2024 01:32:30 +0000 Subject: [PATCH 21/98] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 54c1bdd7b..cd2552f4d 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -9528,6 +9528,9 @@ "type": "integer", "format": "int32" }, + "releaseType": { + "$ref": "#/components/schemas/ReleaseType" + }, "rejections": { "type": "array", "items": { From 13e29bd257ccfccb09e66c940ffabeb6503c05b5 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 7 Mar 2024 21:34:57 +0200 Subject: [PATCH 22/98] Prevent NullRef in naming when truncating a null Release Group --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f527dd334..e8abe01c4 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -622,7 +622,7 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); - tokenHandlers["{Release Group}"] = m => Truncate(episodeFile.ReleaseGroup, m.CustomFormat) ?? m.DefaultValue("Sonarr"); + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup.IsNullOrWhiteSpace() ? m.DefaultValue("Sonarr") : Truncate(episodeFile.ReleaseGroup, m.CustomFormat); } private void AddQualityTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series, EpisodeFile episodeFile) @@ -1168,6 +1168,11 @@ namespace NzbDrone.Core.Organizer private string Truncate(string input, string formatter) { + if (input.IsNullOrWhiteSpace()) + { + return string.Empty; + } + var maxLength = GetMaxLengthFromFormatter(formatter); if (maxLength == 0 || input.Length <= Math.Abs(maxLength)) From a12cdb34bc0ab78937e3c3677012bf030923aebf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 3 Mar 2024 11:48:49 -0800 Subject: [PATCH 23/98] Fixed: Error sending Manual Interaction Required notification --- .../Notifications/NotificationService.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 1dbd1fe9d..649f69581 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -233,15 +233,33 @@ namespace NzbDrone.Core.Notifications public void Handle(ManualInteractionRequiredEvent message) { + var series = message.Episode.Series; + var mess = ""; + + if (series != null) + { + mess = GetMessage(series, message.Episode.Episodes, message.Episode.ParsedEpisodeInfo.Quality); + } + + if (mess.IsNullOrWhiteSpace() && message.TrackedDownload.DownloadItem != null) + { + mess = message.TrackedDownload.DownloadItem.Title; + } + + if (mess.IsNullOrWhiteSpace()) + { + return; + } + var manualInteractionMessage = new ManualInteractionRequiredMessage { - Message = GetMessage(message.Episode.Series, message.Episode.Episodes, message.Episode.ParsedEpisodeInfo.Quality), - Series = message.Episode.Series, + Message = mess, + Series = series, Quality = message.Episode.ParsedEpisodeInfo.Quality, Episode = message.Episode, TrackedDownload = message.TrackedDownload, - DownloadClientInfo = message.TrackedDownload.DownloadItem.DownloadClientInfo, - DownloadId = message.TrackedDownload.DownloadItem.DownloadId, + DownloadClientInfo = message.TrackedDownload.DownloadItem?.DownloadClientInfo, + DownloadId = message.TrackedDownload.DownloadItem?.DownloadId, Release = message.Release }; From 89bef4af99da90df608dbdaad42d87544eb27038 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:50:45 +0200 Subject: [PATCH 24/98] New: Wider modal for Interactive Search and Manual Import --- frontend/src/Components/Modal/Modal.css | 10 +++++++++- frontend/src/Components/Modal/Modal.css.d.ts | 1 + frontend/src/Episode/EpisodeDetailsModal.js | 2 +- frontend/src/Helpers/Props/sizes.js | 4 ++-- .../src/InteractiveImport/InteractiveImportModal.tsx | 2 +- .../src/Series/Search/SeasonInteractiveSearchModal.js | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css index 33f849945..f7a229501 100644 --- a/frontend/src/Components/Modal/Modal.css +++ b/frontend/src/Components/Modal/Modal.css @@ -63,6 +63,13 @@ width: 1280px; } + +.extraExtraLarge { + composes: modal; + + width: 1600px; +} + @media only screen and (max-width: $breakpointExtraLarge) { .modal.extraLarge { width: 90%; @@ -90,7 +97,8 @@ .modal.small, .modal.medium, .modal.large, - .modal.extraLarge { + .modal.extraLarge, + .modal.extraExtraLarge { max-height: 100%; width: 100%; height: 100% !important; diff --git a/frontend/src/Components/Modal/Modal.css.d.ts b/frontend/src/Components/Modal/Modal.css.d.ts index b6576c7de..e582ce0f9 100644 --- a/frontend/src/Components/Modal/Modal.css.d.ts +++ b/frontend/src/Components/Modal/Modal.css.d.ts @@ -1,6 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'extraExtraLarge': string; 'extraLarge': string; 'large': string; 'medium': string; diff --git a/frontend/src/Episode/EpisodeDetailsModal.js b/frontend/src/Episode/EpisodeDetailsModal.js index cd2e20e74..0e9583e3a 100644 --- a/frontend/src/Episode/EpisodeDetailsModal.js +++ b/frontend/src/Episode/EpisodeDetailsModal.js @@ -37,7 +37,7 @@ class EpisodeDetailsModal extends Component { return ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={this.state.closeOnBackgroundClick} onModalClose={onModalClose} > diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js index d7f85df5e..6ac15f3bd 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.js @@ -3,5 +3,5 @@ export const SMALL = 'small'; export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; - -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; +export const EXTRA_EXTRA_LARGE = 'extraExtraLarge'; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.tsx b/frontend/src/InteractiveImport/InteractiveImportModal.tsx index 37b26012e..11dc8e6ae 100644 --- a/frontend/src/InteractiveImport/InteractiveImportModal.tsx +++ b/frontend/src/InteractiveImport/InteractiveImportModal.tsx @@ -47,7 +47,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) { return ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={false} onModalClose={onModalClose} > diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js b/frontend/src/Series/Search/SeasonInteractiveSearchModal.js index a2210222a..861c9113c 100644 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModal.js @@ -15,7 +15,7 @@ function SeasonInteractiveSearchModal(props) { return ( <Modal isOpen={isOpen} - size={sizes.EXTRA_LARGE} + size={sizes.EXTRA_EXTRA_LARGE} closeOnBackgroundClick={false} onModalClose={onModalClose} > From a0329adeba1ccda49d24b12f9ef9281588c9fc89 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 9 Mar 2024 22:51:29 -0600 Subject: [PATCH 25/98] Improve single file detected as full season messaging --- .../EpisodeImport/Specifications/FullSeasonSpecification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 64734ed01..4d13eda6f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -23,8 +23,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (localEpisode.FileEpisodeInfo.FullSeason) { - _logger.Debug("Single episode file detected as containing all episodes in the season"); - return Decision.Reject("Single episode file contains all episodes in seasons"); + _logger.Debug("Single episode file detected as containing all episodes in the season due to no episode parsed from the file name."); + return Decision.Reject("Single episode file contains all episodes in seasons. Review file name or manually import"); } return Decision.Accept(); From 48cb5d227187a06930aad5ee1b4e7b76422d8421 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Sat, 9 Mar 2024 22:53:02 -0600 Subject: [PATCH 26/98] New: 'Custom Format: Format Name' rename token --- .../MediaManagement/Naming/NamingModal.js | 3 +- .../CustomFormatsFixture.cs | 122 ++++++++++++++++++ .../CustomFormats/CustomFormat.cs | 2 +- .../Organizer/FileNameBuilder.cs | 11 +- 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 509c5d940..9af6a1160 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -150,7 +150,8 @@ const mediaInfoTokens = [ const otherTokens = [ { token: '{Release Group}', example: 'Rls Grp' }, - { token: '{Custom Formats}', example: 'iNTERNAL' } + { token: '{Custom Formats}', example: 'iNTERNAL' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' } ]; const originalTokens = [ diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs new file mode 100644 index 000000000..fb644e104 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + + public class CustomFormatsFixture : CoreTest<FileNameBuilder> + { + private Series _series; + private Episode _episode1; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + private List<CustomFormat> _customFormats; + + [SetUp] + public void Setup() + { + _series = Builder<Series> + .CreateNew() + .With(s => s.Title = "South Park") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock<INamingConfigService>() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _episode1 = Builder<Episode>.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.SeasonNumber = 15) + .With(e => e.EpisodeNumber = 6) + .With(e => e.AbsoluteEpisodeNumber = 100) + .Build(); + + _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" }; + + _customFormats = new List<CustomFormat>() + { + new CustomFormat() + { + Name = "INTERNAL", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "AMZN", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "NAME WITH SPACES", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "NotIncludedFormat", + IncludeCustomFormatWhenRenaming = false + } + }; + + Mocker.GetMock<IQualityDefinitionService>() + .Setup(v => v.Get(Moq.It.IsAny<Quality>())) + .Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + } + + [TestCase("{Custom Formats}", "INTERNAL AMZN NAME WITH SPACES")] + public void should_replace_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Formats}", "")] + public void should_replace_custom_formats_with_no_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>()) + .Should().Be(expected); + } + + [TestCase("{Custom Format}", "")] + [TestCase("{Custom Format:INTERNAL}", "INTERNAL")] + [TestCase("{Custom Format:AMZN}", "AMZN")] + [TestCase("{Custom Format:NAME WITH SPACES}", "NAME WITH SPACES")] + [TestCase("{Custom Format:DOESNOTEXIST}", "")] + [TestCase("{Custom Format:INTERNAL} - {Custom Format:AMZN}", "INTERNAL - AMZN")] + [TestCase("{Custom Format:AMZN} - {Custom Format:INTERNAL}", "AMZN - INTERNAL")] + public void should_replace_custom_format(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Format}", "")] + [TestCase("{Custom Format:INTERNAL}", "")] + [TestCase("{Custom Format:AMZN}", "")] + public void should_replace_custom_format_with_no_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>()) + .Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs index 51e61e287..8c58b1e07 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index e8abe01c4..947d5f555 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached<bool> _patternHasEpisodeIdentifierCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9+-]+(?<!-)))?(?<suffix>[- ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", @@ -698,6 +698,15 @@ namespace NzbDrone.Core.Organizer } tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); + tokenHandlers["{Custom Format}"] = m => + { + if (m.CustomFormat.IsNullOrWhiteSpace()) + { + return string.Empty; + } + + return customFormats.Where(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat).FirstOrDefault()?.ToString() ?? string.Empty; + }; } private void AddIdTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series) From d86aeb7472f7f514c6793172e3858af299472bf8 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Sat, 9 Mar 2024 22:54:06 -0600 Subject: [PATCH 27/98] New: Release Hash renaming token Closes #6570 --- .../MediaManagement/Naming/NamingModal.js | 22 +++++ .../AggregateReleaseHashFixture.cs | 83 +++++++++++++++++++ .../FileNameBuilderFixture.cs | 22 +++++ .../ParserTests/AnimeMetadataParserFixture.cs | 32 ++++++- .../Migration/204_add_release_hash.cs | 76 +++++++++++++++++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../Aggregators/AggregateReleaseHash.cs | 41 +++++++++ .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../Organizer/FileNameBuilder.cs | 1 + .../Parser/Model/LocalEpisode.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 48 +++++------ 11 files changed, 303 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 9af6a1160..f873ec1d9 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -154,6 +154,10 @@ const otherTokens = [ { token: '{Custom Format:FormatName}', example: 'AMZN' } ]; +const otherAnimeTokens = [ + { token: '{Release Hash}', example: 'ABCDEFGH' } +]; + const originalTokens = [ { token: '{Original Title}', example: 'The.Series.Title\'s!.S01E01.WEBDL.1080p.x264-EVOLVE' }, { token: '{Original Filename}', example: 'the.series.title\'s!.s01e01.webdl.1080p.x264-EVOLVE' } @@ -535,6 +539,24 @@ class NamingModal extends Component { } ) } + + { + anime && otherAnimeTokens.map(({ token, example }) => { + return ( + <NamingOption + key={token} + name={name} + value={value} + token={token} + example={example} + tokenSeparator={tokenSeparator} + tokenCase={tokenCase} + onPress={this.onOptionPress} + /> + ); + } + ) + } </div> </FieldSet> diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs new file mode 100644 index 000000000..e3e8b848c --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs @@ -0,0 +1,83 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + [TestFixture] + public class AggregateReleaseHashFixture : CoreTest<AggregateReleaseHash> + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder<Series>.CreateNew().Build(); + } + + [Test] + public void should_prefer_file() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCDEFGH]"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("ABCDEFGH"); + } + + [Test] + public void should_fallback_to_downloadclient() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("ABCD1234"); + } + + [Test] + public void should_fallback_to_folder() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("12345678"); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 521d2d8d9..3b0cdb0af 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -991,6 +991,28 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests result.Should().EndWith("HDR"); } + [Test] + public void should_replace_release_hash_with_stored_hash() + { + _namingConfig.StandardEpisodeFormat = "{Release Hash}"; + + _episodeFile.ReleaseHash = "ABCDEFGH"; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile) + .Should().Be("ABCDEFGH"); + } + + [Test] + public void should_replace_null_release_hash_with_empty_string() + { + _namingConfig.StandardEpisodeFormat = "{Release Hash}"; + + _episodeFile.ReleaseHash = null; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile) + .Should().Be(string.Empty); + } + private void GivenMediaInfoModel(string videoCodec = "h264", string audioCodec = "dts", int audioChannels = 6, diff --git a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs index 5a1f8bef4..52794f643 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs @@ -22,12 +22,42 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")] [TestCase("[ACX]Series Title 01 Episode Name [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")] [TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")] - public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash) + + // These tests are dupes of the above, except with parenthesized hashes instead of square bracket + [TestCase("[SubDESU]_Show_Title_DxD_07_(1280x720_x264-AAC)_(6B7FD717)", "SubDESU", "6B7FD717")] + [TestCase("[Chihiro]_Show_Title!!_-_06_[848x480_H.264_AAC](859EEAFA)", "Chihiro", "859EEAFA")] + [TestCase("[Underwater]_Show_Title_-_12_(720p)_(5C7BC4F9)", "Underwater", "5C7BC4F9")] + [TestCase("[HorribleSubs]_Show_Title_-_33_[720p]", "HorribleSubs", "")] + [TestCase("[HorribleSubs] Show-Title - 13 [1080p].mkv", "HorribleSubs", "")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].(C65D4B1F).mkv", "Doremi", "C65D4B1F")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31[1280x720].(C65D4B1F)", "Doremi", "C65D4B1F")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")] + [TestCase("[K-F] Series Title 214", "K-F", "")] + [TestCase("[K-F] Series Title S10E14 214", "K-F", "")] + [TestCase("[K-F] Series Title 10x14 214", "K-F", "")] + [TestCase("[K-F] Series Title 214 10x14", "K-F", "")] + [TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")] + [TestCase("[ACX]Series Title 01 Episode Name [Kosaka] (9C57891E).mkv", "ACX", "9C57891E")] + [TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) (59B3F2EA).mkv", "S-T-D", "59B3F2EA")] + public void should_parse_releasegroup_and_hash(string postTitle, string subGroup, string hash) { var result = Parser.Parser.ParseTitle(postTitle); result.Should().NotBeNull(); result.ReleaseGroup.Should().Be(subGroup); result.ReleaseHash.Should().Be(hash); } + + [TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [8B00F2EA].mkv", "8B00F2EA")] + [TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [10BBF2EA].mkv", "10BBF2EA")] + [TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [008BF28B].mkv", "008BF28B")] + [TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [000BF10B].mkv", "000BF10B")] + [TestCase("[DHD] Series Title! - 08 (1280x720 8bit AAC) [8B8BF2EA].mkv", "8B8BF2EA")] + [TestCase("[DHD] Series Title! - 10 (1280x720 8bit AAC) [10B10BEA].mkv", "10B10BEA")] + public void should_parse_release_hashes_with_10b_or_8b(string postTitle, string hash) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.ReleaseHash.Should().Be(hash); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs b/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs new file mode 100644 index 000000000..887d35cda --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Data; +using System.IO; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(204)] + public class add_add_release_hash : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles").AddColumn("ReleaseHash").AsString().Nullable(); + + Execute.WithConnection(UpdateEpisodeFiles); + } + + private void UpdateEpisodeFiles(IDbConnection conn, IDbTransaction tran) + { + var updates = new List<object>(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"SceneName\", \"RelativePath\", \"OriginalFilePath\" FROM \"EpisodeFiles\""; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var id = reader.GetInt32(0); + var sceneName = reader[1] as string; + var relativePath = reader[2] as string; + var originalFilePath = reader[3] as string; + + ParsedEpisodeInfo parsedEpisodeInfo = null; + + var originalTitle = sceneName; + + if (originalTitle.IsNullOrWhiteSpace() && originalFilePath.IsNotNullOrWhiteSpace()) + { + originalTitle = Path.GetFileNameWithoutExtension(originalFilePath); + } + + if (originalTitle.IsNotNullOrWhiteSpace()) + { + parsedEpisodeInfo = Parser.Parser.ParseTitle(originalTitle); + } + + if (parsedEpisodeInfo == null || parsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) + { + parsedEpisodeInfo = Parser.Parser.ParseTitle(Path.GetFileNameWithoutExtension(relativePath)); + } + + if (parsedEpisodeInfo != null && parsedEpisodeInfo.ReleaseHash.IsNotNullOrWhiteSpace()) + { + updates.Add(new + { + Id = id, + ReleaseHash = parsedEpisodeInfo.ReleaseHash + }); + } + } + } + + if (updates.Count > 0) + { + var updateEpisodeFilesSql = "UPDATE \"EpisodeFiles\" SET \"ReleaseHash\" = @ReleaseHash WHERE \"Id\" = @Id"; + conn.Execute(updateEpisodeFilesSql, updates, transaction: tran); + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 8dee12c2b..cd810a457 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.MediaFiles public string OriginalFilePath { get; set; } public string SceneName { get; set; } public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } public QualityModel Quality { get; set; } public IndexerFlags IndexerFlags { get; set; } public MediaInfoModel MediaInfo { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs new file mode 100644 index 000000000..a2012de14 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs @@ -0,0 +1,41 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateReleaseHash : IAggregateLocalEpisode + { + public int Order => 1; + + public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + var releaseHash = GetReleaseHash(localEpisode.FileEpisodeInfo); + + if (releaseHash.IsNullOrWhiteSpace()) + { + releaseHash = GetReleaseHash(localEpisode.DownloadClientEpisodeInfo); + } + + if (releaseHash.IsNullOrWhiteSpace()) + { + releaseHash = GetReleaseHash(localEpisode.FolderEpisodeInfo); + } + + localEpisode.ReleaseHash = releaseHash; + + return localEpisode; + } + + private string GetReleaseHash(ParsedEpisodeInfo episodeInfo) + { + // ReleaseHash doesn't make sense for a FullSeason, since hashes should be specific to a file + if (episodeInfo == null || episodeInfo.FullSeason) + { + return null; + } + + return episodeInfo.ReleaseHash; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 74e2a71e6..d591a068d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -95,6 +95,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; + episodeFile.ReleaseHash = localEpisode.ReleaseHash; episodeFile.Languages = localEpisode.Languages; // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 947d5f555..c09978bfa 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -623,6 +623,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup.IsNullOrWhiteSpace() ? m.DefaultValue("Sonarr") : Truncate(episodeFile.ReleaseGroup, m.CustomFormat); + tokenHandlers["{Release Hash}"] = m => episodeFile.ReleaseHash ?? string.Empty; } private void AddQualityTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index af7c7347c..65f6e84f8 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -37,6 +37,7 @@ namespace NzbDrone.Core.Parser.Model public bool ExistingFile { get; set; } public bool SceneSource { get; set; } public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } public string SceneName { get; set; } public bool OtherVideoFiles { get; set; } public List<CustomFormat> CustomFormats { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 693eaa36d..aa2a121b9 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -83,11 +83,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Season+Episode - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:[_. ](?!\d+)).*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number + Season+Episode @@ -95,39 +95,39 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Season+Episode + Absolute Episode Number - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|\-[a-z])))+.*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|\-[a-z])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - Batch separated with tilde - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\s?~\s?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\s?~\s?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with season number in brackets Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)[_. ]+?\(Season[_. ](?<season>\d+)\)[-_. ]+?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)[_. ]+?\(Season[_. ](?<season>\d+)\)[-_. ]+?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing 3-digit number and sub title - Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+)[_ ]+)(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+)[_ ]+)(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title - Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number - Absolute Episode Number (batches without full separator between title and absolute episode numbers) - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Multi-episode Repeated (S01E05 - S01E06) @@ -155,11 +155,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [SubGroup] [Hash]? (Series Title Episode 99-100 [RlsGroup] [ABCD1234]) - new Regex(@"^(?<title>.+?)[-_. ]Episode(?:[-_. ]+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?<title>.+?)[-_. ]Episode(?:[-_. ]+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [SubGroup] [Hash] - new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (Year) [SubGroup] @@ -167,11 +167,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title with trailing number, Absolute Episode Number and hash - new Regex(@"^(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])(?:$|\.mkv)", + new Regex(@"^(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [Hash] - new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)", + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>[(\[]\w{8}[)\]])$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with airdate AND season/episode number, capture season/episode only @@ -358,7 +358,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (E195 or E1206) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>(\d{3}|\d{4})(\.\d{1,2})?))+[-_. ].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>(\d{3}|\d{4})(\.\d{1,2})?))+[-_. ].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Supports 1103/1113 naming @@ -386,27 +386,27 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime Range - Title Absolute Episode Number (ep01-12) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:_|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?)-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:_|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?)-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (e66) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,4}(\.\d{1,2})?))+[-_. ].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,4}(\.\d{1,2})?))+[-_. ].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Episode Absolute Episode Number (Series Title Episode 01) - new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime Range - Title Absolute Episode Number (1 or 2 digit absolute episode numbers in a range, 1-10) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-))(?:_|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title {Absolute Episode Number} - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Extant, terrible multi-episode naming (extant.10708.hdtv-lol.mp4) @@ -492,7 +492,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?|10-bit)\s*?", + private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)(b(?![a-z0-9])|bit)|10-bit)\s*?", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -1197,7 +1197,7 @@ namespace NzbDrone.Core.Parser if (hash.Success) { - var hashValue = hash.Value.Trim('[', ']'); + var hashValue = hash.Value.Trim('[', ']', '(', ')'); if (hashValue.Equals("1280x720")) { From 2ec071a5ecab8f5056d179feaaef0147abb944ca Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sun, 10 Mar 2024 04:54:21 +0000 Subject: [PATCH 28/98] Update release profile download client warning --- .../HealthCheck/Checks/DownloadClientRootFolderCheck.cs | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 +- src/NzbDrone.Core/Localization/Core/es.json | 1 - src/NzbDrone.Core/Localization/Core/fi.json | 1 - src/NzbDrone.Core/Localization/Core/fr.json | 1 - src/NzbDrone.Core/Localization/Core/hu.json | 1 - src/NzbDrone.Core/Localization/Core/pt_BR.json | 1 - src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 8 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs index 4f0a6de05..c11f8bc13 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.HealthCheck.Checks _localizationService.GetLocalizedString("DownloadClientRootFolderHealthCheckMessage", new Dictionary<string, object> { { "downloadClientName", client.Definition.Name }, - { "path", folder.FullPath } + { "rootFolderPath", folder.FullPath } }), "#downloads-in-root-folder"); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 15674ddc7..ce503dd49 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1590,7 +1590,7 @@ "ReleaseHash": "Release Hash", "ReleaseProfile": "Release Profile", "ReleaseProfileIndexerHelpText": "Specify what indexer the profile applies to", - "ReleaseProfileIndexerHelpTextWarning": "Using a specific indexer with release profiles can lead to duplicate releases being grabbed", + "ReleaseProfileIndexerHelpTextWarning": "Setting a specific indexer on a release profile will cause this profile to only apply to releases from that indexer.", "ReleaseProfileTagSeriesHelpText": "Release profiles will apply to series with at least one matching tag. Leave blank to apply to all series", "ReleaseProfiles": "Release Profiles", "ReleaseProfilesLoadError": "Unable to load Release Profiles", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 0093deca1..c6ab4d35a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1644,7 +1644,6 @@ "Pending": "Pendiente", "QualityDefinitions": "Definiciones de calidad", "RecyclingBin": "Papelera de reciclaje", - "ReleaseProfileIndexerHelpTextWarning": "Usar un indexador específico con perfiles de lanzamientos puede conllevar que lanzamientos duplicados sean capturados", "ReleaseTitle": "Título de lanzamiento", "RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente", "Remove": "Eliminar", diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index c00cd1c8a..adbb36ef8 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1395,7 +1395,6 @@ "NotificationsValidationInvalidUsernamePassword": "Virheellinen käyttäjätunnus tai salasana", "QueueFilterHasNoItems": "Mikään kohde ei vastaa valittua jonon suodatinta", "RegularExpression": "Säännöllinen lauseke", - "ReleaseProfileIndexerHelpTextWarning": "Tietyn tietolähteen käyttö julkaisuprofiileilla saattaa aiheuttaa julkaisujen kaksoiskappaleiden kaappauksia.", "ReleaseSceneIndicatorUnknownMessage": "Jakson numerointi vaihtelee, eikä julkaisu vastaa mitään tunnettua numerointia.", "DownloadClientSabnzbdValidationEnableJobFolders": "Käytä työkansioita", "EpisodeFileDeleted": "Jaksotiedosto poistettiin", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 438e7cafe..55a004379 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -654,7 +654,6 @@ "ReleaseGroups": "Groupes de version", "ReleaseHash": "Somme de contrôle de la version", "ReleaseProfile": "Profil de version", - "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double.", "ReleaseProfiles": "Profils de version", "ReleaseProfilesLoadError": "Impossible de charger les profils de version", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Le client de téléchargement {downloadClientName} place les téléchargements dans {path} mais {appName} ne peut pas voir ce répertoire. Vous devrez peut-être ajuster les autorisations du dossier.", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index c3213f302..f3a6bc1c2 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -894,7 +894,6 @@ "FormatRuntimeMinutes": "{minutes} p", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "A(z) {downloadClientName} letöltési kliens úgy van beállítva, hogy eltávolítsa a befejezett letöltéseket. Ez azt eredményezheti, hogy a letöltések eltávolításra kerülnek az ügyfélprogramból, mielőtt a {appName} importálhatná őket.", "RecyclingBinCleanupHelpTextWarning": "A kiválasztott napoknál régebbi fájlok a lomtárban automatikusan törlődnek", - "ReleaseProfileIndexerHelpTextWarning": "Egy adott indexelő kiadási profilokkal történő használata duplikált kiadások megragadásához vezethet", "RemotePath": "Távoli útvonal", "RelativePath": "Relatív út", "ReleaseProfile": "Release profil", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 0773f1dac..d21604d83 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -772,7 +772,6 @@ "RegularExpressionsTutorialLink": "Mais detalhes sobre expressões regulares podem ser encontrados [aqui]({url}).", "ReleaseProfile": "Perfil de Lançamento", "ReleaseProfileIndexerHelpText": "Especifique a qual indexador o perfil se aplica", - "ReleaseProfileIndexerHelpTextWarning": "Usar um indexador específico com perfis de lançamento pode levar à captura de lançamentos duplicados", "ReleaseProfileTagSeriesHelpText": "Os perfis de lançamento serão aplicados a séries com pelo menos uma tag correspondente. Deixe em branco para aplicar a todas as séries", "ReleaseProfiles": "Perfis de Lançamentos", "ReleaseProfilesLoadError": "Não foi possível carregar perfis de lançamentos", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 2083d9c24..925e731ca 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1082,7 +1082,6 @@ "RefreshAndScan": "刷新并扫描", "RefreshAndScanTooltip": "刷新信息并扫描磁盘", "ReleaseProfileIndexerHelpText": "指定配置文件应用于哪个索引器", - "ReleaseProfileIndexerHelpTextWarning": "使用有发布配置的特定索引器可能会导致重复获取发布", "ReleaseRejected": "发布被拒绝", "ReleaseSceneIndicatorAssumingTvdb": "推测TVDB编号。", "ReleaseSceneIndicatorMappedNotRequested": "在此搜索中未包含已映射的剧集。", From 4aa56e3f910d597d1b83609bd5715b51e38e6bfc Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 9 Mar 2024 23:35:50 -0800 Subject: [PATCH 29/98] Fixed: Parsing of some French and Spanish anime releases --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 37c54215c..427a3e480 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -132,6 +132,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)] [TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)] [TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)] + [TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)] + [TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index aa2a121b9..2989353ad 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number + Season+Episode @@ -401,6 +401,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - Title Episode/Episodio Absolute Episode Number + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+(?:Episode|Episodio)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - Title Absolute Episode Number new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 86034beccd14c873ce550ad9d3c32a5b4e1b4ab7 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sun, 10 Mar 2024 11:31:15 +0200 Subject: [PATCH 30/98] Bump ImageSharp, Polly, DryIoc, STJson, WindowsServices --- src/NzbDrone.Common/Sonarr.Common.csproj | 6 +++--- src/NzbDrone.Core/Sonarr.Core.csproj | 6 +++--- src/NzbDrone.Host/Sonarr.Host.csproj | 4 ++-- src/NzbDrone.Update/Sonarr.Update.csproj | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 831798a2e..afd914994 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -4,16 +4,16 @@ <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> </PropertyGroup> <ItemGroup> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" /> <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="Sentry" Version="4.0.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" /> - <PackageReference Include="System.Text.Json" Version="6.0.8" /> + <PackageReference Include="System.Text.Json" Version="6.0.9" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" /> diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index d7a4c9fa5..269005cfb 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -7,7 +7,7 @@ <PackageReference Include="Diacritical.Net" Version="1.0.4" /> <PackageReference Include="MailKit" Version="3.6.0" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" /> - <PackageReference Include="Polly" Version="8.2.0" /> + <PackageReference Include="Polly" Version="8.3.1" /> <PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" /> <PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" /> <PackageReference Include="System.Memory" Version="4.5.5" /> @@ -18,12 +18,12 @@ <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="NLog" Version="4.7.14" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> - <PackageReference Include="System.Text.Json" Version="6.0.8" /> + <PackageReference Include="System.Text.Json" Version="6.0.9" /> <PackageReference Include="Npgsql" Version="7.0.4" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index e11144bcf..0ccf5c4a2 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -8,8 +8,8 @@ <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index ab05b2ca4..624151093 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -4,7 +4,7 @@ <TargetFrameworks>net6.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> <PackageReference Include="NLog" Version="4.7.14" /> </ItemGroup> From 6584d95331d0e0763e1688a397a3ccaf5fa6ca38 Mon Sep 17 00:00:00 2001 From: Alan Collins <alanollv@gmail.com> Date: Wed, 13 Mar 2024 23:46:33 -0500 Subject: [PATCH 31/98] New: Update Custom Format renaming token to allow excluding specific formats Closes #6615 --- .../CustomFormatsFixture.cs | 24 +++++++++++++++++ .../Organizer/FileNameBuilder.cs | 27 +++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs index fb644e104..b07fbedbe 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs @@ -93,6 +93,30 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(expected); } + [TestCase("{Custom Formats:-INTERNAL}", "AMZN NAME WITH SPACES")] + [TestCase("{Custom Formats:-NAME WITH SPACES}", "INTERNAL AMZN")] + [TestCase("{Custom Formats:-INTERNAL,NAME WITH SPACES}", "AMZN")] + [TestCase("{Custom Formats:INTERNAL}", "INTERNAL")] + [TestCase("{Custom Formats:NAME WITH SPACES}", "NAME WITH SPACES")] + [TestCase("{Custom Formats:INTERNAL,NAME WITH SPACES}", "INTERNAL NAME WITH SPACES")] + public void should_replace_custom_formats_with_filtered_names(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Formats:-}", "{Custom Formats:-}")] + [TestCase("{Custom Formats:}", "{Custom Formats:}")] + public void should_not_replace_custom_formats_due_to_invalid_token(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + [TestCase("{Custom Format}", "")] [TestCase("{Custom Format:INTERNAL}", "INTERNAL")] [TestCase("{Custom Format:AMZN}", "AMZN")] diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c09978bfa..7e03172ef 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached<bool> _patternHasEpisodeIdentifierCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", @@ -698,7 +698,7 @@ namespace NzbDrone.Core.Organizer customFormats = _formatCalculator.ParseCustomFormat(episodeFile, series); } - tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); + tokenHandlers["{Custom Formats}"] = m => GetCustomFormatsToken(customFormats, m.CustomFormat); tokenHandlers["{Custom Format}"] = m => { if (m.CustomFormat.IsNullOrWhiteSpace()) @@ -717,6 +717,29 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty; } + private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter) + { + var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming); + + var filteredTokens = tokens; + + if (filter.IsNotNullOrWhiteSpace()) + { + if (filter.StartsWith("-")) + { + var splitFilter = filter.Substring(1).Split(','); + filteredTokens = tokens.Where(c => !splitFilter.Contains(c.Name)).ToList(); + } + else + { + var splitFilter = filter.Split(','); + filteredTokens = tokens.Where(c => splitFilter.Contains(c.Name)).ToList(); + } + } + + return string.Join(" ", filteredTokens); + } + private string GetLanguagesToken(List<string> mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { var tokens = new List<string>(); From 4d4d63921b45477b8f3783f4094c30ff3b85db7a Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:47:01 -0700 Subject: [PATCH 32/98] Add notification for build success/failures --- .github/workflows/build.yml | 24 ++++++++++++++++++- .../MediaManagement/Naming/NamingOption.css | 4 ++-- package.json | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c65b49f2..3834c7d35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: run: yarn lint - name: Stylelint - run: yarn stylelint + run: yarn stylelint -f github - name: Build run: yarn build --env production @@ -225,3 +225,25 @@ jobs: branch: ${{ github.ref_name }} major_version: ${{ needs.backend.outputs.major_version }} version: ${{ needs.backend.outputs.version }} + + notify: + name: Discord Notification + needs: [backend, unit_test, unit_test_postgres, integration_test] + if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }} + env: + STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }} + runs-on: ubuntu-latest + + steps: + - name: Notify + uses: tsickert/discord-webhook@v5.3.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + username: 'GitHub Actions' + avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png' + embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}" + embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' + embed-description: | + **Branch** ${{ github.ref }} + **Build** ${{ needs.backend.outputs.version }} + embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index a09c91ec8..204c93d0e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -26,7 +26,7 @@ .token { flex: 0 0 50%; - padding: 6px 6px; + padding: 6px; background-color: var(--popoverTitleBackgroundColor); font-family: $monoSpaceFontFamily; } @@ -36,7 +36,7 @@ align-items: center; justify-content: space-between; flex: 0 0 50%; - padding: 6px 6px; + padding: 6px; background-color: var(--popoverBodyBackgroundColor); .footNote { diff --git a/package.json b/package.json index 267e78f5f..05bac8b34 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "watch": "webpack --watch --config ./frontend/build/webpack.config.js", "lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/", "lint-fix": "yarn lint --fix", - "stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc" + "stylelint": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc" }, "repository": "https://github.com/Sonarr/Sonarr", "author": "Team Sonarr", From 6d552f2a60f44052079b5e8944f5e1bbabac56e0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 12 Mar 2024 22:34:47 -0700 Subject: [PATCH 33/98] New: Show Series title and season number after task name when applicable Closes #6601 --- frontend/src/Commands/Command.ts | 2 + .../Selectors/createMultiSeriesSelector.ts | 14 + .../src/System/Tasks/Queued/QueuedTaskRow.css | 9 - .../Tasks/Queued/QueuedTaskRow.css.d.ts | 2 - .../src/System/Tasks/Queued/QueuedTaskRow.js | 279 ------------------ .../src/System/Tasks/Queued/QueuedTaskRow.tsx | 238 +++++++++++++++ .../Tasks/Queued/QueuedTaskRowConnector.js | 31 -- .../Tasks/Queued/QueuedTaskRowNameCell.css | 8 + .../Queued/QueuedTaskRowNameCell.css.d.ts | 8 + .../Tasks/Queued/QueuedTaskRowNameCell.tsx | 54 ++++ .../src/System/Tasks/Queued/QueuedTasks.js | 90 ------ .../src/System/Tasks/Queued/QueuedTasks.tsx | 74 +++++ .../Tasks/Queued/QueuedTasksConnector.js | 46 --- frontend/src/System/Tasks/Tasks.js | 4 +- 14 files changed, 400 insertions(+), 459 deletions(-) create mode 100644 frontend/src/Store/Selectors/createMultiSeriesSelector.ts delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRow.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTasks.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTasks.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTasksConnector.js diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index 45a5beed7..0830fd34b 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -13,6 +13,8 @@ export interface CommandBody { trigger: string; suppressMessages: boolean; seriesId?: number; + seriesIds?: number[]; + seasonNumber?: number; } interface Command extends ModelBase { diff --git a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts new file mode 100644 index 000000000..119ccd1ee --- /dev/null +++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts @@ -0,0 +1,14 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +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]]); + } + ); +} + +export default createMultiSeriesSelector; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css index 034804711..6e38929c9 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css @@ -10,15 +10,6 @@ width: 100%; } -.commandName { - display: inline-block; - min-width: 220px; -} - -.userAgent { - color: #b0b0b0; -} - .queued, .started, .ended { diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts index 3bc00b738..2c6010533 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts @@ -2,14 +2,12 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'commandName': string; 'duration': string; 'ended': string; 'queued': string; 'started': string; 'trigger': string; 'triggerContent': string; - 'userAgent': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js deleted file mode 100644 index 8b8a62d3a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js +++ /dev/null @@ -1,279 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './QueuedTaskRow.css'; - -function getStatusIconProps(status, message) { - const title = titleCase(status); - - switch (status) { - case 'queued': - return { - name: icons.PENDING, - title - }; - - case 'started': - return { - name: icons.REFRESH, - isSpinning: true, - title - }; - - case 'completed': - return { - name: icons.CHECK, - kind: kinds.SUCCESS, - title: message === 'Completed' ? title : `${title}: ${message}` - }; - - case 'failed': - return { - name: icons.FATAL, - kind: kinds.DANGER, - title: `${title}: ${message}` - }; - - default: - return { - name: icons.UNKNOWN, - title - }; - } -} - -function getFormattedDates(props) { - const { - queued, - started, - ended, - showRelativeDates, - shortDateFormat - } = props; - - if (showRelativeDates) { - return { - queuedAt: moment(queued).fromNow(), - startedAt: started ? moment(started).fromNow() : '-', - endedAt: ended ? moment(ended).fromNow() : '-' - }; - } - - return { - queuedAt: formatDate(queued, shortDateFormat), - startedAt: started ? formatDate(started, shortDateFormat) : '-', - endedAt: ended ? formatDate(ended, shortDateFormat) : '-' - }; -} - -class QueuedTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - ...getFormattedDates(props), - isCancelConfirmModalOpen: false - }; - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - queued, - started, - ended - } = this.props; - - if ( - queued !== prevProps.queued || - started !== prevProps.started || - ended !== prevProps.ended - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Control - - setUpdateTimer() { - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, 30000); - } - - // - // Listeners - - onCancelPress = () => { - this.setState({ - isCancelConfirmModalOpen: true - }); - }; - - onAbortCancel = () => { - this.setState({ - isCancelConfirmModalOpen: false - }); - }; - - // - // Render - - render() { - const { - trigger, - commandName, - queued, - started, - ended, - status, - duration, - message, - clientUserAgent, - longDateFormat, - timeFormat, - onCancelPress - } = this.props; - - const { - queuedAt, - startedAt, - endedAt, - isCancelConfirmModalOpen - } = this.state; - - let triggerIcon = icons.QUICK; - - if (trigger === 'manual') { - triggerIcon = icons.INTERACTIVE; - } else if (trigger === 'scheduled') { - triggerIcon = icons.SCHEDULED; - } - - return ( - <TableRow> - <TableRowCell className={styles.trigger}> - <span className={styles.triggerContent}> - <Icon - name={triggerIcon} - title={titleCase(trigger)} - /> - - <Icon - {...getStatusIconProps(status, message)} - /> - </span> - </TableRowCell> - - <TableRowCell> - <span className={styles.commandName}> - {commandName} - </span> - { - clientUserAgent ? - <span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}> - {translate('From')}: {clientUserAgent} - </span> : - null - } - </TableRowCell> - - <TableRowCell - className={styles.queued} - title={formatDateTime(queued, longDateFormat, timeFormat)} - > - {queuedAt} - </TableRowCell> - - <TableRowCell - className={styles.started} - title={formatDateTime(started, longDateFormat, timeFormat)} - > - {startedAt} - </TableRowCell> - - <TableRowCell - className={styles.ended} - title={formatDateTime(ended, longDateFormat, timeFormat)} - > - {endedAt} - </TableRowCell> - - <TableRowCell className={styles.duration}> - {formatTimeSpan(duration)} - </TableRowCell> - - <TableRowCell - className={styles.actions} - > - { - status === 'queued' && - <IconButton - title={translate('RemovedFromTaskQueue')} - name={icons.REMOVE} - onPress={this.onCancelPress} - /> - } - </TableRowCell> - - <ConfirmModal - isOpen={isCancelConfirmModalOpen} - kind={kinds.DANGER} - title={translate('Cancel')} - message={translate('CancelPendingTask')} - confirmLabel={translate('YesCancel')} - cancelLabel={translate('NoLeaveIt')} - onConfirm={onCancelPress} - onCancel={this.onAbortCancel} - /> - </TableRow> - ); - } -} - -QueuedTaskRow.propTypes = { - trigger: PropTypes.string.isRequired, - commandName: PropTypes.string.isRequired, - queued: PropTypes.string.isRequired, - started: PropTypes.string, - ended: PropTypes.string, - status: PropTypes.string.isRequired, - duration: PropTypes.string, - message: PropTypes.string, - clientUserAgent: PropTypes.string, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onCancelPress: PropTypes.func.isRequired -}; - -export default QueuedTaskRow; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx new file mode 100644 index 000000000..4511bcbf4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx @@ -0,0 +1,238 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CommandBody } from 'Commands/Command'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import { cancelCommand } from 'Store/Actions/commandActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; +import styles from './QueuedTaskRow.css'; + +function getStatusIconProps(status: string, message: string | undefined) { + const title = titleCase(status); + + switch (status) { + case 'queued': + return { + name: icons.PENDING, + title, + }; + + case 'started': + return { + name: icons.REFRESH, + isSpinning: true, + title, + }; + + case 'completed': + return { + name: icons.CHECK, + kind: kinds.SUCCESS, + title: message === 'Completed' ? title : `${title}: ${message}`, + }; + + case 'failed': + return { + name: icons.FATAL, + kind: kinds.DANGER, + title: `${title}: ${message}`, + }; + + default: + return { + name: icons.UNKNOWN, + title, + }; + } +} + +function getFormattedDates( + queued: string, + started: string | undefined, + ended: string | undefined, + showRelativeDates: boolean, + shortDateFormat: string +) { + if (showRelativeDates) { + return { + queuedAt: moment(queued).fromNow(), + startedAt: started ? moment(started).fromNow() : '-', + endedAt: ended ? moment(ended).fromNow() : '-', + }; + } + + return { + queuedAt: formatDate(queued, shortDateFormat), + startedAt: started ? formatDate(started, shortDateFormat) : '-', + endedAt: ended ? formatDate(ended, shortDateFormat) : '-', + }; +} + +interface QueuedTimes { + queuedAt: string; + startedAt: string; + endedAt: string; +} + +export interface QueuedTaskRowProps { + id: number; + trigger: string; + commandName: string; + queued: string; + started?: string; + ended?: string; + status: string; + duration?: string; + message?: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRow(props: QueuedTaskRowProps) { + const { + id, + trigger, + commandName, + queued, + started, + ended, + status, + duration, + message, + body, + clientUserAgent, + } = props; + + const dispatch = useDispatch(); + const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } = + useSelector(createUISettingsSelector()); + + const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>( + null + ); + const [times, setTimes] = useState<QueuedTimes>( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + + const [ + isCancelConfirmModalOpen, + openCancelConfirmModal, + closeCancelConfirmModal, + ] = useModalOpenState(false); + + const handleCancelPress = useCallback(() => { + dispatch(cancelCommand({ id })); + }, [id, dispatch]); + + useEffect(() => { + updateTimeTimeoutId.current = setTimeout(() => { + setTimes( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + }, 30000); + + return () => { + if (updateTimeTimeoutId.current) { + clearTimeout(updateTimeTimeoutId.current); + } + }; + }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]); + + const { queuedAt, startedAt, endedAt } = times; + + let triggerIcon = icons.QUICK; + + if (trigger === 'manual') { + triggerIcon = icons.INTERACTIVE; + } else if (trigger === 'scheduled') { + triggerIcon = icons.SCHEDULED; + } + + return ( + <TableRow> + <TableRowCell className={styles.trigger}> + <span className={styles.triggerContent}> + <Icon name={triggerIcon} title={titleCase(trigger)} /> + + <Icon {...getStatusIconProps(status, message)} /> + </span> + </TableRowCell> + + <QueuedTaskRowNameCell + commandName={commandName} + body={body} + clientUserAgent={clientUserAgent} + /> + + <TableRowCell + className={styles.queued} + title={formatDateTime(queued, longDateFormat, timeFormat)} + > + {queuedAt} + </TableRowCell> + + <TableRowCell + className={styles.started} + title={formatDateTime(started, longDateFormat, timeFormat)} + > + {startedAt} + </TableRowCell> + + <TableRowCell + className={styles.ended} + title={formatDateTime(ended, longDateFormat, timeFormat)} + > + {endedAt} + </TableRowCell> + + <TableRowCell className={styles.duration}> + {formatTimeSpan(duration)} + </TableRowCell> + + <TableRowCell className={styles.actions}> + {status === 'queued' && ( + <IconButton + title={translate('RemovedFromTaskQueue')} + name={icons.REMOVE} + onPress={openCancelConfirmModal} + /> + )} + </TableRowCell> + + <ConfirmModal + isOpen={isCancelConfirmModalOpen} + kind={kinds.DANGER} + title={translate('Cancel')} + message={translate('CancelPendingTask')} + confirmLabel={translate('YesCancel')} + cancelLabel={translate('NoLeaveIt')} + onConfirm={handleCancelPress} + onCancel={closeCancelConfirmModal} + /> + </TableRow> + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js deleted file mode 100644 index f55ab985a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js +++ /dev/null @@ -1,31 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { cancelCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import QueuedTaskRow from './QueuedTaskRow'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return { - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onCancelPress() { - dispatch(cancelCommand({ - id: props.id - })); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css new file mode 100644 index 000000000..41acb33f8 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css @@ -0,0 +1,8 @@ +.commandName { + display: inline-block; + min-width: 220px; +} + +.userAgent { + color: #b0b0b0; +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts new file mode 100644 index 000000000..fc9081492 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'commandName': string; + 'userAgent': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx new file mode 100644 index 000000000..193c78afc --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CommandBody } from 'Commands/Command'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector'; +import translate from 'Utilities/String/translate'; +import styles from './QueuedTaskRowNameCell.css'; + +export interface QueuedTaskRowNameCellProps { + commandName: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRowNameCell( + props: QueuedTaskRowNameCellProps +) { + const { commandName, body, clientUserAgent } = props; + const seriesIds = [...(body.seriesIds ?? [])]; + + if (body.seriesId) { + seriesIds.push(body.seriesId); + } + + const series = useSelector(createMultiSeriesSelector(seriesIds)); + + return ( + <TableRowCell> + <span className={styles.commandName}> + {commandName} + {series.length ? ( + <span> - {series.map((s) => s.title).join(', ')}</span> + ) : null} + {body.seasonNumber ? ( + <span> + {' '} + {translate('SeasonNumberToken', { + seasonNumber: body.seasonNumber, + })} + </span> + ) : null} + </span> + + {clientUserAgent ? ( + <span + className={styles.userAgent} + title={translate('TaskUserAgentTooltip')} + > + {translate('From')}: {clientUserAgent} + </span> + ) : null} + </TableRowCell> + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js deleted file mode 100644 index dac38f1d4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRowConnector from './QueuedTaskRowConnector'; - -const columns = [ - { - name: 'trigger', - label: '', - isVisible: true - }, - { - name: 'commandName', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'queued', - label: () => translate('Queued'), - isVisible: true - }, - { - name: 'started', - label: () => translate('Started'), - isVisible: true - }, - { - name: 'ended', - label: () => translate('Ended'), - isVisible: true - }, - { - name: 'duration', - label: () => translate('Duration'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function QueuedTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( - <FieldSet legend={translate('Queue')}> - { - isFetching && !isPopulated && - <LoadingIndicator /> - } - - { - isPopulated && - <Table - columns={columns} - > - <TableBody> - { - items.map((item) => { - return ( - <QueuedTaskRowConnector - key={item.id} - {...item} - /> - ); - }) - } - </TableBody> - </Table> - } - </FieldSet> - ); -} - -QueuedTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default QueuedTasks; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx new file mode 100644 index 000000000..e79deed7c --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchCommands } from 'Store/Actions/commandActions'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRow from './QueuedTaskRow'; + +const columns = [ + { + name: 'trigger', + label: '', + isVisible: true, + }, + { + name: 'commandName', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'queued', + label: () => translate('Queued'), + isVisible: true, + }, + { + name: 'started', + label: () => translate('Started'), + isVisible: true, + }, + { + name: 'ended', + label: () => translate('Ended'), + isVisible: true, + }, + { + name: 'duration', + label: () => translate('Duration'), + isVisible: true, + }, + { + name: 'actions', + isVisible: true, + }, +]; + +export default function QueuedTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.commands + ); + + useEffect(() => { + dispatch(fetchCommands()); + }, [dispatch]); + + return ( + <FieldSet legend={translate('Queue')}> + {isFetching && !isPopulated && <LoadingIndicator />} + + {isPopulated && ( + <Table columns={columns}> + <TableBody> + {items.map((item) => { + return <QueuedTaskRow key={item.id} {...item} />; + })} + </TableBody> + </Table> + )} + </FieldSet> + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js deleted file mode 100644 index 5fa4d9ead..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchCommands } from 'Store/Actions/commandActions'; -import QueuedTasks from './QueuedTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.commands, - (commands) => { - return commands; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchCommands: fetchCommands -}; - -class QueuedTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchCommands(); - } - - // - // Render - - render() { - return ( - <QueuedTasks - {...this.props} - /> - ); - } -} - -QueuedTasksConnector.propTypes = { - dispatchFetchCommands: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js index 032dbede8..03a3b6ce4 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.js @@ -2,7 +2,7 @@ import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import QueuedTasksConnector from './Queued/QueuedTasksConnector'; +import QueuedTasks from './Queued/QueuedTasks'; import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { @@ -10,7 +10,7 @@ function Tasks() { <PageContent title={translate('Tasks')}> <PageContentBody> <ScheduledTasksConnector /> - <QueuedTasksConnector /> + <QueuedTasks /> </PageContentBody> </PageContent> ); From 063dba22a803295adee4fdcbe42718af3e85ca78 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:05:15 -0700 Subject: [PATCH 34/98] Fixed: Disabled select option still selectable --- .../src/Components/Form/MonitorEpisodesSelectInput.js | 8 ++++---- .../src/Components/Form/MonitorNewItemsSelectInput.js | 8 ++++---- .../Components/Form/QualityProfileSelectInputConnector.js | 4 ++-- frontend/src/Components/Form/SeriesTypeSelectInput.tsx | 6 +++--- .../Series/Index/Select/Edit/EditSeriesModalContent.tsx | 4 ++-- .../Manage/Edit/ManageDownloadClientsEditModalContent.tsx | 2 +- .../Manage/Edit/ManageImportListsEditModalContent.tsx | 2 +- .../Manage/Edit/ManageIndexersEditModalContent.tsx | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js index 9b80cc587..a4ee4fd85 100644 --- a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js +++ b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import monitorOptions from 'Utilities/Series/monitorOptions'; import translate from 'Utilities/String/translate'; -import SelectInput from './SelectInput'; +import EnhancedSelectInput from './EnhancedSelectInput'; function MonitorEpisodesSelectInput(props) { const { @@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) { get value() { return translate('NoChange'); }, - disabled: true + isDisabled: true }); } @@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) { get value() { return `(${translate('Mixed')})`; }, - disabled: true + isDisabled: true }); } return ( - <SelectInput + <EnhancedSelectInput values={values} {...otherProps} /> diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js index c704e5c1f..be179c3e5 100644 --- a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js +++ b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions'; -import SelectInput from './SelectInput'; +import EnhancedSelectInput from './EnhancedSelectInput'; function MonitorNewItemsSelectInput(props) { const { @@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'noChange', value: 'No Change', - disabled: true + isDisabled: true }); } @@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'mixed', value: '(Mixed)', - disabled: true + isDisabled: true }); } return ( - <SelectInput + <EnhancedSelectInput values={values} {...otherProps} /> diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index cc8ffbdb8..48fc6bc35 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -28,7 +28,7 @@ function createMapStateToProps() { get value() { return translate('NoChange'); }, - disabled: includeNoChangeDisabled + isDisabled: includeNoChangeDisabled }); } @@ -38,7 +38,7 @@ function createMapStateToProps() { get value() { return `(${translate('Mixed')})`; }, - disabled: true + isDisabled: true }); } diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx index 471d6592b..cea7f4fb5 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx @@ -15,7 +15,7 @@ interface ISeriesTypeOption { key: string; value: string; format?: string; - disabled?: boolean; + isDisabled?: boolean; } const seriesTypeOptions: ISeriesTypeOption[] = [ @@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { values.unshift({ key: 'noChange', value: translate('NoChange'), - disabled: includeNoChangeDisabled, + isDisabled: includeNoChangeDisabled, }); } @@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { values.unshift({ key: 'mixed', value: `(${translate('Mixed')})`, - disabled: true, + isDisabled: true, }); } diff --git a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx index 27b54f95b..434318334 100644 --- a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx @@ -36,7 +36,7 @@ const monitoredOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'monitored', @@ -58,7 +58,7 @@ const seasonFolderOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'yes', diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 3a024b559..893e2542d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 8660f2fd3..f95d65314 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -31,7 +31,7 @@ const autoAddOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index a7b7187e3..00555433c 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', From 9f705e4161af3f4dd55b399d56b0b9c5a36e181b Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 13 Mar 2024 21:15:36 -0700 Subject: [PATCH 35/98] Fixed: Release push with only Magnet URL Closes #6622 --- src/Sonarr.Api.V3/Indexers/ReleasePushController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index b36aedc41..1b2f22417 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -41,7 +41,8 @@ namespace Sonarr.Api.V3.Indexers _logger = logger; PostValidator.RuleFor(s => s.Title).NotEmpty(); - PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty().When(s => s.MagnetUrl.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.MagnetUrl).NotEmpty().When(s => s.DownloadUrl.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Protocol).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); } @@ -50,7 +51,7 @@ namespace Sonarr.Api.V3.Indexers [Consumes("application/json")] public ActionResult<List<ReleaseResource>> Create(ReleaseResource release) { - _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); + _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl); ValidateResource(release); From 381ce61aefd9ea3e48fbef2c13ac9c81b4442a07 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 14 Mar 2024 04:44:14 +0000 Subject: [PATCH 36/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Dennis Langthjem <dennis@langthjem.dk> Co-authored-by: DimitriDR <dimitridroeck@gmail.com> Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Ihor Mudryi <mudryy33@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/da/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/da.json | 19 +++++- src/NzbDrone.Core/Localization/Core/es.json | 3 +- src/NzbDrone.Core/Localization/Core/fr.json | 3 +- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/uk.json | 58 ++++++++++++++++++- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 66f4c6531..740402a20 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -17,5 +17,22 @@ "AddANewPath": "Tilføj en ny sti", "AddConditionImplementation": "Tilføj betingelse - {implementationName}", "AddConnectionImplementation": "Tilføj forbindelse - {implementationName}", - "AddCustomFilter": "Tilføj tilpasset filter" + "AddCustomFilter": "Tilføj tilpasset filter", + "ApplyChanges": "Anvend ændringer", + "Test": "Afprøv", + "AddImportList": "Tilføj importliste", + "AddExclusion": "Tilføj undtagelse", + "TestAll": "Afprøv alle", + "TestAllClients": "Afprøv alle klienter", + "TestAllLists": "Afprøv alle lister", + "Unknown": "Ukendt", + "AllTitles": "All titler", + "TablePageSize": "Sidestørrelse", + "TestAllIndexers": "Afprøv alle indeks", + "AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}", + "AddIndexerError": "Kunne ikke tilføje en ny indekser. Prøv igen.", + "AddImportListImplementation": "Tilføj importliste - {implementationName}", + "AddRootFolderError": "Kunne ikke tilføje rodmappe", + "Table": "Tabel", + "AddIndexer": "Tilføj indekser" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index c6ab4d35a..b53c02002 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2056,5 +2056,6 @@ "NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}", "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')" + "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." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 55a004379..1f66dc409 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -2056,5 +2056,6 @@ "MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série", "NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise", "DatabaseMigration": "Migration des bases de données", - "Filters": "Filtres" + "Filters": "Filtres", + "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index d21604d83..65b22e2ad 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -2056,5 +2056,6 @@ "DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído", "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" + "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." } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 0f30d9911..e6b2d5d39 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -38,5 +38,61 @@ "AddList": "Додати список", "AddListError": "Неможливо додати новий список, спробуйте ще раз.", "AddListExclusionError": "Неможливо додати новий виняток зі списку, спробуйте ще раз.", - "AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків" + "AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків", + "AllSeriesAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром", + "AlternateTitles": "Альтернативна назва", + "Analytics": "Аналітика", + "Apply": "Застосувати", + "ApplyTags": "Застосувати теги", + "ApplyTagsHelpTextAdd": "Додати: додати теги до наявного списку тегів", + "ApplyTagsHelpTextHowToApplyImportLists": "Як застосувати теги до вибраних списків імпорту", + "ApplyTagsHelpTextRemove": "Видалити: видалити введені теги", + "ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних індексаторів", + "ApplyTagsHelpTextReplace": "Замінити: Змінити наявні теги на введені теги (залишіть порожнім, щоб очистити всі теги)", + "AuthenticationMethodHelpTextWarning": "Виберіть дійсний метод автентифікації", + "AirsDateAtTimeOn": "{date} о {time} на {networkLabel}", + "AirDate": "Дата трансляції", + "AddRemotePathMapping": "Додати віддалений шлях", + "AddRemotePathMappingError": "Не вдалося додати нове зіставлення віддаленого шляху, спробуйте ще раз.", + "AnalyticsEnabledHelpText": "Надсилайте анонімну інформацію про використання та помилки на сервери {appName}. Це включає інформацію про ваш веб-переглядач, які сторінки {appName} WebUI ви використовуєте, звіти про помилки, а також версію ОС і часу виконання. Ми будемо використовувати цю інформацію, щоб визначити пріоритети функцій і виправлення помилок.", + "ApiKeyValidationHealthCheckMessage": "Будь ласка оновіть ключ API, щоб він містив принаймні {length} символів. Ви можете зробити це в налаштуваннях або в файлі конфігурації", + "AppDataLocationHealthCheckMessage": "Оновлення буде неможливим, щоб запобігти видаленню AppData під час оновлення", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Як застосувати теги до вибраних клієнтів завантаження", + "AllResultsAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром", + "AudioInfo": "Аудіо інформація", + "Age": "Вік", + "All": "Всі", + "Anime": "Аніме", + "AgeWhenGrabbed": "Вік (коли схоплено)", + "AnimeEpisodeFormat": "Формат серії аніме", + "ApiKey": "API Ключ", + "ApplicationURL": "URL програми", + "AppDataDirectory": "Каталог AppData", + "AptUpdater": "Використовуйте apt для інсталяції оновлення", + "AddRootFolder": "Додати корневий каталог", + "AllTitles": "Усі Назви", + "Always": "Завжди", + "AddNewSeriesError": "Не вдалося завантажити результати пошуку, спробуйте ще.", + "AlreadyInYourLibrary": "Вже у вашій бібліотеці", + "AddDelayProfileError": "Неможливо додати новий профіль затримки, будь ласка спробуйте ще.", + "AddNewSeriesHelpText": "Додати новий серіал легко, просто почніть вводити назву серіалу, який ви хочете додати.", + "AddNewSeriesRootFolderHelpText": "Підпапка '{folder}' буде створена автоматично", + "AddNewSeriesSearchForMissingEpisodes": "Почніть пошук відсутніх епізодів", + "AddNotificationError": "Не вдалося додати нове сповіщення, спробуйте ще раз.", + "AddQualityProfile": "Додати профіль якості", + "AddQualityProfileError": "Не вдалося додати новий профіль якості, спробуйте ще раз.", + "AddReleaseProfile": "Додати профіль релізу", + "AirsTimeOn": "{time} на {networkLabel}", + "AllFiles": "Всі файли", + "AirsTomorrowOn": "Завтра о {time} на {networkLabel}", + "AnalyseVideoFiles": "Аналізувати відео файли", + "AnalyseVideoFilesHelpText": "Отримайте з файлів інформацію про відео, таку як роздільна здатність, час виконання та кодек. Це вимагає, щоб {appName} читав частини файлу, що може спричинити високу дискову або мережеву активність під час сканування.", + "Any": "Будь-який", + "AppUpdated": "{appName} Оновлено", + "ApplicationUrlHelpText": "Зовнішня URL-адреса цієї програми, включаючи http(s)://, порт і базу URL-адрес", + "ApplyChanges": "Застосувати зміни", + "AudioLanguages": "Мови аудіо", + "AuthForm": "Форми (сторінка входу)", + "Authentication": "Автентифікація", + "AuthenticationMethod": "Метод автентифікації" } From e14568adef8c73eae3a93ac0c28d2ed8baff72b9 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:33:43 +0200 Subject: [PATCH 37/98] Ensure not allowed cursor is shown for disabled select inputs --- frontend/src/Components/Form/EnhancedSelectInput.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index 56f5564b9..defefb18e 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -19,7 +19,7 @@ .isDisabled { opacity: 0.7; - cursor: not-allowed; + cursor: not-allowed !important; } .dropdownArrowContainer { From 172b1a82d1d061b1b44e2a561391d6742a3d3820 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:35:22 +0200 Subject: [PATCH 38/98] Sort series by title in task name --- frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx index 193c78afc..a3e327e01 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -23,13 +23,16 @@ export default function QueuedTaskRowNameCell( } const series = useSelector(createMultiSeriesSelector(seriesIds)); + const sortedSeries = series.sort((a, b) => + a.sortTitle.localeCompare(b.sortTitle) + ); return ( <TableRowCell> <span className={styles.commandName}> {commandName} - {series.length ? ( - <span> - {series.map((s) => s.title).join(', ')}</span> + {sortedSeries.length ? ( + <span> - {sortedSeries.map((s) => s.title).join(', ')}</span> ) : null} {body.seasonNumber ? ( <span> From 58de0310fd1376d74eb100a53ecea75b38b7f47c Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sun, 17 Mar 2024 17:58:50 +0000 Subject: [PATCH 39/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: infoaitek24 <info@aitekph.com> Co-authored-by: reloxx <reloxx@interia.pl> Co-authored-by: vfaergestad <vgf@hotmail.no> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/de.json | 31 +++++++++++++------ src/NzbDrone.Core/Localization/Core/it.json | 5 +-- .../Localization/Core/nb_NO.json | 9 +++++- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 0ee4eb808..901b52856 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -41,7 +41,7 @@ "SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen", "AbsoluteEpisodeNumber": "Exakte Folgennummer", "AddConnection": "Verbindung hinzufügen", - "AddAutoTagError": "Der neue automatische Tag konnte nicht hinzugefügt werden, bitte versuche es erneut.", + "AddAutoTagError": "Auto-Tag konnte nicht hinzugefügt werden. Bitte erneut versuchen.", "AddConditionError": "Neue Bedingung konnte nicht hinzugefügt werden, bitte erneut versuchen.", "AddCustomFormat": "Eigenes Format hinzufügen", "AddCustomFormatError": "Neues eigenes Format kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.", @@ -146,7 +146,7 @@ "AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.", "AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien extrahieren. Dies erfordert, dass {appName} Teile der Datei liest, was bei Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.", "AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.", - "AutoTaggingNegateHelpText": "Wenn diese Option aktiviert ist, wird die automatische Tagging-Regel nicht angewendet, wenn diese {implementationName}-Bedingung zutrifft.", + "AutoTaggingNegateHelpText": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.", "CopyUsingHardlinksSeriesHelpText": "Mithilfe von Hardlinks kann {appName} Seeding-Torrents in den Serienordner importieren, ohne zusätzlichen Speicherplatz zu beanspruchen oder den gesamten Inhalt der Datei zu kopieren. Hardlinks funktionieren nur, wenn sich Quelle und Ziel auf demselben Volume befinden", "DailyEpisodeTypeFormat": "Datum ({format})", "DefaultDelayProfileSeries": "Dies ist das Standardprofil. Es gilt für alle Serien, die kein explizites Profil haben.", @@ -171,7 +171,7 @@ "BackupIntervalHelpText": "Intervall zwischen automatischen Sicherungen", "BuiltIn": "Eingebaut", "ChangeFileDate": "Ändern Sie das Dateidatum", - "CustomFormatsLoadError": "Benutzerdefinierte Formate können nicht geladen werden", + "CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden", "DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?", "DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren", "DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen", @@ -205,7 +205,7 @@ "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", - "AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss zutreffen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht eine einzelne {implementationName}-Übereinstimmung aus.", + "AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.", "BackupRetentionHelpText": "Automatische Backups, die älter als der Aufbewahrungszeitraum sind, werden automatisch bereinigt", "BindAddressHelpText": "Gültige IP-Adresse, localhost oder „*“ für alle Schnittstellen", "BackupsLoadError": "Sicherrungen können nicht geladen werden", @@ -280,8 +280,8 @@ "Custom": "Benutzerdefiniert", "CustomFilters": "Benutzerdefinierte Filter", "CustomFormat": "Benutzerdefiniertes Format", - "CustomFormats": "Benutzerdefinierte Formate", - "CustomFormatsSettingsSummary": "Benutzerdefinierte Formate und Einstellungen", + "CustomFormats": "Eigene Formate", + "CustomFormatsSettingsSummary": "Eigene Formate und Einstellungen", "DailyEpisodeFormat": "Tägliches Episodenformat", "Database": "Datenbank", "Dates": "Termine", @@ -540,7 +540,7 @@ "ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu", "ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen", "ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).", - "Wanted": "› Gesucht", + "Wanted": "Gesucht", "ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.", "Continuing": "Fortsetzung", "CopyUsingHardlinksHelpTextWarning": "Gelegentlich können Dateisperren das Umbenennen von Dateien verhindern, die geseedet werden. Sie können das Seeding vorübergehend deaktivieren und als Workaround die Umbenennungsfunktion von {appName} verwenden.", @@ -550,7 +550,7 @@ "CountIndexersSelected": "{count} Indexer ausgewählt", "CountSelectedFiles": "{selectedCount} ausgewählte Dateien", "CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“", - "CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen", + "CustomFormatsSettings": "Einstellungen für eigene Formate", "Daily": "Täglich", "Dash": "Bindestrich", "Debug": "Debuggen", @@ -709,7 +709,7 @@ "ClickToChangeSeason": "Klicken Sie hier, um die Staffel zu ändern", "BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert", "BlackholeWatchFolder": "Überwachter Ordner", - "BlackholeWatchFolderHelpText": "Ordner, aus dem {appName} abgeschlossene Downloads importieren soll", + "BlackholeWatchFolderHelpText": "Der Ordner, aus dem {appName} fertige Downloads importieren soll", "BrowserReloadRequired": "Neuladen des Browsers erforderlich", "CalendarOptions": "Kalenderoptionen", "CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?", @@ -776,5 +776,16 @@ "Airs": "Wird ausgestrahlt", "AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden", "IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht", - "DownloadClientSettingsAddPaused": "Pausiert hinzufügen" + "DownloadClientSettingsAddPaused": "Pausiert hinzufügen", + "ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern", + "BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten", + "BlocklistAndSearch": "Sperrliste und Suche", + "AddDelayProfileError": "Verzögerungsprofil konnte nicht hinzugefügt werden. Bitte erneut versuchen.", + "BlocklistAndSearchHint": "Starte Suche nach einer Alternative, falls es der Sperrliste hinzugefügt wurde", + "BlocklistAndSearchMultipleHint": "Starte Suchen nach einer Alternative, falls es der Sperrliste hinzugefügt wurde", + "BlocklistMultipleOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternativen zu suchen", + "BlocklistOnly": "Nur der Sperrliste hinzufügen", + "BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen", + "BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren", + "ChangeCategory": "Kategorie wechseln" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 3cb19a328..8b6adc4bf 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -110,7 +110,7 @@ "AddAutoTagError": "Impossibile aggiungere un nuovo tag automatico, riprova.", "AddCustomFormat": "Aggiungi Formato Personalizzato", "AddDownloadClient": "Aggiungi Client di Download", - "AddCustomFormatError": "Non riesco ad aggiungere un nuovo formato personalizzato, riprova.", + "AddCustomFormatError": "Impossibile aggiungere un nuovo formato personalizzato, riprova.", "AddDownloadClientError": "Impossibile aggiungere un nuovo client di download, riprova.", "AddDelayProfile": "Aggiungi Profilo di Ritardo", "AddIndexerError": "Impossibile aggiungere un nuovo Indicizzatore, riprova.", @@ -248,5 +248,6 @@ "AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto", "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", "AutoRedownloadFailed": "Download fallito", - "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova." + "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", + "Cutoff": "Taglio" } diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index b763b0c4c..ed9052f14 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -8,5 +8,12 @@ "Absolute": "Absolutt", "Activity": "Aktivitet", "About": "Om", - "CalendarOptions": "Kalenderinnstillinger" + "CalendarOptions": "Kalenderinnstillinger", + "AbsoluteEpisodeNumbers": "Absolutte Episode Numre", + "AddANewPath": "Legg til ny filsti", + "AddConditionImplementation": "Legg til betingelse - {implementationName}", + "AddConditionError": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "AbsoluteEpisodeNumber": "Absolutt Episode Nummer", + "AddAutoTagError": "Ikke mulig å legge til ny automatisk tagg, vennligst prøv igjen", + "Actions": "Handlinger" } From c6417337812f3578a27f9dc1e44fdad80f557271 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 16:48:35 -0700 Subject: [PATCH 40/98] Fixed: Task progress messages in the UI Closes #6632 --- .../IndexerSearch/ReleaseSearchService.cs | 2 +- src/NzbDrone.Core/Indexers/RssSyncCommand.cs | 1 - src/NzbDrone.Core/Messaging/Commands/Command.cs | 2 +- .../ProgressMessaging/ProgressMessageContext.cs | 16 +++++++++++++--- .../Tv/Commands/RefreshSeriesCommand.cs | 2 ++ .../Update/Commands/ApplicationUpdateCommand.cs | 2 -- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 97420fb65..51b0a75cf 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -522,7 +522,7 @@ namespace NzbDrone.Core.IndexerSearch var reports = batch.SelectMany(x => x).ToList(); - _logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count); + _logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count); // Update the last search time for all episodes if at least 1 indexer was searched. if (indexers.Any()) diff --git a/src/NzbDrone.Core/Indexers/RssSyncCommand.cs b/src/NzbDrone.Core/Indexers/RssSyncCommand.cs index 4722b32f2..ea9c6fd8a 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncCommand.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncCommand.cs @@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers public class RssSyncCommand : Command { public override bool SendUpdatesToClient => true; - public override bool IsLongRunning => true; } } diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 023b52d85..5aa4cf514 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Messaging.Commands } public virtual bool UpdateScheduledTask => true; - public virtual string CompletionMessage => "Completed"; + public virtual string CompletionMessage => null; public virtual bool RequiresDiskAccess => false; public virtual bool IsExclusive => false; public virtual bool IsLongRunning => false; diff --git a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs index fba9ca3f3..09fecee2c 100644 --- a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs +++ b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs @@ -1,10 +1,13 @@ -using System; +using System; +using System.Threading; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.ProgressMessaging { public static class ProgressMessageContext { + private static AsyncLocal<CommandModel> _commandModelAsync = new AsyncLocal<CommandModel>(); + [ThreadStatic] private static CommandModel _commandModel; @@ -13,8 +16,15 @@ namespace NzbDrone.Core.ProgressMessaging public static CommandModel CommandModel { - get { return _commandModel; } - set { _commandModel = value; } + get + { + return _commandModel ?? _commandModelAsync.Value; + } + set + { + _commandModel = value; + _commandModelAsync.Value = value; + } } public static bool LockReentrancy() diff --git a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs b/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs index da6e853dc..1c8a6f269 100644 --- a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs +++ b/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs @@ -39,5 +39,7 @@ namespace NzbDrone.Core.Tv.Commands public override bool UpdateScheduledTask => SeriesIds.Empty(); public override bool IsLongRunning => true; + + public override string CompletionMessage => "Completed"; } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 0ca1d8074..59a827a0b 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -6,7 +6,5 @@ namespace NzbDrone.Core.Update.Commands { public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; - - public override string CompletionMessage => null; } } From 29204c93a37881d9066d8a18a338edbbe4c0d807 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 17:15:23 -0700 Subject: [PATCH 41/98] New: Parsing multi-episode file with two and three digit episode numbers Closes #6631 --- .../ParserTests/MultiEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index e3c098f4f..22c607198 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title (S15E06-08) City Sushi", "Series Title", 15, new[] { 6, 7, 8 })] [TestCase("Босх: Спадок (S2E1-4) / Series: Legacy (S2E1-4) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })] [TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })] + [TestCase("Series Title - S26E96-97-98-99-100 - Episode 5931 + Episode 5932 + Episode 5933 + Episode 5934 + Episode 5935", "Series Title", 26, new[] { 96, 97, 98, 99, 100 })] // [TestCase("", "", , new [] { })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 2989353ad..3c15ca812 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -186,6 +186,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^((?<title>.*?)[ ._]\/[ ._])+\(?S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?([ ._]of[ ._]\d+)?\)?[ ._][\(\[]", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-episode with title (S01E99-100, S01E05-06) + new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{2,3}(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+)))+(?:[-_. ]|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-episode with title (S01E05-06, S01E05-6) new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{1,2}(?!\d+))(?:-(?<episode>\d{1,2}(?!\d+)))+(?:[-_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 88de9274358d7005fa9c677bb8c86f046a2a23a9 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 18 Mar 2024 17:24:28 -0700 Subject: [PATCH 42/98] Fixed: Plex Watchlist import list --- src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs index ecc2c4392..85e24ce99 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv var clientIdentifier = _configService.PlexClientIdentifier; - var requestBuilder = new HttpRequestBuilder("https://metadata.provider.plex.tv/library/sections/watchlist/all") + var requestBuilder = new HttpRequestBuilder("https://discover.provider.plex.tv/library/sections/watchlist/all") .Accept(HttpAccept.Json) .AddQueryParam("clientID", clientIdentifier) .AddQueryParam("context[device][product]", BuildInfo.AppName) @@ -107,7 +107,8 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv .AddQueryParam("context[device][platformVersion]", "7") .AddQueryParam("context[device][version]", BuildInfo.Version.ToString()) .AddQueryParam("includeFields", "title,type,year,ratingKey") - .AddQueryParam("includeElements", "Guid") + .AddQueryParam("excludeElements", "Image") + .AddQueryParam("includeGuids", "1") .AddQueryParam("sort", "watchlistedAt:desc") .AddQueryParam("type", (int)PlexMediaType.Show) .AddQueryParam("X-Plex-Container-Size", pageSize) From 40bac236985c682f1da06d56221ecb2ba807931e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Wed, 20 Mar 2024 16:48:01 -0700 Subject: [PATCH 43/98] New: Support parsing season number from season folder when importing Closes #903 --- .../ParserTests/PathParserFixture.cs | 3 +++ .../Organizer/FileNameBuilder.cs | 4 ++-- .../Organizer/FileNameValidation.cs | 3 +++ .../Organizer/FileNameValidationService.cs | 15 ++++++++---- src/NzbDrone.Core/Parser/Parser.cs | 24 +++++++++++++++++++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 03bc64fa5..8ef926316 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase(@"C:\Test\Series\Season 01\1 Pilot (1080p HD).mkv", 1, 1)] [TestCase(@"C:\Test\Series\Season 1\02 Honor Thy Father (1080p HD).m4v", 1, 2)] [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:\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) @@ -45,6 +47,7 @@ namespace NzbDrone.Core.Test.ParserTests } [TestCase("01-03\\The Series Title (2010) - 1x01-02-03 - Episode Title HDTV-720p Proper", "The Series Title (2010)", 1, new[] { 1, 2, 3 })] + [TestCase("Season 2\\E05-06 - Episode Title HDTV-720p Proper", "", 2, new[] { 5, 6 })] public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes) { var result = Parser.Parser.ParsePath(path.AsOsAgnostic()); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7e03172ef..ab532f0a5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -50,10 +50,10 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[ ,a-z0-9+-]+(?<![- ])))?(?<suffix>[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", + public static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", + public static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})", diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index bcda9c884..e8d39469f 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } } @@ -91,6 +92,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AirDateRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } @@ -109,6 +111,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..8a50137fd 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Linq; using FluentValidation.Results; using NzbDrone.Core.Parser.Model; @@ -20,7 +21,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -38,7 +41,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -66,7 +71,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3c15ca812..9104af5ed 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -554,6 +554,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex ArticleWordRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); 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 RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); @@ -563,6 +565,28 @@ namespace NzbDrone.Core.Parser { var fileInfo = new FileInfo(path); + // Parse using the folder and file separately, but combine if they both parse correctly. + var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name); + + if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null) + { + var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name); + + if (parsedFileInfo != null) + { + var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name); + + if (seasonMatch.Success && seasonMatch.Groups["season"].Success) + { + parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value); + + 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)) From dec3fc68897a17b8b7b1e51b043bc0c6c03eec33 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 21 Mar 2024 21:21:04 -0700 Subject: [PATCH 44/98] Fixed: Don't add series from import list with no matched TVDB ID --- .../ImportListSyncServiceFixture.cs | 32 +++++++++++++++++++ .../ImportLists/ImportListSyncService.cs | 8 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index d9702c24b..26e8cab7e 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -120,6 +120,17 @@ namespace NzbDrone.Core.Test.ImportListTests private void WithImdbId() { _list1Series.First().ImdbId = "tt0496424"; + + Mocker.GetMock<ISearchForNewSeries>() + .Setup(s => s.SearchForNewSeriesByImdbId(_list1Series.First().ImdbId)) + .Returns( + Builder<Series> + .CreateListOfSize(1) + .All() + .With(s => s.Title = "Breaking Bad") + .With(s => s.TvdbId = 81189) + .Build() + .ToList()); } private void WithExistingSeries() @@ -342,6 +353,7 @@ namespace NzbDrone.Core.Test.ImportListTests public void should_add_new_series_from_single_list_to_library() { _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithTvdbId(); WithList(1, true); WithCleanLevel(ListSyncLevelType.Disabled); @@ -358,6 +370,7 @@ namespace NzbDrone.Core.Test.ImportListTests _importListFetch.Series.ForEach(m => m.ImportListId = 1); _importListFetch.Series.AddRange(_list2Series); + WithTvdbId(); WithList(1, true); WithList(2, true); @@ -376,6 +389,7 @@ namespace NzbDrone.Core.Test.ImportListTests _importListFetch.Series.ForEach(m => m.ImportListId = 1); _importListFetch.Series.AddRange(_list2Series); + WithTvdbId(); WithList(1, true); WithList(2, false); @@ -422,12 +436,17 @@ namespace NzbDrone.Core.Test.ImportListTests public void should_search_by_imdb_if_series_title_and_series_imdb() { _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithImdbId(); + Subject.Execute(_commandAll); Mocker.GetMock<ISearchForNewSeries>() .Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once()); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1), It.IsAny<bool>())); } [Test] @@ -498,5 +517,18 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock<IImportListExclusionService>() .Verify(v => v.All(), Times.Never); } + + [Test] + public void should_not_add_if_tvdbid_is_0() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithExcludedSeries(); + + Subject.Execute(_commandAll); + + Mocker.GetMock<IAddSeriesService>() + .Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>())); + } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 2c51ea6c0..eccbed341 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -190,6 +190,12 @@ namespace NzbDrone.Core.ImportLists item.Title = mappedSeries.Title; } + if (item.TvdbId == 0) + { + _logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title); + continue; + } + // Check to see if series excluded var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault(); @@ -202,7 +208,7 @@ namespace NzbDrone.Core.ImportLists // Break if Series Exists in DB if (existingTvdbIds.Any(x => x == item.TvdbId)) { - _logger.Debug("{0} [{1}] Rejected, Series Exists in DB", item.TvdbId, item.Title); + _logger.Debug("{0} [{1}] Rejected, series exists in database", item.TvdbId, item.Title); continue; } From cf3d51bab27e99ec719f882ff0b58291b18f5f10 Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 22 Mar 2024 04:19:30 +0000 Subject: [PATCH 45/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Casselluu <jack10193@163.com> Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it> Co-authored-by: Jason54 <jason54700.jg@gmail.com> Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: infoaitek24 <info@aitekph.com> Co-authored-by: reloxx <reloxx@interia.pl> Co-authored-by: shimmyx <shimmygodx@gmail.com> Co-authored-by: vfaergestad <vgf@hotmail.no> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nb_NO/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/ca.json | 10 +++++++++- src/NzbDrone.Core/Localization/Core/fr.json | 14 +++++++------- src/NzbDrone.Core/Localization/Core/zh_CN.json | 5 +++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 08c1aba65..32c883eb9 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -665,5 +665,13 @@ "NotificationsPushoverSettingsRetry": "Torna-ho a provar", "NotificationsSettingsWebhookMethod": "Mètode", "Other": "Altres", - "Monitor": "Monitora" + "Monitor": "Monitora", + "AutoTaggingSpecificationOriginalLanguage": "Llenguatge", + "AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat", + "AutoTaggingSpecificationRootFolder": "Carpeta arrel", + "AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar", + "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" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 1f66dc409..b6b0239e5 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -245,7 +245,7 @@ "SubtitleLanguages": "Langues des sous-titres", "Clone": "Dupliquer", "ColonReplacementFormatHelpText": "Changer la manière dont {appName} remplace les « deux-points »", - "DefaultCase": "Casse par défaut", + "DefaultCase": "Case par défaut", "Delete": "Supprimer", "DelayProfiles": "Profils de retard", "DelayProfilesLoadError": "Impossible de charger les profils de retard", @@ -1281,8 +1281,8 @@ "CreateEmptySeriesFolders": "Créer des dossiers de séries vides", "Custom": "Customisé", "CopyUsingHardlinksSeriesHelpText": "Les liens physiques permettent à {appName} d'importer des torrents dans le dossier de la série sans prendre d'espace disque supplémentaire ni copier l'intégralité du contenu du fichier. Les liens physiques ne fonctionneront que si la source et la destination sont sur le même volume", - "CustomFormatsSettingsSummary": "Paramètres de formats personnalisés", - "CustomFormatsSettings": "Paramètres de formats personnalisés", + "CustomFormatsSettingsSummary": "Formats et paramètres personnalisés", + "CustomFormatsSettings": "Paramètre des formats personnalisés", "DefaultDelayProfileSeries": "Il s'agit du profil par défaut. Cela s'applique à toutes les séries qui n'ont pas de profil explicite.", "DeleteDownloadClient": "Supprimer le client de téléchargement", "DeleteEmptyFolders": "Supprimer les dossiers vides", @@ -1355,7 +1355,7 @@ "DownloadClientStatusAllClientHealthCheckMessage": "Tous les clients de téléchargement sont indisponibles en raison d'échecs", "DownloadClientsLoadError": "Impossible de charger les clients de téléchargement", "DownloadPropersAndRepacks": "Propriétés et reconditionnements", - "DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins distants", + "DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins d'accès à distance", "DownloadPropersAndRepacksHelpText": "S'il faut ou non mettre à niveau automatiquement vers Propers/Repacks", "DownloadPropersAndRepacksHelpTextWarning": "Utilisez des formats personnalisés pour les mises à niveau automatiques vers Propers/Repacks", "DownloadPropersAndRepacksHelpTextCustomFormat": "Utilisez « Ne pas préférer » pour trier par score de format personnalisé sur Propers/Repacks", @@ -1415,7 +1415,7 @@ "EpisodesLoadError": "Impossible de charger les épisodes", "Files": "Fichiers", "Continuing": "Continuer", - "Donate": "Faire un don", + "Donate": "Donation", "EditConditionImplementation": "Modifier la condition – {implementationName}", "EditConnectionImplementation": "Modifier la connexion - {implementationName}", "EditImportListImplementation": "Modifier la liste d'importation - {implementationName}", @@ -1931,8 +1931,8 @@ "DownloadClientDelugeSettingsDirectoryHelpText": "Emplacement dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement Deluge par défaut", "DownloadClientDelugeSettingsDirectory": "Dossier de téléchargement", "DownloadClientDelugeSettingsDirectoryCompleted": "Dossier de déplacement une fois terminé", - "ClickToChangeIndexerFlags": "Cliquer pour changer les attributs de l'indexer", - "CustomFormatsSpecificationFlag": "Attribut", + "ClickToChangeIndexerFlags": "Cliquez pour changer les drapeaux de l'indexeur", + "CustomFormatsSpecificationFlag": "Drapeau", "CustomFilter": "Filtre personnalisé", "ImportListsTraktSettingsAuthenticateWithTrakt": "S'authentifier avec Trakt", "SelectIndexerFlags": "Sélectionner les drapeaux de l'indexeur", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 925e731ca..78129b628 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1786,7 +1786,7 @@ "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", - "ChangeCategory": "改变分类", + "ChangeCategory": "修改分类", "IgnoreDownload": "忽略下载", "IgnoreDownloads": "忽略下载", "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", @@ -1808,5 +1808,6 @@ "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", - "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。" + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", + "AutoTaggingSpecificationOriginalLanguage": "语言" } From c403b2cdd5fa89c95a32c06dd6c4d0d60c5040be Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Wed, 27 Mar 2024 16:57:16 +0000 Subject: [PATCH 46/98] 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 --- src/NzbDrone.Core/Localization/Core/ca.json | 44 +++++++++++++++++++-- src/NzbDrone.Core/Localization/Core/es.json | 16 ++++---- src/NzbDrone.Core/Localization/Core/sk.json | 42 +++++++++++++++++++- src/NzbDrone.Core/Localization/Core/tr.json | 2 +- 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 32c883eb9..4deede861 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -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" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index b53c02002..8b9fbedcf 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -537,8 +537,8 @@ "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", @@ -570,15 +570,15 @@ "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')", + "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, por defecto es '{port}'", "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{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.", @@ -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", diff --git a/src/NzbDrone.Core/Localization/Core/sk.json b/src/NzbDrone.Core/Localization/Core/sk.json index 0967ef424..3bb1bc1e3 100644 --- a/src/NzbDrone.Core/Localization/Core/sk.json +++ b/src/NzbDrone.Core/Localization/Core/sk.json @@ -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" +} diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 32a93393a..3a45b393b 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -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", From fc6494c569324c839debdb1d08dde23b8f1b8d76 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Mar 2024 21:42:54 -0700 Subject: [PATCH 47/98] Fixed: Task with removed series causing error --- .../src/Store/Selectors/createMultiSeriesSelector.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts index 119ccd1ee..fa8235c45 100644 --- a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts +++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts @@ -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; + }, []); } ); } From d338425951af50a710c6c4411a72f05d14737ddd Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 23 Mar 2024 21:44:23 -0700 Subject: [PATCH 48/98] Fixed: Use custom formats from import during rename --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 1f2f3cb9c..dde75161a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -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); From 1335efd487a02ab66f075064781c35634494f2f1 Mon Sep 17 00:00:00 2001 From: iceypotato <nickyjedi@gmail.com> Date: Mon, 17 Jul 2023 10:54:09 -0700 Subject: [PATCH 49/98] New: My Anime List import list Closes #5148 --- .../ImportLists/ImportListSyncService.cs | 17 +++ .../MyAnimeList/MyAnimeListImport.cs | 121 ++++++++++++++++++ .../MyAnimeList/MyAnimeListParser.cs | 31 +++++ .../MyAnimeListRequestGenerator.cs | 50 ++++++++ .../MyAnimeList/MyAnimeListResponses.cs | 55 ++++++++ .../MyAnimeList/MyAnimeListSettings.cs | 58 +++++++++ .../MyAnimeList/MyAnimeListStatus.cs | 25 ++++ src/NzbDrone.Core/Localization/Core/en.json | 3 + .../MetadataSource/ISearchForNewSeries.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 7 + 10 files changed, 368 insertions(+) create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index eccbed341..291ecba27 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -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); diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs new file mode 100644 index 000000000..1ba6898c9 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs @@ -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."); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs new file mode 100644 index 000000000..23a74b1f4 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs @@ -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; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs new file mode 100644 index 000000000..7bf62254a --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs @@ -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; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs new file mode 100644 index 000000000..9c55eecd6 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs @@ -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; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs new file mode 100644 index 000000000..aad6257c8 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs @@ -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)); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs new file mode 100644 index 000000000..b08c9e41f --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs @@ -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 + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ce503dd49..b5d0cb157 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -838,6 +838,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", diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs index f8aef8654..c5d89bbaf 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs @@ -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); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 76efea07d..9313b0661 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -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}"); From 13c925b3418d1d48ec041e3d97ab51aaf2b8977a Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:24:49 +0200 Subject: [PATCH 50/98] New: Advanced settings toggle in import list, notification and download client modals --- .../EditDownloadClientModalContent.js | 9 +++++++++ .../EditDownloadClientModalContentConnector.js | 17 +++++++++++++++-- .../ImportLists/EditImportListModalContent.js | 9 +++++++++ .../EditImportListModalContentConnector.js | 17 +++++++++++++++-- .../EditNotificationModalContent.js | 9 +++++++++ .../EditNotificationModalContentConnector.js | 17 +++++++++++++++-- 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index f2509603f..34213928d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -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 }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js index f2d4ad6ff..3c9289763 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -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 }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index 2c1ab4bb0..e5a0dcd10 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -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 }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js index 3a7ad8b1f..ebcbd4ae1 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js @@ -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 }; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js index 83f5d257d..f52655289 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -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 }; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js index 3d6e9378e..658d72da8 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -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 }; From 588372fd950fc85f5e9a4275fbcb423b247ed0ee Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento <carlos5678@gmail.com> Date: Thu, 28 Mar 2024 06:28:41 +0100 Subject: [PATCH 51/98] Fixed: qBittorrent not correctly handling retention during testing --- .../Download/Clients/QBittorrent/QBittorrent.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index e8917a0c9..b072b193d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -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")) { From 35d0e6a6f806c68756450a7d199600d7fb49d6c5 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Thu, 28 Mar 2024 06:29:15 +0100 Subject: [PATCH 52/98] Fixed: Handling torrents with relative path in rTorrent --- src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 5705e33a3..fd91a3833 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -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(); From 1ec1ce58e9f095222e7fe4a8c74a0720fed71558 Mon Sep 17 00:00:00 2001 From: Alex Cortelyou <1689668+acortelyou@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:30:21 -0700 Subject: [PATCH 53/98] New: Add additional fields to Webhook Manual Interaction Required events --- .../Notifications/Webhook/WebhookBase.cs | 2 ++ .../Webhook/WebhookDownloadStatusMessage.cs | 18 ++++++++++++++++++ .../Webhook/WebhookManualInteractionPayload.cs | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index a5a1dec6d..d114a88db 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -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) }; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs new file mode 100644 index 000000000..5e8b47870 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs @@ -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(); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs index fca226f4b..b217f9284 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs @@ -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; } } From 7353fe479dbb8d0dab76993ebed92d48e1b05524 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Thu, 28 Mar 2024 07:30:45 +0200 Subject: [PATCH 54/98] New: Allow HEAD requests to ping endpoint Closes #6656 --- src/Sonarr.Http/Ping/PingController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sonarr.Http/Ping/PingController.cs b/src/Sonarr.Http/Ping/PingController.cs index c1b9c02fc..091e391cf 100644 --- a/src/Sonarr.Http/Ping/PingController.cs +++ b/src/Sonarr.Http/Ping/PingController.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Http [AllowAnonymous] [HttpGet("/ping")] + [HttpHead("/ping")] [Produces("application/json")] public ActionResult<PingResource> GetStatus() { From 060b789bc6f10f667795697eb536d4bd3851da49 Mon Sep 17 00:00:00 2001 From: Louis R <covert8@users.noreply.github.com> Date: Thu, 28 Mar 2024 06:31:28 +0100 Subject: [PATCH 55/98] Fixed: Exceptions when checking for routable IPv4 addresses --- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 36 +++++++++++++------ src/NzbDrone.Core.Test/Framework/CoreTest.cs | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 2215a953f..678e16548 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -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 { diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index a55bd51a4..8f8b7723c 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -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()); } From f010f56290bd7df301391f917928d2edefc4509d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Sat, 30 Mar 2024 18:46:47 +0000 Subject: [PATCH 56/98] Multiple Translations updated by Weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/NzbDrone.Core/Localization/Core/es.json | 63 ++++++++++--------- .../Localization/Core/pt_BR.json | 5 +- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 8b9fbedcf..777377345 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -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,7 +530,7 @@ "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", @@ -543,7 +543,7 @@ "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", @@ -579,8 +579,8 @@ "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, por defecto es '{port}'", - "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)", + "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", @@ -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 usarán todas las opciones.", + "IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usarán 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" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 65b22e2ad..44c9c1038 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -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" } From 5a66b949cf5a08483feb1d5b24e936d3a6004892 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Mon, 1 Apr 2024 00:14:26 +0000 Subject: [PATCH 57/98] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index cd2552f4d..f6befb85f 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -5143,6 +5143,23 @@ } } } + }, + "head": { + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResource" + } + } + } + } + } } }, "/api/v3/qualitydefinition/{id}": { From 4e83820511b35f67478d6084fef15d547d793035 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 31 Mar 2024 21:01:14 -0700 Subject: [PATCH 58/98] Bump version to 4.0.3 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3834c7d35..74a9b33df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: From 60ee7cc716d344fc904fa6fb28f7be0386ae710d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:06:35 -0700 Subject: [PATCH 59/98] Fixed: Cleanse BHD RSS key in log files Closes #6666 --- .../InstrumentationTests/CleanseLogMessageFixture.cs | 2 ++ src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 6b1ea4171..cd8ed3476 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -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"" }, ")] diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index 12d027afa..c2b496302 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -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), From 0937ee6fefce628b1641f87ca77a626d483f1e79 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 2 Apr 2024 17:09:07 -0700 Subject: [PATCH 60/98] Fixed: Path parsing incorrectly treating series title as episode number --- .../ParserTests/PathParserFixture.cs | 2 ++ .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 36 ++++++++++++------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 8ef926316..f2c8d2a84 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -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) diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 3f4eeee25..f1a7bdcb4 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -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; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9104af5ed..53dba36e3 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -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); + pathTitle += $"-E{last:00}"; + } - return parsedFileInfo; + 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 From 6003ca1696adde0dafb695e383007d17e274fa73 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 2 Apr 2024 20:59:08 -0700 Subject: [PATCH 61/98] Fixed: Deleted episodes not being unmonitored when series folder has been deleted Closes #6678 --- .../MediaFiles/DiskScanService.cs | 12 +++++-- .../MediaFiles/MediaFileDeletionService.cs | 34 ++++++++++--------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 999d4d067..f7d878f16 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -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); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index 064f15157..bd8d66025 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -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)) - { - if (_diskProvider.FolderExists(folder)) - { - _diskProvider.RemoveEmptySubfolders(folder); - } + var series = message.EpisodeFile.Series.Value; + var seriesPath = series.Path; + var folder = message.EpisodeFile.Path.GetParentPath(); - folder = folder.GetParentPath(); + while (seriesPath.IsParentPath(folder)) + { + if (_diskProvider.FolderExists(folder)) + { + _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); } } } From 2ef46e5b902b42cdbafbd80b3154a5d22b090f29 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Thu, 21 Mar 2024 01:22:05 +0100 Subject: [PATCH 62/98] Fix incorrect subtitle copy regex --- src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs | 1 + src/NzbDrone.Core/Parser/LanguageParser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index af422fa1b..dce7fafc6 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -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) { diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index d1c995b87..4071539c0 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -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) { From 0a7f3a12c2be783c6374864e1e8f49ff5e969166 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Thu, 21 Mar 2024 02:04:17 +0100 Subject: [PATCH 63/98] Do not remove all extras when script importing --- src/NzbDrone.Core/Extras/ExistingExtraFileService.cs | 8 ++++---- src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs | 2 +- src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs | 9 +++++++-- .../Extras/Metadata/ExistingMetadataImporter.cs | 4 ++-- .../Extras/Others/ExistingOtherExtraImporter.cs | 4 ++-- .../Extras/Subtitles/ExistingSubtitleImporter.cs | 4 ++-- .../MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index ae17b44c6..14aba7b31 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -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, bool keepExistingEntries); } 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, bool keepExistingEntries) { _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, keepExistingEntries); 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, false); _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); } diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs index ad14b60a5..d2dd17265 100644 --- a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -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, bool keepExistingEntries); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index a2dddaa69..c45f2dbc8 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,10 +19,15 @@ 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, bool keepExistingEntries); - 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) { + if (keepExistingEntries) + { + return Filter(series, filesOnDisk, importedFiles, new List<TExtraFile>()); + } + var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); Clean(series, filesOnDisk, importedFiles, seriesFiles); diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 8ceb31a7f..42c1dc1ae 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -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, bool keepExistingEntries) { _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, keepExistingEntries); foreach (var possibleMetadataFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 572149965..1ac074394 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -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, bool keepExistingEntries) { _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, keepExistingEntries); foreach (var possibleExtraFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index 05fdf5770..9ca3f3e67 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -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, bool keepExistingEntries) { _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, keepExistingEntries); foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index d591a068d..030079081 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -176,7 +176,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (localEpisode.ScriptImported) { - _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles); + _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, true); if (localEpisode.FileRenamedAfterScriptImport) { From af5a681ab7edf7f72544647c490216907853d77d Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Sun, 24 Mar 2024 15:08:59 +0100 Subject: [PATCH 64/98] Fix ignoring title based on pre-rename episodefile --- .../AggregateSubtitleInfoFixture.cs | 19 ++++++++++--------- .../Extras/ExistingExtraFileService.cs | 8 ++++---- .../Extras/IImportExistingExtraFiles.cs | 2 +- .../Extras/ImportExistingExtraFilesBase.cs | 2 +- .../Metadata/ExistingMetadataImporter.cs | 4 ++-- .../Others/ExistingOtherExtraImporter.cs | 4 ++-- .../Subtitles/ExistingSubtitleImporter.cs | 7 ++++--- .../MediaFiles/EpisodeFileMovingService.cs | 2 +- .../Aggregators/AggregateSubtitleInfo.cs | 6 +++--- .../EpisodeImport/ImportApprovedEpisodes.cs | 4 ++-- .../Parser/Model/LocalEpisode.cs | 2 +- 11 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs index d5e4a472a..201537188 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs @@ -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"); } diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 14aba7b31..d357c5ba6 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Extras { public interface IExistingExtraFiles { - List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, bool keepExistingEntries); + 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, bool keepExistingEntries) + 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, keepExistingEntries); + 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, false); + var importedFiles = ImportExtraFiles(series, possibleExtraFiles, null); _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); } diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs index d2dd17265..97b85d80f 100644 --- a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -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, bool keepExistingEntries); + IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index c45f2dbc8..c3ae44cba 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries); + 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, bool keepExistingEntries) { diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 42c1dc1ae..373282259 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -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, bool keepExistingEntries) + 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, keepExistingEntries); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleMetadataFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 1ac074394..ea8d021de 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -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, bool keepExistingEntries) + 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, keepExistingEntries); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleExtraFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index 9ca3f3e67..631c92be3 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -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, bool keepExistingEntries) + 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, keepExistingEntries); + 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 diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index dde75161a..e186246ba 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -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) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs index 65eaf0cca..53418a6ff 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs @@ -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))) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 030079081..4cdf288fd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -176,9 +176,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (localEpisode.ScriptImported) { - _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, true); + _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, localEpisode.FileNameBeforeRename); - if (localEpisode.FileRenamedAfterScriptImport) + if (localEpisode.FileNameBeforeRename != episodeFile.RelativePath) { _extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile); } diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 65f6e84f8..0129c5d0c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -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; } From 7776ec995571a6bc3ff1a35bbede02c05b943063 Mon Sep 17 00:00:00 2001 From: Jendrik Weise <jewe37@gmail.com> Date: Tue, 26 Mar 2024 15:25:23 +0100 Subject: [PATCH 65/98] Reimport files imported prematurely during script import --- src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index c3ae44cba..fdfc7e67e 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -23,13 +23,17 @@ namespace NzbDrone.Core.Extras 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>()); } - var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); - Clean(series, filesOnDisk, importedFiles, seriesFiles); return Filter(series, filesOnDisk, importedFiles, seriesFiles); From 1562d3bae3002947f9e428321d2b162ad69c3309 Mon Sep 17 00:00:00 2001 From: Cuki <2996147+cuki@users.noreply.github.com> Date: Sat, 6 Apr 2024 08:08:08 +0200 Subject: [PATCH 66/98] Fixed: Use widely supported display mode for PWA --- frontend/src/Content/Images/Icons/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json index 9da62fe58..c7bd44495 100644 --- a/frontend/src/Content/Images/Icons/manifest.json +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -15,5 +15,5 @@ "start_url": "../../../../", "theme_color": "#3a3f51", "background_color": "#3a3f51", - "display": "minimal-ui" + "display": "standalone" } From 238ba85f0a2639608d9890292dfe0b96c0212f10 Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 6 Apr 2024 08:08:57 +0200 Subject: [PATCH 67/98] New: Informational text on Custom Formats modal --- .../CustomFormats/EditCustomFormatModalContent.js | 5 +++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 2 files changed, 6 insertions(+) diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js index 33497ce44..44fa9c5ca 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js @@ -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) => { diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b5d0cb157..5e614e84c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -278,6 +278,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", From e672996dbb5f15226f505b080f2c038c0c964300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kr=C3=BCss?= <tillkruss@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:09:55 -0700 Subject: [PATCH 68/98] Improve text for file deleted through UI/API --- src/NzbDrone.Core/Localization/Core/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5e614e84c..983284b25 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -379,7 +379,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", From 7fc3bebc91db217a1c24ab2d01ebbc5bf03c918e Mon Sep 17 00:00:00 2001 From: fireph <443370+fireph@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:10:42 -0700 Subject: [PATCH 69/98] New: Footnote to indicate some renaming tokens support truncation --- .../MediaManagement/Naming/NamingModal.js | 55 ++++++++++++------- src/NzbDrone.Core/Localization/Core/en.json | 3 + 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index f873ec1d9..eec2449cd 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -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')}> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 983284b25..b41d10d93 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -636,6 +636,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", @@ -1590,6 +1591,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", @@ -1780,6 +1782,7 @@ "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", From a169ebff2adda5c8585c6aae6249b1c1f7c12264 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:11:03 -0700 Subject: [PATCH 70/98] Fixed: Sending ntfy.sh notifications with unicode characters Closes #6679 --- src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs index 4198ce38c..d35810db8 100644 --- a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs +++ b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs @@ -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()) From 74cdf01e49565c5bc63b0e90890a9e170caa3db4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Fri, 5 Apr 2024 23:11:17 -0700 Subject: [PATCH 71/98] New: Set 'Release Type' during Manual Import Closes #6681 --- frontend/src/Episode/getReleaseTypeName.ts | 17 ++++ frontend/src/EpisodeFile/EpisodeFile.ts | 2 + .../InteractiveImportModalContent.tsx | 41 +++++++- .../Interactive/InteractiveImportRow.tsx | 44 ++++++++- .../InteractiveImport/InteractiveImport.ts | 1 + .../ReleaseType/SelectReleaseTypeModal.tsx | 30 ++++++ .../SelectReleaseTypeModalContent.tsx | 99 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../ManualImport/ManualImportController.cs | 1 + 10 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 frontend/src/Episode/getReleaseTypeName.ts create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx diff --git a/frontend/src/Episode/getReleaseTypeName.ts b/frontend/src/Episode/getReleaseTypeName.ts new file mode 100644 index 000000000..a2bb1af5b --- /dev/null +++ b/frontend/src/Episode/getReleaseTypeName.ts @@ -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'); + } +} diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts index 53dd53750..da362db82 100644 --- a/frontend/src/EpisodeFile/EpisodeFile.ts +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -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; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index e421db602..dbcd10613 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -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} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 2f6f11af4..3a2b81874 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -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} diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 1feea60c0..d9e0b1b04 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -15,6 +15,7 @@ export interface InteractiveImportCommandOptions { quality: QualityModel; languages: Language[]; indexerFlags: number; + releaseType: ReleaseType; downloadId?: string; episodeFileId?: number; } diff --git a/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx new file mode 100644 index 000000000..720914b43 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx @@ -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; diff --git a/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx new file mode 100644 index 000000000..610811195 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx @@ -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; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b41d10d93..1e47f15ea 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -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", @@ -1777,6 +1778,7 @@ "SelectLanguages": "Select Languages", "SelectQuality": "Select Quality", "SelectReleaseGroup": "Select Release Group", + "SelectReleaseType": "Select Release Type", "SelectSeason": "Select Season", "SelectSeasonModalTitle": "{modalTitle} - Select Season", "SelectSeries": "Select Series", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 4cdf288fd..39c3c849f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -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 diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index eb6787c5b..46ab91a95 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -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); From aca10f6f4f379366edf28af1e1a4f4f9529d915b Mon Sep 17 00:00:00 2001 From: Qstick <qstick@gmail.com> Date: Sun, 9 Jul 2023 23:08:05 -0500 Subject: [PATCH 72/98] 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) --- src/NzbDrone.Core/Tv/MoveSeriesService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/NzbDrone.Core/Tv/MoveSeriesService.cs b/src/NzbDrone.Core/Tv/MoveSeriesService.cs index 49e245d11..be9037d96 100644 --- a/src/NzbDrone.Core/Tv/MoveSeriesService.cs +++ b/src/NzbDrone.Core/Tv/MoveSeriesService.cs @@ -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 From dac69445e4ab77fbce093b8dd859390e2e8fef2d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Thu, 4 Apr 2024 11:58:59 +0000 Subject: [PATCH 73/98] 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 --- src/NzbDrone.Core/Localization/Core/fr.json | 5 ++++- src/NzbDrone.Core/Localization/Core/ro.json | 4 +++- src/NzbDrone.Core/Localization/Core/uk.json | 12 +++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index b6b0239e5..01c5df548 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -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" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index d6a639da8..183427ba3 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -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" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index e6b2d5d39..a514c1313 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -94,5 +94,15 @@ "AudioLanguages": "Мови аудіо", "AuthForm": "Форми (сторінка входу)", "Authentication": "Автентифікація", - "AuthenticationMethod": "Метод автентифікації" + "AuthenticationMethod": "Метод автентифікації", + "Yes": "Так", + "AuthenticationRequired": "Потрібна Автентифікація", + "UpdateAll": "Оновити все", + "WhatsNew": "Що нового ?", + "Yesterday": "Вчора", + "AddedToDownloadQueue": "Додано в чергу на завантаження", + "AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.", + "AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм", + "AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль", + "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача" } From 5c42935eb3add91051aab84b6a19cdd10e007546 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 6 Apr 2024 12:34:28 -0700 Subject: [PATCH 74/98] Fixed: Improve AniList testing with Media filters --- .../ImportLists/AniList/List/AniListImport.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs index f2ecb26e7..adcb9b9d9 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -12,6 +13,7 @@ using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.AniList.List { @@ -153,5 +155,63 @@ namespace NzbDrone.Core.ImportLists.AniList.List return new ImportListFetchResult(CleanupListItems(releases), anyFailure); } + + protected override ValidationFailure TestConnection() + { + try + { + var parser = GetParser(); + var generator = GetRequestGenerator(); + var pageIndex = 1; + var continueTesting = true; + var hasResults = false; + + // Anilist caps the result list to 50 items at maximum per query, so the data must be pulled in batches. + // The number of pages are not known upfront, so the fetch logic must be changed to look at the returned page data. + do + { + var currentRequest = generator.GetRequest(pageIndex); + var response = FetchImportListResponse(currentRequest); + var page = parser.ParseResponse(response, out var pageInfo).ToList(); + + // Continue testing additional pages if all results were filtered out by 'Media' filters and there are additional pages + continueTesting = pageInfo.HasNextPage && page.Count == 0; + pageIndex = pageInfo.CurrentPage + 1; + hasResults = page.Count > 0; + } + while (continueTesting); + + if (!hasResults) + { + return new NzbDroneValidationFailure(string.Empty, + "No results were returned from your import list, please check your settings and the log for details.") + { IsWarning = true }; + } + } + catch (RequestLimitReachedException) + { + _logger.Warn("Request limit reached"); + } + catch (UnsupportedFeedException ex) + { + _logger.Warn(ex, "Import list feed is not supported"); + + return new ValidationFailure(string.Empty, "Import list feed is not supported: " + ex.Message); + } + catch (ImportListException ex) + { + _logger.Warn(ex, "Unable to connect to import list"); + + return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details."); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to import list"); + + return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details."); + } + + return null; + } } } From 37863a8deb339ef730b2dd5be61e1da1311fdd23 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Apr 2024 16:12:20 -0700 Subject: [PATCH 75/98] New: Option to prefix app name on Telegram notification titles --- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ .../Notifications/Telegram/Telegram.cs | 36 ++++++++++++++----- .../Notifications/Telegram/TelegramProxy.cs | 3 +- .../Telegram/TelegramSettings.cs | 3 ++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 1e47f15ea..c751eefe9 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1409,6 +1409,8 @@ "NotificationsTelegramSettingsBotToken": "Bot Token", "NotificationsTelegramSettingsChatId": "Chat ID", "NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages", + "NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications", "NotificationsTelegramSettingsSendSilently": "Send Silently", "NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound", "NotificationsTelegramSettingsTopicId": "Topic ID", diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 3d056d115..1edbfa909 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -18,47 +18,65 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE; + + _proxy.SendNotification(title, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { - _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE; + + _proxy.SendNotification(title, deleteMessage.Message, Settings); } public override void OnSeriesAdd(SeriesAddMessage message) { - _proxy.SendNotification(SERIES_ADDED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { - _proxy.SendNotification(SERIES_DELETED_TITLE, deleteMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE; + + _proxy.SendNotification(title, deleteMessage.Message, Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { - _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; + + _proxy.SendNotification(title, healthCheck.Message, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; + + _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { - _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; + + _proxy.SendNotification(title, updateMessage.Message, Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { - _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs index fbeb625df..f1cc39f1a 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs @@ -54,10 +54,11 @@ namespace NzbDrone.Core.Notifications.Telegram { try { + const string brandedTitle = "Sonarr - Test Notification"; const string title = "Test Notification"; const string body = "This is a test message from Sonarr"; - SendNotification(title, body, settings); + SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index ede7b3ad3..2b768ce45 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -32,6 +32,9 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(3, Label = "NotificationsTelegramSettingsSendSilently", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsSendSilentlyHelpText")] public bool SendSilently { get; set; } + [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] + public bool IncludeAppNameInTitle { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); From 5061dc4b5e5ea9925740496a5939a1762788b793 Mon Sep 17 00:00:00 2001 From: Josh McKinney <joshka@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:12:58 -0700 Subject: [PATCH 76/98] Add DevContainer, VSCode config and extensions.json --- .devcontainer/devcontainer.json | 19 ++++++++++++++ .github/dependabot.yml | 12 +++++++++ .gitignore | 1 + .vscode/extensions.json | 7 ++++++ .vscode/launch.json | 26 +++++++++++++++++++ .vscode/tasks.json | 44 +++++++++++++++++++++++++++++++++ src/Directory.Build.props | 34 +++++++++++++++++++++++-- 7 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/dependabot.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..629a2aa21 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "Sonarr", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "16", + "nvmVersion": "latest" + } + }, + "forwardPorts": [8989], + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f33a02cd1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 73bd6ad62..4094c46a6 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ coverage*.xml coverage*.json setup/Output/ *.~is +.mono #VS outout folders bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7a36fefe1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "ms-dotnettools.csdevkit", + "ms-vscode-remote.remote-containers" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..6ea80f418 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": "Run Sonarr", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build dotnet", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/_output/net6.0/Sonarr", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..cfd41d42f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build dotnet", + "command": "dotnet", + "type": "process", + "args": [ + "msbuild", + "-restore", + "${workspaceFolder}/src/Sonarr.sln", + "-p:GenerateFullPaths=true", + "-p:Configuration=Debug", + "-p:Platform=Posix", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Sonarr.sln", + "-property:GenerateFullPaths=true", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/Sonarr.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ef0944ea9..239a98d73 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -175,16 +175,46 @@ </Otherwise> </Choose> + <!-- + Set architecture to RuntimeInformation.ProcessArchitecture if not specified --> + <Choose> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'"> + <PropertyGroup> + <Architecture>x64</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'"> + <PropertyGroup> + <Architecture>x86</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'"> + <PropertyGroup> + <Architecture>arm64</Architecture> + </PropertyGroup> + </When> + <When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'"> + <PropertyGroup> + <Architecture>arm</Architecture> + </PropertyGroup> + </When> + <Otherwise> + <PropertyGroup> + <Architecture></Architecture> + </PropertyGroup> + </Otherwise> + </Choose> + <PropertyGroup Condition="'$(IsWindows)' == 'true' and '$(RuntimeIdentifier)' == ''"> <_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier> - <RuntimeIdentifier>win-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier> </PropertyGroup> <PropertyGroup Condition="'$(IsLinux)' == 'true' and '$(RuntimeIdentifier)' == ''"> <_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier> - <RuntimeIdentifier>linux-x64</RuntimeIdentifier> + <RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier> </PropertyGroup> <PropertyGroup Condition="'$(IsOSX)' == 'true' and From f4c19a384bd9bb4e35c9fa0ca5d9a448c04e409e Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sun, 7 Apr 2024 16:22:21 -0700 Subject: [PATCH 77/98] New: Auto tag series based on tags present/absent on series Closes #6236 --- .../src/Components/Form/FormInputGroup.js | 4 ++ .../Components/Form/ProviderFieldFormGroup.js | 2 + .../src/Components/Form/SeriesTagInput.tsx | 53 +++++++++++++++++++ frontend/src/Helpers/Props/inputTypes.js | 2 + frontend/src/Settings/Tags/TagInUse.js | 2 +- .../Housekeepers/CleanupUnusedTagsFixture.cs | 33 ++++++++++++ .../Annotations/FieldDefinitionAttribute.cs | 3 +- .../Specifications/TagSpecification.cs | 36 +++++++++++++ .../Housekeepers/CleanupUnusedTags.cs | 36 +++++++++++-- src/NzbDrone.Core/Localization/Core/en.json | 1 + src/NzbDrone.Core/Tags/TagService.cs | 23 +++++++- 11 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 frontend/src/Components/Form/SeriesTagInput.tsx create mode 100644 src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index f7b2ce75e..7a3191cdc 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -22,6 +22,7 @@ import PasswordInput from './PasswordInput'; import PathInputConnector from './PathInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; +import SeriesTagInput from './SeriesTagInput'; import SeriesTypeSelectInput from './SeriesTypeSelectInput'; import TagInputConnector from './TagInputConnector'; import TagSelectInputConnector from './TagSelectInputConnector'; @@ -87,6 +88,9 @@ function getComponent(type) { case inputTypes.DYNAMIC_SELECT: return EnhancedSelectInputConnector; + case inputTypes.SERIES_TAG: + return SeriesTagInput; + case inputTypes.SERIES_TYPE_SELECT: return SeriesTypeSelectInput; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index a184aa1ec..4fcf99cc0 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.DYNAMIC_SELECT; } return inputTypes.SELECT; + case 'seriesTag': + return inputTypes.SERIES_TAG; case 'tag': return inputTypes.TEXT_TAG; case 'tagSelect': diff --git a/frontend/src/Components/Form/SeriesTagInput.tsx b/frontend/src/Components/Form/SeriesTagInput.tsx new file mode 100644 index 000000000..3d8279aa6 --- /dev/null +++ b/frontend/src/Components/Form/SeriesTagInput.tsx @@ -0,0 +1,53 @@ +import React, { useCallback } from 'react'; +import TagInputConnector from './TagInputConnector'; + +interface SeriesTageInputProps { + name: string; + value: number | number[]; + onChange: ({ + name, + value, + }: { + name: string; + value: number | number[]; + }) => void; +} + +export default function SeriesTagInput(props: SeriesTageInputProps) { + const { value, onChange, ...otherProps } = props; + const isArray = Array.isArray(value); + + const handleChange = useCallback( + ({ name, value: newValue }: { name: string; value: number[] }) => { + if (isArray) { + onChange({ name, value: newValue }); + } else { + onChange({ + name, + value: newValue.length ? newValue[newValue.length - 1] : 0, + }); + } + }, + [isArray, onChange] + ); + + let finalValue: number[] = []; + + if (isArray) { + finalValue = value; + } else if (value === 0) { + finalValue = []; + } else { + finalValue = [value]; + } + + return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore 2786 'TagInputConnector' isn't typed yet + <TagInputConnector + {...otherProps} + value={finalValue} + onChange={handleChange} + /> + ); +} diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index dcf4b539c..a71c28d8c 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -17,6 +17,7 @@ export const LANGUAGE_SELECT = 'languageSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; +export const SERIES_TAG = 'seriesTag'; export const DYNAMIC_SELECT = 'dynamicSelect'; export const SERIES_TYPE_SELECT = 'seriesTypeSelect'; export const TAG = 'tag'; @@ -45,6 +46,7 @@ export const all = [ ROOT_FOLDER_SELECT, LANGUAGE_SELECT, SELECT, + SERIES_TAG, DYNAMIC_SELECT, SERIES_TYPE_SELECT, TAG, diff --git a/frontend/src/Settings/Tags/TagInUse.js b/frontend/src/Settings/Tags/TagInUse.js index 9fb57d230..27228fa2e 100644 --- a/frontend/src/Settings/Tags/TagInUse.js +++ b/frontend/src/Settings/Tags/TagInUse.js @@ -12,7 +12,7 @@ export default function TagInUse(props) { return null; } - if (count > 1 && labelPlural ) { + if (count > 1 && labelPlural) { return ( <div> {count} {labelPlural.toLowerCase()} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs index a26f08a67..99b6676be 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Tags; @@ -45,5 +48,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); AllStoredModels.Should().HaveCount(1); } + + [Test] + public void should_not_delete_used_auto_tagging_tag_specification_tags() + { + var tags = Builder<Tag> + .CreateListOfSize(2) + .All() + .With(x => x.Id = 0) + .BuildList(); + Db.InsertMany(tags); + + var autoTags = Builder<AutoTag>.CreateListOfSize(1) + .All() + .With(x => x.Id = 0) + .With(x => x.Specifications = new List<IAutoTaggingSpecification> + { + new TagSpecification + { + Name = "Test", + Value = tags[0].Id + } + }) + .BuildList(); + + Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All()) + .Returns(autoTags); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } } } diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index a0a1896e0..398376117 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -85,7 +85,8 @@ namespace NzbDrone.Core.Annotations Device, TagSelect, RootFolder, - QualityProfile + QualityProfile, + SeriesTag } public enum HiddenType diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs new file mode 100644 index 000000000..c736f8899 --- /dev/null +++ b/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.AutoTagging.Specifications +{ + public class TagSpecificationValidator : AbstractValidator<TagSpecification> + { + public TagSpecificationValidator() + { + RuleFor(c => c.Value).GreaterThan(0); + } + } + + public class TagSpecification : AutoTaggingSpecificationBase + { + private static readonly TagSpecificationValidator Validator = new (); + + public override int Order => 1; + public override string ImplementationName => "Tag"; + + [FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.SeriesTag)] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(Series series) + { + return series.Tags.Contains(Value); + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 9bd726ba3..c68d053f9 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Data; using System.Linq; using Dapper; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -9,17 +11,24 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public class CleanupUnusedTags : IHousekeepingTask { private readonly IMainDatabase _database; + private readonly IAutoTaggingRepository _autoTaggingRepository; - public CleanupUnusedTags(IMainDatabase database) + public CleanupUnusedTags(IMainDatabase database, IAutoTaggingRepository autoTaggingRepository) { _database = database; + _autoTaggingRepository = autoTaggingRepository; } public void Clean() { using var mapper = _database.OpenConnection(); - var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" } + var usedTags = new[] + { + "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", + "AutoTagging", "DownloadClients" + } .SelectMany(v => GetUsedTags(v, mapper)) + .Concat(GetAutoTaggingTagSpecificationTags(mapper)) .Distinct() .ToArray(); @@ -37,10 +46,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private int[] GetUsedTags(string table, IDbConnection mapper) { - return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") + return mapper + .Query<List<int>>( + $"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") .SelectMany(x => x) .Distinct() .ToArray(); } + + private List<int> GetAutoTaggingTagSpecificationTags(IDbConnection mapper) + { + var tags = new List<int>(); + var autoTags = _autoTaggingRepository.All(); + + foreach (var autoTag in autoTags) + { + foreach (var specification in autoTag.Specifications) + { + if (specification is TagSpecification tagSpec) + { + tags.Add(tagSpec.Value); + } + } + } + + return tags; + } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index c751eefe9..f6b312928 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -136,6 +136,7 @@ "AutoTaggingSpecificationRootFolder": "Root Folder", "AutoTaggingSpecificationSeriesType": "Series Type", "AutoTaggingSpecificationStatus": "Status", + "AutoTaggingSpecificationTag": "Tag", "Automatic": "Automatic", "AutomaticAdd": "Automatic Add", "AutomaticSearch": "Automatic Search", diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index da67f0705..b97b279a5 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.ImportLists; @@ -120,7 +121,7 @@ namespace NzbDrone.Core.Tags var restrictions = _releaseProfileService.All(); var series = _seriesService.GetAllSeriesTags(); var indexers = _indexerService.All(); - var autotags = _autoTaggingService.All(); + var autoTags = _autoTaggingService.All(); var downloadClients = _downloadClientFactory.All(); var details = new List<TagDetails>(); @@ -137,7 +138,7 @@ namespace NzbDrone.Core.Tags RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), SeriesIds = series.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(), IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), - AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + AutoTagIds = GetAutoTagIds(tag, autoTags), DownloadClientIds = downloadClients.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), }); } @@ -188,5 +189,23 @@ namespace NzbDrone.Core.Tags _repo.Delete(tagId); _eventAggregator.PublishEvent(new TagsUpdatedEvent()); } + + private List<int> GetAutoTagIds(Tag tag, List<AutoTag> autoTags) + { + var autoTagIds = autoTags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(); + + foreach (var autoTag in autoTags) + { + foreach (var specification in autoTag.Specifications) + { + if (specification is TagSpecification) + { + autoTagIds.Add(autoTag.Id); + } + } + } + + return autoTagIds.Distinct().ToList(); + } } } From fc06e5135213f218648c8b36747d3bdf361f08b4 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:13:59 +0300 Subject: [PATCH 78/98] Fixed: Renaming episodes for a series Closes #6640 --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index e186246ba..2518df234 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -122,7 +122,11 @@ namespace NzbDrone.Core.MediaFiles } episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); - localEpisode.FileNameBeforeRename = episodeFile.RelativePath; + + if (localEpisode is not null) + { + localEpisode.FileNameBeforeRename = episodeFile.RelativePath; + } if (localEpisode is not null && _scriptImportDecider.TryImport(episodeFilePath, destinationFilePath, localEpisode, episodeFile, mode) is var scriptImportDecision && scriptImportDecision != ScriptImportDecision.DeferMove) { From 1aef91041e404f76f278f430e4e53140fb125792 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:44:07 +0300 Subject: [PATCH 79/98] New: Detect shfs mounts in disk space --- src/NzbDrone.Mono/Disk/FindDriveType.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Mono/Disk/FindDriveType.cs b/src/NzbDrone.Mono/Disk/FindDriveType.cs index d0481c3d4..08cc611de 100644 --- a/src/NzbDrone.Mono/Disk/FindDriveType.cs +++ b/src/NzbDrone.Mono/Disk/FindDriveType.cs @@ -6,15 +6,16 @@ namespace NzbDrone.Mono.Disk { public static class FindDriveType { - private static readonly Dictionary<string, DriveType> DriveTypeMap = new Dictionary<string, DriveType> - { - { "afpfs", DriveType.Network }, - { "apfs", DriveType.Fixed }, - { "fuse.mergerfs", DriveType.Fixed }, - { "fuse.glusterfs", DriveType.Network }, - { "nullfs", DriveType.Fixed }, - { "zfs", DriveType.Fixed } - }; + private static readonly Dictionary<string, DriveType> DriveTypeMap = new () + { + { "afpfs", DriveType.Network }, + { "apfs", DriveType.Fixed }, + { "fuse.mergerfs", DriveType.Fixed }, + { "fuse.shfs", DriveType.Fixed }, + { "fuse.glusterfs", DriveType.Network }, + { "nullfs", DriveType.Fixed }, + { "zfs", DriveType.Fixed } + }; public static DriveType Find(string driveFormat) { From 1fcd2b492c9c02713f708f529bcf090ca48d8523 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:09:12 +0300 Subject: [PATCH 80/98] Prevent multiple enumerations in Custom Formats token --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index ab532f0a5..29cab2edb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -706,7 +706,7 @@ namespace NzbDrone.Core.Organizer return string.Empty; } - return customFormats.Where(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat).FirstOrDefault()?.ToString() ?? string.Empty; + return customFormats.FirstOrDefault(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat)?.ToString() ?? string.Empty; }; } @@ -719,7 +719,7 @@ namespace NzbDrone.Core.Organizer private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter) { - var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming); + var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming).ToList(); var filteredTokens = tokens; From 476e7a7b94406c8424a2939939df87d9614e693f Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:14:56 +0300 Subject: [PATCH 81/98] Fixed: Changing Release Type in Manage Episodes Closes #6706 --- src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index d77338ea3..552a34326 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Http.REST; @@ -26,7 +27,7 @@ namespace Sonarr.Api.V3.EpisodeFiles public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int? IndexerFlags { get; set; } - public int? ReleaseType { get; set; } + public ReleaseType? ReleaseType { get; set; } public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -64,7 +65,7 @@ namespace Sonarr.Api.V3.EpisodeFiles CustomFormats = customFormats.ToResource(false), CustomFormatScore = customFormatScore, IndexerFlags = (int)model.IndexerFlags, - ReleaseType = (int)model.ReleaseType, + ReleaseType = model.ReleaseType, }; } } From 4b8afe3d33ffcd311918c38f62858483bbd8265d Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 8 Apr 2024 10:59:00 +0000 Subject: [PATCH 82/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: fordas <fordas15@gmail.com> Co-authored-by: myrad2267 <myrad2267@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/es.json | 10 ++++++++-- src/NzbDrone.Core/Localization/Core/fr.json | 12 +++++++++--- src/NzbDrone.Core/Localization/Core/pt_BR.json | 7 ++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 777377345..cd074ec2a 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -267,7 +267,7 @@ "DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada", "DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?", "DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales", - "DeletedReasonManual": "El archivo fue borrado por vía UI", + "DeletedReasonManual": "El archivo fue eliminado usando {appName}, o bien manualmente o por otra herramienta a través de la API", "ClearBlocklist": "Limpiar lista de bloqueos", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", "MonitorPilotEpisode": "Episodio Piloto", @@ -2060,5 +2060,11 @@ "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" + "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas", + "CustomFormatsSettingsTriggerInfo": "Un formato personalizado será aplicado al lanzamiento o archivo cuando coincida con al menos uno de los diferentes tipos de condición elegidos.", + "ClickToChangeReleaseType": "Haz clic para cambiar el tipo de lanzamiento", + "ReleaseGroupFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Grupo de lanzamiento:30}`) como desde el principio (p. ej. `{Grupo de lanzamiento:-30}`).", + "SelectReleaseType": "Seleccionar tipo de lanzamiento", + "SeriesFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de serie:30}`) como desde el principio (p. ej. `{Título de serie:-30}`).", + "EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario." } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 01c5df548..bfcedc8d1 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1102,7 +1102,7 @@ "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Activer la gestion des téléchargements terminés si possible (multi-ordinateur non pris en charge)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Activer la gestion des téléchargements terminés", "ImportUsingScript": "Importer à l'aide d'un script", - "IncludeHealthWarnings": "Inclure des avertissements de santé", + "IncludeHealthWarnings": "Inclure les avertissements de santé", "Indexer": "Indexeur", "LibraryImportTipsSeriesUseRootFolder": "Pointez {appName} vers le dossier contenant toutes vos émissions de télévision, pas une en particulier. par exemple. \"`{goodFolderExample}`\" et non \"`{badFolderExample}`\". De plus, chaque série doit se trouver dans son propre dossier dans le dossier racine/bibliothèque.", "Links": "Liens", @@ -1249,7 +1249,7 @@ "Debug": "Déboguer", "DelayProfileSeriesTagsHelpText": "S'applique aux séries avec au moins une balise correspondante", "DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}", - "DeletedReasonManual": "Le fichier a été supprimé via l'interface utilisateur", + "DeletedReasonManual": "Le fichier a été supprimé à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API.", "DeleteRemotePathMapping": "Supprimer la correspondance de chemin distant", "DestinationPath": "Chemin de destination", "DestinationRelativePath": "Chemin relatif de destination", @@ -2060,5 +2060,11 @@ "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" + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authentifiez-vous avec MyAnimeList", + "CustomFormatsSettingsTriggerInfo": "Un format personnalisé sera appliqué à une version ou à un fichier lorsqu'il correspond à au moins un de chacun des différents types de conditions choisis.", + "ClickToChangeReleaseType": "Cliquez pour changer le type de version", + "EpisodeTitleFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Episode Title:30}`) ou du début (par exemple `{Episode Title:-30}`) sont toutes deux prises en charge. Les titres des épisodes seront automatiquement tronqués en fonction des limitations du système de fichiers si nécessaire.", + "SelectReleaseType": "Sélectionnez le type de version", + "SeriesFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Series Title:30}`) ou du début (par exemple `{Series Title:-30}`) sont toutes deux prises en charge.", + "ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`)." } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 44c9c1038..4e6bd8e3c 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -605,11 +605,11 @@ "Importing": "Importando", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear", "IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}", - "IncludeHealthWarnings": "Incluir Advertências de Saúde", + "IncludeHealthWarnings": "Incluir Alertas de Saúde", "IndexerDownloadClientHelpText": "Especifique qual cliente de download é usado para baixar deste indexador", "IndexerOptionsLoadError": "Não foi possível carregar as opções do indexador", "IndexerPriority": "Prioridade do indexador", - "IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado como desempate para lançamentos iguais ao obter lançamentos, o {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS", + "IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado ao capturar lançamentos como desempate para lançamentos iguais, {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS", "IndexerSettings": "Configurações do indexador", "IndexersLoadError": "Não foi possível carregar os indexadores", "IndexersSettingsSummary": "Indexadores e opções de indexador", @@ -2060,5 +2060,6 @@ "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" + "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como 'Todas' para todas as listas", + "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos." } From 8a7b67c593c349d04bccd4270663d3671ba920d2 Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Tue, 9 Apr 2024 23:17:30 +0000 Subject: [PATCH 83/98] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index f6befb85f..14444bdba 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -8292,9 +8292,7 @@ "nullable": true }, "releaseType": { - "type": "integer", - "format": "int32", - "nullable": true + "$ref": "#/components/schemas/ReleaseType" }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" @@ -10234,6 +10232,9 @@ "isSplitEpisode": { "type": "boolean" }, + "isMiniSeries": { + "type": "boolean" + }, "special": { "type": "boolean" }, From 0fdbbd018cb56640e1a5e1f5b48b8066bc80d222 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Tue, 9 Apr 2024 16:58:01 -0700 Subject: [PATCH 84/98] New: Parse absolute episode numbers within square brackets Closes #6694 --- .../AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 427a3e480..30c2907f2 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -134,6 +134,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)] [TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)] [TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)] + [TestCase("Series_Title_2_[01]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) @@ -179,6 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Erai-raws] Series-Title! 2 - 01~10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)] [TestCase("[Erai-raws] Series Title! - 01 ~ 10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)] [TestCase("[Erai-raws] Series-Title! 2 - 01 ~ 10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)] + [TestCase("Series_Title_2_[01-05]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 5)] // [TestCase("", "", 1, 2)] public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 53dba36e3..c4f61fbce 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -294,10 +294,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)(?:_|-|\s|\.)+S(?<season>\d{2}(?!\d+))(\W-\W)E(?<episode>(?<!\d+)\d{2}(?!\d+))(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Season and episode numbers in square brackets (single and mult-episode) + // Season and episode numbers in square brackets (single and multi-episode) // Series Title - [02x01] - Episode 1 // Series Title - [02x01x02] - Episode 1 - new Regex(@"^(?<title>.+?)?(?:[-_\W](?<![()\[!]))+\[(?<season>(?<!\d+)\d{1,2})(?:(?:-|x){1,2}(?<episode>\d{2}))+\].+?(?:\.|$)", + new Regex(@"^(?<title>.+?)?(?:[-_\W](?<![()\[!]))+\[(?:s)?(?<season>(?<!\d+)\d{1,2})(?:(?:[ex])(?<episode>\d{2}))(?:(?:[-ex]){1,2}(?<episode>\d{2}))*\].+?(?:\.|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title with season number - Absolute Episode Number (Title S01 - EP14) @@ -328,10 +328,6 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{4}(?![-_. ]?\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Episodes with a title and season/episode in square brackets - new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+\[S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+|i|p)))+\])\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Supports 103/113 naming new Regex(@"^(?<title>.+?)?(?:(?:[_.-](?<![()\[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+(?:[_.]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -409,6 +405,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+(?:Episode|Episodio)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - Title [Absolute Episode Number] from AniLibriaTV + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]\[)(?:(?:-?)(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+(?:\][-_. ]).*?(?<hash>[(\[]\w{8}[)\]])?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - Title Absolute Episode Number new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), From 9afe1c4b3fb01ebe81bf4ecbc1bd4d64ab00502b Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Fri, 12 Apr 2024 02:59:00 +0000 Subject: [PATCH 85/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Havok Dan <havokdan@yahoo.com.br> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: YSLG <1451164040@qq.com> Co-authored-by: fordas <fordas15@gmail.com> Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ 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 --- src/NzbDrone.Core/Localization/Core/es.json | 5 ++++- src/NzbDrone.Core/Localization/Core/pt_BR.json | 11 +++++++++-- src/NzbDrone.Core/Localization/Core/zh_CN.json | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index cd074ec2a..ee29b93a7 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -2066,5 +2066,8 @@ "ReleaseGroupFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Grupo de lanzamiento:30}`) como desde el principio (p. ej. `{Grupo de lanzamiento:-30}`).", "SelectReleaseType": "Seleccionar tipo de lanzamiento", "SeriesFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de serie:30}`) como desde el principio (p. ej. `{Título de serie:-30}`).", - "EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario." + "EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario.", + "AutoTaggingSpecificationTag": "Etiqueta", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Prefija opcionalmente el título de mensaje con {appName} para diferenciar notificaciones de aplicaciones diferentes" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 4e6bd8e3c..4797f8499 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -989,7 +989,7 @@ "AgeWhenGrabbed": "Tempo de vida (quando obtido)", "DelayingDownloadUntil": "Atrasando o download até {date} às {time}", "DeletedReasonEpisodeMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então o arquivo foi desvinculado do episódio no banco de dados", - "DeletedReasonManual": "O arquivo foi excluído por meio da IU", + "DeletedReasonManual": "O arquivo foi excluído usando {appName} manualmente ou por outra ferramenta por meio da API", "DownloadFailed": "Download Falhou", "DestinationRelativePath": "Caminho Relativo de Destino", "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", @@ -2061,5 +2061,12 @@ "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar com MyAnimeList", "ImportListsMyAnimeListSettingsListStatus": "Status da Lista", "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como 'Todas' para todas as listas", - "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos." + "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos.", + "EpisodeTitleFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Episode Title:30}`) ou do início (por exemplo, `{Episode Title:-30}`) é suportado. Os títulos dos episódios serão automaticamente truncados de acordo com as limitações do sistema de arquivos, se necessário.", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente, prefixe o título da mensagem com {appName} para diferenciar notificações de diferentes aplicativos", + "ReleaseGroupFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Release Group:30}`) ou do início (por exemplo, `{Release Group:-30}`) é suportado.`).", + "ClickToChangeReleaseType": "Clique para alterar o tipo de lançamento", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} no Título", + "SelectReleaseType": "Selecionar o Tipo de Lançamento", + "SeriesFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Series Title:30}`) ou do início (por exemplo, `{Series Title:-30}`) é suportado." } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 78129b628..d04a2d4b3 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -588,7 +588,6 @@ "DownloadIgnored": "忽略下载", "DownloadIgnoredEpisodeTooltip": "集下载被忽略", "EditAutoTag": "编辑自动标签", - "AddAutoTagError": "无法添加新的自动标签,请重试。", "AddImportListExclusionError": "无法添加新排除列表,请再试一次。", "AddIndexer": "添加索引器", "AddImportList": "添加导入列表", From 6b08117d7d1502c1e9cc38949efdf364d3b5f3d4 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 11 Apr 2024 16:32:28 -0700 Subject: [PATCH 86/98] Improve release notes for main releases --- .github/workflows/deploy.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c477cd8b9..4fa5b54ee 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,12 +69,38 @@ jobs: pattern: release_* merge-multiple: true + - name: Get Previous Release + id: previous-release + uses: cardinalby/git-get-release-action@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + latest: true + prerelease: ${{ inputs.branch != 'main' }} + + - name: Generate Release Notes + id: generate-release-notes + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + result-encoding: string + script: | + const { data } = await github.rest.repos.generateReleaseNotes({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: 'v${{ inputs.version }}', + target_commitish: '${{ github.sha }}', + previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}', + }) + return data.body + - name: Create release uses: ncipollo/release-action@v1 with: artifacts: _artifacts/Sonarr.* commit: ${{ github.sha }} - generateReleaseNotes: true + generateReleaseNotes: false + body: ${{ steps.generate-release-notes.outputs.result }} name: ${{ inputs.version }} prerelease: ${{ inputs.branch != 'main' }} skipIfReleaseExists: true From 10daf97d81ad97e828741ae157eb6fa228320512 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Thu, 11 Apr 2024 16:32:48 -0700 Subject: [PATCH 87/98] Improve build step dependencies --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74a9b33df..12c770a7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -217,7 +217,7 @@ jobs: deploy: if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} - needs: [backend, unit_test, unit_test_postgres, integration_test] + needs: [backend, frontend, unit_test, unit_test_postgres, integration_test] secrets: inherit uses: ./.github/workflows/deploy.yml with: @@ -228,7 +228,7 @@ jobs: notify: name: Discord Notification - needs: [backend, unit_test, unit_test_postgres, integration_test] + needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy] if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }} env: STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }} From 941985f65b0c5c810294a6462028698e4972b170 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 13 Apr 2024 09:29:35 -0700 Subject: [PATCH 88/98] Bump version to 4.0.4 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12c770a7b..736d5d8ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: FRAMEWORK: net6.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.3 + VERSION: 4.0.4 jobs: backend: From 317ce39aa26fa05d48c3a827601b71f06ea0b41a Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Mon, 15 Apr 2024 20:59:14 +0000 Subject: [PATCH 89/98] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Altair <villagermd@outlook.com> Co-authored-by: Fonkio <maxime.fabre10@gmail.com> Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com> Co-authored-by: Jacopo Luca Maria Latrofa <jacopo.latrofa@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/it/ Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/ Translation: Servarr/Sonarr --- src/NzbDrone.Core/Localization/Core/fr.json | 11 ++- src/NzbDrone.Core/Localization/Core/it.json | 3 +- src/NzbDrone.Core/Localization/Core/tr.json | 82 ++++++++++++++++++++- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index bfcedc8d1..975286610 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -68,11 +68,11 @@ "CancelPendingTask": "Êtes-vous sur de vouloir annuler cette tâche en attente ?", "Clear": "Effacer", "AddAutoTagError": "Impossible d'ajouter un nouveau tag automatique, veuillez réessayer.", - "AddConditionError": "Impossible d'ajouter une nouvelle condition, Réessayer.", + "AddConditionError": "Impossible d'ajouter une nouvelle condition, veuillez réessayer.", "AddCondition": "Ajouter une condition", "AddAutoTag": "Ajouter un tag automatique", "AddCustomFormatError": "Impossible d'ajouter un nouveau format personnalisé, veuillez réessayer.", - "AddIndexerError": "Impossible d'ajouter un nouvelle indexeur, veuillez réessayer.", + "AddIndexerError": "Impossible d'ajouter un nouvel indexeur, veuillez réessayer.", "AddNewRestriction": "Ajouter une nouvelle restriction", "AddListError": "Impossible d'ajouter une nouvelle liste, veuillez réessayer.", "AddDownloadClientError": "Impossible d'ajouter un nouveau client de téléchargement, veuillez réessayer.", @@ -534,7 +534,7 @@ "IndexerSearchNoInteractiveHealthCheckMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée. {appName} ne fournira aucun résultat de recherche interactif", "IndexerStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs : {indexerNames}", "Info": "Information", - "InstallLatest": "Installer le dernier", + "InstallLatest": "Installer la dernière", "InteractiveImportNoLanguage": "La ou les langues doivent être choisies pour chaque fichier sélectionné", "InteractiveImportNoQuality": "La qualité doit être choisie pour chaque fichier sélectionné", "InteractiveSearchModalHeader": "Recherche interactive", @@ -2066,5 +2066,8 @@ "EpisodeTitleFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Episode Title:30}`) ou du début (par exemple `{Episode Title:-30}`) sont toutes deux prises en charge. Les titres des épisodes seront automatiquement tronqués en fonction des limitations du système de fichiers si nécessaire.", "SelectReleaseType": "Sélectionnez le type de version", "SeriesFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Series Title:30}`) ou du début (par exemple `{Series Title:-30}`) sont toutes deux prises en charge.", - "ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`)." + "ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`).", + "AutoTaggingSpecificationTag": "Étiquette", + "NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 8b6adc4bf..d8113dd53 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -249,5 +249,6 @@ "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", "AutoRedownloadFailed": "Download fallito", "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", - "Cutoff": "Taglio" + "Cutoff": "Taglio", + "AddListExclusion": "Aggiungi elenco esclusioni" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 3a45b393b..adaa32070 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -136,12 +136,90 @@ "AuthenticationMethod": "Kimlik Doğrulama Yöntemi", "AuthenticationRequired": "Kimlik Doğrulama Gerekli", "AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.", - "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter sayısı kadar güncelleyiniz. Bunu ayarlar veya yapılandırma dosyası üzerinden yapabilirsiniz", "ClearBlocklistMessageText": "Engellenenler listesindeki tüm öğeleri temizlemek istediğinizden emin misiniz?", "AutomaticUpdatesDisabledDocker": "Docker güncelleme mekanizması kullanıldığında otomatik güncellemeler doğrudan desteklenmez. Kapsayıcı görüntüsünü {appName} dışında güncellemeniz veya bir komut dosyası kullanmanız gerekecek", "ConnectionLostReconnect": "{appName} otomatik bağlanmayı deneyecek veya aşağıda yeniden yükle seçeneğini işaretleyebilirsiniz.", "BlackholeWatchFolderHelpText": "{appName} uygulamasının tamamlanmış indirmeleri içe aktaracağı klasör", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Yeni şifreyi onayla", "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", - "CloneAutoTag": "Otomatik Etiketi Klonla" + "CloneAutoTag": "Otomatik Etiketi Klonla", + "Dash": "Çizgi", + "DeleteReleaseProfileMessageText": "'{name}' bu sürüm profilini silmek istediğinizden emin misiniz?", + "DownloadClientFreeboxApiError": "Freebox API'si şu hatayı döndürdü: {errorDescription}", + "DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil", + "DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?", + "DeleteRootFolderMessageText": "'{path}' kök klasörünü silmek istediğinizden emin misiniz?", + "DeleteSpecificationHelpText": "'{name}' spesifikasyonunu silmek istediğinizden emin misiniz?", + "DeletedReasonUpgrade": "Bir yükseltmeyi içe aktarmak için dosya silindi", + "DelayMinutes": "{delay} Dakika", + "DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?", + "DeleteReleaseProfile": "Sürüm Profilini Sil", + "DeleteSelectedIndexers": "Dizin Oluşturucuları Sil", + "Directory": "Rehber", + "Donate": "Bağış yap", + "DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil", + "DownloadClientFloodSettingsAdditionalTags": "Ek Etiketler", + "DownloadClientFloodSettingsPostImportTags": "İçe Aktarma Sonrası Etiketler", + "DownloadClientFloodSettingsStartOnAdd": "Eklemeye Başla", + "DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.", + "DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın", + "DefaultNameCopiedProfile": "{name} - Kopyala", + "DeleteAutoTag": "Etiketi Otomatik Sil", + "DeleteCondition": "Koşulu Sil", + "DeleteDelayProfileMessageText": "Bu gecikme profilini silmek istediğinizden emin misiniz?", + "DeleteRootFolder": "Kök Klasörü Sil", + "DeleteSpecification": "Spesifikasyonu Sil", + "DeletedReasonManual": "Dosya, {appName} kullanılarak manuel olarak veya API aracılığıyla başka bir araçla silindi", + "DeleteCustomFormatMessageText": "'{name}' özel biçimini silmek istediğinizden emin misiniz?", + "DefaultNameCopiedSpecification": "{name} - Kopyala", + "DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?", + "DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?", + "DeleteQualityProfileMessageText": "'{name}' kalite profilini silmek istediğinizden emin misiniz?", + "DeleteSelectedIndexersMessageText": "Seçilen {count} dizin oluşturucuyu silmek istediğinizden emin misiniz?", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName}, etiketi {clientName} uygulamasına ekleyemedi.", + "DownloadClientDownloadStationProviderMessage": "DSM hesabınızda 2 Faktörlü Kimlik Doğrulama etkinleştirilmişse {appName}, Download Station'a bağlanamaz", + "DownloadClientDownloadStationValidationApiVersion": "Download Station API sürümü desteklenmiyor; en az {requiredVersion} olmalıdır. {minVersion}'dan {maxVersion}'a kadar destekler", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Varsayılan hedef yok", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Medyanın özelliklerini etiket olarak ekler. İpuçları örnektir.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "İndirmelere içe aktarıldıktan sonra etiket ekler.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Flood API'sine {url} gibi bir önek ekler", + "ReplaceIllegalCharactersHelpText": "Geçersiz karakterleri değiştirin. İşaretlenmezse bunun yerine {appName} bunları kaldıracak", + "ConnectionSettingsUrlBaseHelpText": "{connectionName} URL'sine {url} gibi bir önek ekler", + "DeleteSelectedImportLists": "İçe Aktarma Listelerini Sil", + "DelayingDownloadUntil": "İndirme işlemi {date} tarihine, {time} tarihine kadar erteleniyor", + "Destination": "Hedef", + "DoNotBlocklist": "Engelleme Listesine Eklemeyin", + "DoNotBlocklistHint": "Engellenenler listesine eklemeden kaldır", + "DownloadClientDelugeTorrentStateError": "Deluge bir hata bildiriyor", + "DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil", + "DeleteImportList": "İçe Aktarma Listesini Sil", + "IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan sürümler için eşitlik bozucu olarak sürümler alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek", + "DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı", + "DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation'da '{sharedFolder}' adında bir Paylaşımlı Klasör yok, bunu doğru belirttiğinizden emin misiniz?", + "DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizin Oluşturucular'daki mevcut tohum kriterlerine göre torrentlerin otomatik olarak kaldırılmasını gerçekleştirecek", + "Database": "Veri tabanı", + "DelayProfileProtocol": "Protokol: {preferredProtocol}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "'{downloadDir}' klasörü mevcut değil, '{sharedFolder}' Paylaşımlı Klasöründe manuel olarak oluşturulması gerekiyor.", + "DeleteAutoTagHelpText": "'{name}' etiketini otomatik silmek istediğinizden emin misiniz?", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}", + "DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir", + "DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {istisnaMessage})", + "CustomFormatsSettingsTriggerInfo": "Bir yayına veya dosyaya, seçilen farklı koşul türlerinden en az biriyle eşleştiğinde Özel Format uygulanacaktır.", + "Default": "Varsayılan", + "DeleteSelectedImportListsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?", + "DownloadClientDelugeSettingsDirectory": "İndirme Dizini", + "DownloadClientDelugeSettingsDirectoryCompleted": "Tamamlandığında Dizini Taşı", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Tamamlanan indirmelerin taşınacağı isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", + "DownloadClientDelugeSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın", + "ApiKey": "API Anahtarı", + "Analytics": "Analiz", + "All": "Herşey", + "AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak", + "AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} Web arayüz sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız." } From d6278fced49b26be975c3a6039b38a94f700864b Mon Sep 17 00:00:00 2001 From: Josh McKinney <joshka@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:16:43 +0000 Subject: [PATCH 90/98] Add dev container workspace Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace --- .devcontainer/Sonarr.code-workspace | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .devcontainer/Sonarr.code-workspace diff --git a/.devcontainer/Sonarr.code-workspace b/.devcontainer/Sonarr.code-workspace new file mode 100644 index 000000000..a46158e44 --- /dev/null +++ b/.devcontainer/Sonarr.code-workspace @@ -0,0 +1,13 @@ +// This file is used to open the backend and frontend in the same workspace, which is necessary as +// the frontend has vscode settings that are distinct from the backend +{ + "folders": [ + { + "path": ".." + }, + { + "path": "../frontend" + } + ], + "settings": {} +} From 6c232b062c5c11b76a2f205fcd949619e4346d16 Mon Sep 17 00:00:00 2001 From: Gauthier <mail@gauthierth.fr> Date: Tue, 16 Apr 2024 05:24:05 +0200 Subject: [PATCH 91/98] New: Multi Language selection per indexer Closes #2854 --- .../IndexerTests/TestIndexerSettings.cs | 3 +++ .../BroadcastheNet/BroadcastheNetSettings.cs | 7 +++++++ .../Indexers/Fanzub/FanzubSettings.cs | 7 +++++++ .../Indexers/FileList/FileListSettings.cs | 5 +++++ .../Indexers/HDBits/HDBitsSettings.cs | 5 +++++ src/NzbDrone.Core/Indexers/IIndexerSettings.cs | 5 ++++- .../Indexers/IPTorrents/IPTorrentsSettings.cs | 7 +++++++ src/NzbDrone.Core/Indexers/IndexerBase.cs | 12 ++++++++++++ .../Indexers/Newznab/NewznabSettings.cs | 8 +++++++- .../Indexers/Nyaa/NyaaSettings.cs | 7 +++++++ .../TorrentRss/TorrentRssIndexerSettings.cs | 7 +++++++ .../Torrentleech/TorrentleechSettings.cs | 7 +++++++ .../Indexers/Torznab/TorznabSettings.cs | 6 +++--- .../Languages/RealLanguageFieldConverter.cs | 18 ++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 15 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs index 0ee1716fa..948867108 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexerSettings.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Indexers; using NzbDrone.Core.Validation; @@ -12,5 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests } public string BaseUrl { get; set; } + + public IEnumerable<int> MultiLanguages { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index 7f4491a2b..e424a46f8 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.BroadcastheNet @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet { BaseUrl = "https://api.broadcasthe.net/"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -40,6 +44,9 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index 51ff19fa1..57abc672e 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Fanzub @@ -19,6 +22,7 @@ namespace NzbDrone.Core.Indexers.Fanzub public FanzubSettings() { BaseUrl = "http://fanzub.com/rss/"; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl", HelpText = "IndexerSettingsRssUrlHelpText")] @@ -28,6 +32,9 @@ namespace NzbDrone.Core.Indexers.Fanzub [FieldDefinition(1, Label = "IndexerSettingsAnimeStandardFormatSearch", Type = FieldType.Checkbox, HelpText = "IndexerSettingsAnimeStandardFormatSearchHelpText")] public bool AnimeStandardFormatSearch { get; set; } + [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index 2ef02c7de..13846a25f 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.FileList @@ -35,6 +36,7 @@ namespace NzbDrone.Core.Indexers.FileList }; AnimeCategories = Array.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)] @@ -43,6 +45,9 @@ namespace NzbDrone.Core.Indexers.FileList [FieldDefinition(1, Label = "IndexerSettingsPasskey", Privacy = PrivacyLevel.ApiKey)] public string Passkey { get; set; } + [FieldDefinition(2, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + [FieldDefinition(3, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] public string BaseUrl { get; set; } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index b5833789f..7d90cfa40 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.HDBits @@ -29,6 +30,7 @@ namespace NzbDrone.Core.Indexers.HDBits Categories = new[] { (int)HdBitsCategory.Tv, (int)HdBitsCategory.Documentary }; Codecs = Array.Empty<int>(); Mediums = Array.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] @@ -58,6 +60,9 @@ namespace NzbDrone.Core.Indexers.HDBits [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs index 87e7f03d2..5491b7c52 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerSettings.cs @@ -1,9 +1,12 @@ -using NzbDrone.Core.ThingiProvider; +using System.Collections.Generic; +using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers { public interface IIndexerSettings : IProviderConfig { string BaseUrl { get; set; } + + IEnumerable<int> MultiLanguages { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index 5c1271459..841c98ebf 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -1,7 +1,10 @@ +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.IPTorrents @@ -29,6 +32,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents public IPTorrentsSettings() { MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerIPTorrentsSettingsFeedUrl", HelpText = "IndexerIPTorrentsSettingsFeedUrlHelpText")] @@ -43,6 +47,9 @@ namespace NzbDrone.Core.Indexers.IPTorrents [FieldDefinition(3, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(4, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index dbb9916c0..4696bea3c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation.Results; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Languages; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -17,6 +20,8 @@ namespace NzbDrone.Core.Indexers public abstract class IndexerBase<TSettings> : IIndexer where TSettings : IIndexerSettings, new() { + private static readonly Regex MultiRegex = new (@"[_. ](?<multi>multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + protected readonly IIndexerStatusService _indexerStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; @@ -84,9 +89,16 @@ namespace NzbDrone.Core.Indexers protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases) { var result = releases.DistinctBy(v => v.Guid).ToList(); + var settings = Definition.Settings as IIndexerSettings; result.ForEach(c => { + // Use multi languages from setting if ReleaseInfo languages is empty + if (c.Languages.Empty() && MultiRegex.IsMatch(c.Title) && settings.MultiLanguages.Any()) + { + c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + } + c.IndexerId = Definition.Id; c.Indexer = Definition.Name; c.DownloadProtocol = Protocol; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index a38229560..b329140ea 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Newznab @@ -55,6 +57,7 @@ namespace NzbDrone.Core.Indexers.Newznab ApiPath = "/api"; Categories = new[] { 5030, 5040 }; AnimeCategories = Enumerable.Empty<int>(); + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "URL")] @@ -79,7 +82,10 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(6, Label = "IndexerSettingsAdditionalParameters", HelpText = "IndexerSettingsAdditionalNewznabParametersHelpText", Advanced = true)] public string AdditionalParameters { get; set; } - // Field 7 is used by TorznabSettings MinimumSeeders + [FieldDefinition(7, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + + // Field 8 is used by TorznabSettings MinimumSeeders // If you need to add another field here, update TorznabSettings as well and this comment public virtual NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 6983b2d67..516c34604 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Nyaa @@ -25,6 +28,7 @@ namespace NzbDrone.Core.Indexers.Nyaa BaseUrl = ""; AdditionalParameters = "&cats=1_0&filter=1"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -45,6 +49,9 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 898442bf7..5b3d4f3ef 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.TorrentRss @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss BaseUrl = string.Empty; AllowZeroSize = false; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsRssUrl")] @@ -43,6 +47,9 @@ namespace NzbDrone.Core.Indexers.TorrentRss [FieldDefinition(5, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(6, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index d999d84ba..47713230d 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Indexers.Torrentleech @@ -23,6 +26,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech { BaseUrl = "http://rss.torrentleech.org"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + MultiLanguages = Array.Empty<int>(); } [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] @@ -40,6 +44,9 @@ namespace NzbDrone.Core.Indexers.Torrentleech [FieldDefinition(4, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + [FieldDefinition(5, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable<int> MultiLanguages { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 6ed534bb2..6a84b59cd 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -49,13 +49,13 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(7, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] + [FieldDefinition(8, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(8)] + [FieldDefinition(9)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); - [FieldDefinition(9, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + [FieldDefinition(10, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs b/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs new file mode 100644 index 000000000..daca95472 --- /dev/null +++ b/src/NzbDrone.Core/Languages/RealLanguageFieldConverter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Languages +{ + public class RealLanguageFieldConverter : ISelectOptionsConverter + { + public List<SelectOption> GetSelectOptions() + { + return Language.All + .Where(l => l != Language.Unknown) + .OrderBy(l => l.Id > 0).ThenBy(l => l.Name) + .ToList() + .ConvertAll(v => new SelectOption { Value = v.Id, Name = v.Name }); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f6b312928..78b8557e7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -978,6 +978,8 @@ "IndexerSettingsPasskey": "Passkey", "IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", + "IndexerSettingsMultiLanguageRelease": "Multi Languages", + "IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?", "IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", From d71c619f1a22825ca02f458c9217d9b32601d4be Mon Sep 17 00:00:00 2001 From: Uruk <uruknarb20@gmail.com> Date: Sun, 14 Apr 2024 12:03:16 +0200 Subject: [PATCH 92/98] Update CI dependencies --- .github/workflows/build.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/lock.yml | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 736d5d8ee..6cddbc438 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: steps: - name: Notify - uses: tsickert/discord-webhook@v5.3.0 + uses: tsickert/discord-webhook@v6.0.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} username: 'GitHub Actions' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 857cfb4a7..ab2292824 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 0435b1c71..03ec90954 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,13 +9,13 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '90' - issue-exclude-created-before: '' - issue-exclude-labels: 'one-day-maybe' - issue-lock-labels: '' - issue-lock-comment: '' + issue-inactive-days: '90' + exclude-issue-created-before: '' + exclude-any-issue-labels: 'one-day-maybe' + add-issue-labels: '' + issue-comment: '' issue-lock-reason: 'resolved' process-only: '' From 016c4b353b64a8ea3c6e1d6e8e7b4cf71901d011 Mon Sep 17 00:00:00 2001 From: Gauvino <68083474+Gauvino@users.noreply.github.com> Date: Tue, 16 Apr 2024 05:25:13 +0200 Subject: [PATCH 93/98] Add merge conflict labeler --- .github/workflows/conflict_labeler.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/conflict_labeler.yml diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml new file mode 100644 index 000000000..a19496985 --- /dev/null +++ b/.github/workflows/conflict_labeler.yml @@ -0,0 +1,24 @@ +name: Merge Conflict Labeler + +on: + push: + branches: + - develop + pull_request_target: + issue_comment: + +permissions: {} + +jobs: + label: + name: Labeling + runs-on: ubuntu-latest + if: ${{ github.repository == 'Sonarr/Sonarr' }} + steps: + - name: Apply label + uses: eps1lon/actions-label-merge-conflict@v3 + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} + with: + dirtyLabel: 'merge-conflict' + repoToken: "${{ secrets.GITHUB_TOKEN }}" + \ No newline at end of file From e9662544621b2d1fb133ff9d96d0eb20b8198725 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:43:52 +0300 Subject: [PATCH 94/98] Fixed: Re-testing edited providers will forcibly test them --- .../Creators/createTestProviderHandler.js | 24 +++++++++++++++++-- src/Sonarr.Api.V3/ProviderControllerBase.cs | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js index ca26883fb..e35157dbd 100644 --- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -1,8 +1,11 @@ +import $ from 'jquery'; +import _ from 'lodash'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getProviderState from 'Utilities/State/getProviderState'; import { set } from '../baseActions'; const abortCurrentRequests = {}; +let lastTestData = null; export function createCancelTestProviderHandler(section) { return function(getState, payload, dispatch) { @@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) { return function(getState, payload, dispatch) { dispatch(set({ section, isTesting: true })); - const testData = getProviderState(payload, getState, section); + const { + queryParams = {}, + ...otherPayload + } = payload; + + const testData = getProviderState({ ...otherPayload }, getState, section); + const params = { ...queryParams }; + + // If the user is re-testing the same provider without changes + // force it to be tested. + + if (_.isEqual(testData, lastTestData)) { + params.forceTest = true; + } + + lastTestData = testData; const ajaxOptions = { - url: `${url}/test`, + url: `${url}/test?${$.param(params, true)}`, method: 'POST', contentType: 'application/json', dataType: 'json', @@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) { abortCurrentRequests[section] = abortRequest; request.done((data) => { + lastTestData = null; + dispatch(set({ section, isTesting: false, diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index ca1082609..2622b9b02 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -205,10 +205,10 @@ namespace Sonarr.Api.V3 [SkipValidation(true, false)] [HttpPost("test")] [Consumes("application/json")] - public object Test([FromBody] TProviderResource providerResource) + public object Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false) { var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; - var providerDefinition = GetDefinition(providerResource, existingDefinition, true, true, true); + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true); Test(providerDefinition, true); From f9b013a8bfa3ea65590e4a3c34f31b2c847daeaf Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 17:12:26 -0700 Subject: [PATCH 95/98] New: Parse releases with multiple Ukranian audio tracks Closes #6714 --- .../ParserTests/LanguageParserFixture.cs | 20 +++++++++++++++++++ src/NzbDrone.Core/Parser/LanguageParser.cs | 7 ++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index dce7fafc6..a7a363c9f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -429,6 +429,26 @@ namespace NzbDrone.Core.Test.ParserTests result.Languages.Should().Contain(Language.English); } + [TestCase("Остання серія (Сезон 1) / The Last Series (Season 1) (2024) WEB-DLRip-AVC 2xUkr/Eng | Sub Ukr/Eng")] + [TestCase("Справжня серія (Сезон 1-3) / True Series (Season 1-3) (2014-2019) BDRip-AVC 3xUkr/Eng | Ukr/Eng")] + [TestCase("Серія (Сезон 1-3) / The Series (Seasons 1-3) (2019-2022) BDRip-AVC 4xUkr/Eng | Sub 2xUkr/Eng")] + public void should_parse_english_and_ukranian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Languages.Count.Should().Be(2); + result.Languages.Should().Contain(Language.Ukrainian); + result.Languages.Should().Contain(Language.English); + } + + [TestCase("Серія (Сезон 1, серії 01-26 із 51) / Seri (Season 1, episodes 01-26) (2018) WEBRip-AVC 2Ukr/Tur")] + public void should_parse_turkish_and_ukranian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Languages.Count.Should().Be(2); + result.Languages.Should().Contain(Language.Ukrainian); + result.Languages.Should().Contain(Language.Turkish); + } + [TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")] diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 4071539c0..1548e4f82 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; - private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:ukr)\b)|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b))(?:(?i)(?![\W|_|^]SUB))", @@ -470,6 +470,11 @@ namespace NzbDrone.Core.Parser { languages.Add(Language.Latvian); } + + if (match.Groups["turkish"].Success) + { + languages.Add(Language.Turkish); + } } return languages; From ef6cc7fa3aa0c34b3b830fdf22dc3015a5039d4d Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Tue, 16 Apr 2024 03:27:46 +0000 Subject: [PATCH 96/98] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 14444bdba..4492f1d6a 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -1654,6 +1654,16 @@ "tags": [ "DownloadClient" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -2897,6 +2907,16 @@ "tags": [ "ImportList" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -3487,6 +3507,16 @@ "tags": [ "Indexer" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -4470,6 +4500,16 @@ "tags": [ "Metadata" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -5032,6 +5072,16 @@ "tags": [ "Notification" ], + "parameters": [ + { + "name": "forceTest", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { From cf6748a80ce7039eef2612d9f7720f5f391c4523 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 20:40:39 -0700 Subject: [PATCH 97/98] Fix merge conflict labeling --- .github/workflows/conflict_labeler.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml index a19496985..e9afb71a3 100644 --- a/.github/workflows/conflict_labeler.yml +++ b/.github/workflows/conflict_labeler.yml @@ -5,20 +5,22 @@ on: branches: - develop pull_request_target: - issue_comment: - -permissions: {} + branches: + - develop + types: [synchronize] jobs: label: name: Labeling runs-on: ubuntu-latest if: ${{ github.repository == 'Sonarr/Sonarr' }} + permissions: + contents: read + pull-requests: write steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@v3 - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: dirtyLabel: 'merge-conflict' - repoToken: "${{ secrets.GITHUB_TOKEN }}" + repoToken: '${{ secrets.GITHUB_TOKEN }}' \ No newline at end of file From b81c3ee4a8114f4271a517425d5ac3b81e4efeaa Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 15 Apr 2024 21:13:53 -0700 Subject: [PATCH 98/98] Fix labeling config --- .github/labeler.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 3b42128d4..fdd66d11a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,17 +1,23 @@ 'connection': - - src/NzbDrone.Core/Notifications/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Notifications/**/* 'db-migration': - - src/NzbDrone.Core/Datastore/Migration/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Datastore/Migration/* 'download-client': - - src/NzbDrone.Core/Download/Clients/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Download/Clients/**/* 'indexer': - - src/NzbDrone.Core/Indexers/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Indexers/**/* 'parsing': - - src/NzbDrone.Core/Parser/**/* + - changed-files: + - any-glob-to-any-file: src/NzbDrone.Core/Parser/**/* 'ui-only': - - all: ['frontend/**/*'] + - changed-files: + - any-glob-to-all-files: frontend/**/*