diff --git a/README.md b/README.md index 243c4129f..ef3c2ecea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Sonarr Sonarr +[![Translated](https://translate.servarr.com/widgets/servarr/-/sonarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/) +[![Backers on Open Collective](https://opencollective.com/Sonarr/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/Sonarr/sponsors/badge.svg)](#sponsors) +[![Mega Sponsors on Open Collective](https://opencollective.com/Sonarr/megasponsors/badge.svg)](#mega-sponsors) + Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available. ## Getting Started diff --git a/frontend/src/Activity/Blocklist/Blocklist.js b/frontend/src/Activity/Blocklist/Blocklist.js index b2af5a035..797aa5175 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.js +++ b/frontend/src/Activity/Blocklist/Blocklist.js @@ -36,6 +36,7 @@ class Blocklist extends Component { lastToggled: null, selectedState: {}, isConfirmRemoveModalOpen: false, + isConfirmClearModalOpen: false, items: props.items }; } @@ -90,6 +91,19 @@ class Blocklist extends Component { this.setState({ isConfirmRemoveModalOpen: false }); }; + onClearBlocklistPress = () => { + this.setState({ isConfirmClearModalOpen: true }); + }; + + onClearBlocklistConfirmed = () => { + this.props.onClearBlocklistPress(); + this.setState({ isConfirmClearModalOpen: false }); + }; + + onConfirmClearModalClose = () => { + this.setState({ isConfirmClearModalOpen: false }); + }; + // // Render @@ -103,7 +117,6 @@ class Blocklist extends Component { totalRecords, isRemoving, isClearingBlocklistExecuting, - onClearBlocklistPress, ...otherProps } = this.props; @@ -111,7 +124,8 @@ class Blocklist extends Component { allSelected, allUnselected, selectedState, - isConfirmRemoveModalOpen + isConfirmRemoveModalOpen, + isConfirmClearModalOpen } = this.state; const selectedIds = this.getSelectedIds(); @@ -131,8 +145,9 @@ class Blocklist extends Component { @@ -215,6 +230,16 @@ class Blocklist extends Component { onConfirm={this.onRemoveSelectedConfirmed} onCancel={this.onConfirmRemoveModalClose} /> + + ); } diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js index 874ec52c9..862d8707e 100644 --- a/frontend/src/Activity/History/Details/HistoryDetails.js +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -231,7 +231,7 @@ function HistoryDetails(props) { reasonMessage = translate('DeletedReasonManual'); break; case 'MissingFromDisk': - reasonMessage = translate('DeletedReasonMissingFromDisk'); + reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk'); break; case 'Upgrade': reasonMessage = translate('DeletedReasonUpgrade'); diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js index 522092725..e5cc31ecd 100644 --- a/frontend/src/Activity/History/History.js +++ b/frontend/src/Activity/History/History.js @@ -15,6 +15,7 @@ import TablePager from 'Components/Table/TablePager'; import { align, icons, kinds } from 'Helpers/Props'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import translate from 'Utilities/String/translate'; +import HistoryFilterModal from './HistoryFilterModal'; import HistoryRowConnector from './HistoryRowConnector'; class History extends Component { @@ -52,6 +53,7 @@ class History extends Component { columns, selectedFilterKey, filters, + customFilters, totalRecords, isEpisodesFetching, isEpisodesPopulated, @@ -92,7 +94,8 @@ class History extends Component { alignMenu={align.RIGHT} selectedFilterKey={selectedFilterKey} filters={filters} - customFilters={[]} + customFilters={customFilters} + filterModalConnectorComponent={HistoryFilterModal} onFilterSelect={onFilterSelect} /> @@ -163,8 +166,9 @@ History.propTypes = { error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.string.isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, totalRecords: PropTypes.number, isEpisodesFetching: PropTypes.bool.isRequired, isEpisodesPopulated: PropTypes.bool.isRequired, diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js index 74b7fdfb4..b407960bd 100644 --- a/frontend/src/Activity/History/HistoryConnector.js +++ b/frontend/src/Activity/History/HistoryConnector.js @@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage'; import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; import * as historyActions from 'Store/Actions/historyActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; @@ -15,11 +16,13 @@ function createMapStateToProps() { return createSelector( (state) => state.history, (state) => state.episodes, - (history, episodes) => { + createCustomFiltersSelector('history'), + (history, episodes, customFilters) => { return { isEpisodesFetching: episodes.isFetching, isEpisodesPopulated: episodes.isPopulated, episodesError: episodes.error, + customFilters, ...history }; } diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.js index 55b1351f1..cce30c6e5 100644 --- a/frontend/src/Activity/History/HistoryEventTypeCell.js +++ b/frontend/src/Activity/History/HistoryEventTypeCell.js @@ -39,19 +39,19 @@ function getIconKind(eventType) { function getTooltip(eventType, data) { switch (eventType) { case 'grabbed': - return translate('GrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient }); + return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient }); case 'seriesFolderImported': return translate('SeriesFolderImportedTooltip'); case 'downloadFolderImported': return translate('EpisodeImportedTooltip'); case 'downloadFailed': - return translate('DownloadFailedTooltip'); + return translate('DownloadFailedEpisodeTooltip'); case 'episodeFileDeleted': return translate('EpisodeFileDeletedTooltip'); case 'episodeFileRenamed': return translate('EpisodeFileRenamedTooltip'); case 'downloadIgnored': - return translate('DownloadIgnoredTooltip'); + return translate('DownloadIgnoredEpisodeTooltip'); default: return translate('UnknownEventTooltip'); } diff --git a/frontend/src/Activity/History/HistoryFilterModal.tsx b/frontend/src/Activity/History/HistoryFilterModal.tsx new file mode 100644 index 000000000..f4ad2e57c --- /dev/null +++ b/frontend/src/Activity/History/HistoryFilterModal.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setHistoryFilter } from 'Store/Actions/historyActions'; + +function createHistorySelector() { + return createSelector( + (state: AppState) => state.history.items, + (queueItems) => { + return queueItems; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.history.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface HistoryFilterModalProps { + isOpen: boolean; +} + +export default function HistoryFilterModal(props: HistoryFilterModalProps) { + const sectionItems = useSelector(createHistorySelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'history'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + dispatch(setHistoryFilter(payload)); + }, + [dispatch] + ); + + return ( + + ); +} diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 0b5827ea9..633357b7e 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; @@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; +import QueueFilterModal from './QueueFilterModal'; import QueueOptionsConnector from './QueueOptionsConnector'; import QueueRowConnector from './QueueRowConnector'; import RemoveQueueItemsModal from './RemoveQueueItemsModal'; @@ -151,11 +153,16 @@ class Queue extends Component { isEpisodesPopulated, episodesError, columns, + selectedFilterKey, + filters, + customFilters, + count, totalRecords, isGrabbing, isRemoving, isRefreshMonitoredDownloadsExecuting, onRefreshPress, + onFilterSelect, ...otherProps } = this.props; @@ -218,6 +225,15 @@ class Queue extends Component { iconName={icons.TABLE} /> + + @@ -239,7 +255,11 @@ class Queue extends Component { { isAllPopulated && !hasError && !items.length ? - {translate('QueueIsEmpty')} + { + selectedFilterKey !== 'all' && count > 0 ? + translate('QueueFilterHasNoItems') : + translate('QueueIsEmpty') + } : null } @@ -323,13 +343,22 @@ Queue.propTypes = { isEpisodesPopulated: PropTypes.bool.isRequired, episodesError: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + count: PropTypes.number.isRequired, totalRecords: PropTypes.number, isGrabbing: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, onRefreshPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired, - onRemoveSelectedPress: PropTypes.func.isRequired + onRemoveSelectedPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired +}; + +Queue.defaultProps = { + count: 0 }; export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index f7d5d5152..178cb8e5f 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -7,6 +7,7 @@ import withCurrentPage from 'Components/withCurrentPage'; import { executeCommand } from 'Store/Actions/commandActions'; import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; import * as queueActions from 'Store/Actions/queueActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; @@ -18,12 +19,16 @@ function createMapStateToProps() { (state) => state.episodes, (state) => state.queue.options, (state) => state.queue.paged, + (state) => state.queue.status.item, + createCustomFiltersSelector('queue'), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), - (episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => { + (episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => { return { + count: options.includeUnknownSeriesItems ? status.totalCount : status.count, isEpisodesFetching: episodes.isFetching, isEpisodesPopulated: episodes.isPopulated, episodesError: episodes.error, + customFilters, isRefreshMonitoredDownloadsExecuting, ...options, ...queue @@ -122,6 +127,10 @@ class QueueConnector extends Component { this.props.setQueueSort({ sortKey }); }; + onFilterSelect = (selectedFilterKey) => { + this.props.setQueueFilter({ selectedFilterKey }); + }; + onTableOptionChange = (payload) => { this.props.setQueueTableOption(payload); @@ -156,6 +165,7 @@ class QueueConnector extends Component { onLastPagePress={this.onLastPagePress} onPageSelect={this.onPageSelect} onSortPress={this.onSortPress} + onFilterSelect={this.onFilterSelect} onTableOptionChange={this.onTableOptionChange} onRefreshPress={this.onRefreshPress} onGrabSelectedPress={this.onGrabSelectedPress} @@ -178,6 +188,7 @@ QueueConnector.propTypes = { gotoQueueLastPage: PropTypes.func.isRequired, gotoQueuePage: PropTypes.func.isRequired, setQueueSort: PropTypes.func.isRequired, + setQueueFilter: PropTypes.func.isRequired, setQueueTableOption: PropTypes.func.isRequired, clearQueue: PropTypes.func.isRequired, grabQueueItems: PropTypes.func.isRequired, diff --git a/frontend/src/Activity/Queue/QueueDetails.js b/frontend/src/Activity/Queue/QueueDetails.js index b9b610176..abc97b75c 100644 --- a/frontend/src/Activity/Queue/QueueDetails.js +++ b/frontend/src/Activity/Queue/QueueDetails.js @@ -81,4 +81,9 @@ QueueDetails.propTypes = { progressBar: PropTypes.node.isRequired }; +QueueDetails.defaultProps = { + trackedDownloadStatus: 'ok', + trackedDownloadState: 'downloading' +}; + export default QueueDetails; diff --git a/frontend/src/Activity/Queue/QueueFilterModal.tsx b/frontend/src/Activity/Queue/QueueFilterModal.tsx new file mode 100644 index 000000000..3fce6c166 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueFilterModal.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setQueueFilter } from 'Store/Actions/queueActions'; + +function createQueueSelector() { + return createSelector( + (state: AppState) => state.queue.paged.items, + (queueItems) => { + return queueItems; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.queue.paged.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface QueueFilterModalProps { + isOpen: boolean; +} + +export default function QueueFilterModal(props: QueueFilterModalProps) { + const sectionItems = useSelector(createQueueSelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'queue'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + dispatch(setQueueFilter(payload)); + }, + [dispatch] + ); + + return ( + + ); +} diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js index e5ebb2bf6..4e8b9658c 100644 --- a/frontend/src/Activity/Queue/QueueStatusCell.js +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import { tooltipPositions } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; import QueueStatus from './QueueStatus'; import styles from './QueueStatusCell.css'; @@ -41,8 +40,8 @@ QueueStatusCell.propTypes = { }; QueueStatusCell.defaultProps = { - trackedDownloadStatus: translate('Ok'), - trackedDownloadState: translate('Downloading') + trackedDownloadStatus: 'ok', + trackedDownloadState: 'downloading' }; export default QueueStatusCell; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js index d79ef665c..0cf7af855 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemModal.js +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.js @@ -120,7 +120,7 @@ class RemoveQueueItemModal extends Component { type={inputTypes.CHECK} name="blocklist" value={blocklist} - helpText={translate('BlocklistReleaseHelpText')} + helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')} onChange={this.onBlocklistChange} /> diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js index 6a212868c..18ea39aea 100644 --- a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -123,7 +123,7 @@ class RemoveQueueItemsModal extends Component { type={inputTypes.CHECK} name="blocklist" value={blocklist} - helpText={translate('BlocklistReleaseHelpText')} + helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')} onChange={this.onBlocklistChange} /> diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.js index ab1134b8c..b280b5a06 100644 --- a/frontend/src/Activity/Queue/TimeleftCell.js +++ b/frontend/src/Activity/Queue/TimeleftCell.js @@ -1,6 +1,9 @@ import PropTypes from 'prop-types'; import React from 'react'; +import Icon from 'Components/Icon'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatTime from 'Utilities/Date/formatTime'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; @@ -25,11 +28,13 @@ function TimeleftCell(props) { const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( - - - + + } + tooltip={translate('DelayingDownloadUntil', { date, time })} + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> ); } @@ -39,11 +44,13 @@ function TimeleftCell(props) { const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); return ( - - - + + } + tooltip={translate('RetryingDownloadOn', { date, time })} + kind={kinds.INVERSE} + position={tooltipPositions.TOP} + /> ); } diff --git a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js index 8c8dda91e..939da79d2 100644 --- a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js +++ b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js @@ -79,17 +79,17 @@ class ImportSeriesSelectFolder extends Component { !error && isPopulated &&
- {translate('LibraryImportHeader')} + {translate('LibraryImportSeriesHeader')}
{translate('LibraryImportTips')}
  • - +
  • - +
  • {translate('LibraryImportTipsDontUseDownloadsFolder')} diff --git a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js index 76bb2f340..1df231f4e 100644 --- a/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js +++ b/frontend/src/AddSeries/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js @@ -5,12 +5,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { addRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import ImportSeriesSelectFolder from './ImportSeriesSelectFolder'; function createMapStateToProps() { return createSelector( - (state) => state.rootFolders, + createRootFoldersSelector(), createSystemStatusSelector(), (rootFolders, systemStatus) => { return { diff --git a/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js b/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js new file mode 100644 index 000000000..c70ec0dec --- /dev/null +++ b/frontend/src/AddSeries/SeriesMonitorNewItemsOptionsPopoverContent.js @@ -0,0 +1,22 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import translate from 'Utilities/String/translate'; + +function SeriesMonitorNewItemsOptionsPopoverContent() { + return ( + + + + + + ); +} + +export default SeriesMonitorNewItemsOptionsPopoverContent; diff --git a/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js b/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js index c1fa934d5..21289fcb8 100644 --- a/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js +++ b/frontend/src/AddSeries/SeriesMonitoringOptionsPopoverContent.js @@ -26,29 +26,39 @@ function SeriesMonitoringOptionsPopoverContent() { data={translate('MonitorExistingEpisodesDescription')} /> + + + + ); diff --git a/frontend/src/AddSeries/SeriesTypePopoverContent.js b/frontend/src/AddSeries/SeriesTypePopoverContent.js index c71dcbb4d..9771bd8db 100644 --- a/frontend/src/AddSeries/SeriesTypePopoverContent.js +++ b/frontend/src/AddSeries/SeriesTypePopoverContent.js @@ -8,17 +8,17 @@ function SeriesTypePopoverContent() { ); diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js index 1f81e1660..8cce1bc16 100644 --- a/frontend/src/App/AppUpdatedModalContent.js +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) { return ( - {translate('AppUpdated', { appName: 'Sonarr' })} + {translate('AppUpdated')}
    - +
    { diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.js index 3497d8345..5c08f491f 100644 --- a/frontend/src/App/ConnectionLostModal.js +++ b/frontend/src/App/ConnectionLostModal.js @@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
    - {translate('ConnectionLostToBackend', { appName: 'Sonarr' })} + {translate('ConnectionLostToBackend')}
    - {translate('ConnectionLostReconnect', { appName: 'Sonarr' })} + {translate('ConnectionLostReconnect')}
    diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index d511963fc..cabc39b1c 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,4 +1,5 @@ import SortDirection from 'Helpers/Props/SortDirection'; +import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -20,6 +21,10 @@ export interface PagedAppSectionState { pageSize: number; } +export interface AppSectionFilterState { + filterBuilderProps: FilterBuilderProp[]; +} + export interface AppSectionSchemaState { isSchemaFetching: boolean; isSchemaPopulated: boolean; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 572a5a11e..fcf1833ee 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -3,6 +3,7 @@ import CalendarAppState from './CalendarAppState'; import CommandAppState from './CommandAppState'; import EpisodeFilesAppState from './EpisodeFilesAppState'; import EpisodesAppState from './EpisodesAppState'; +import HistoryAppState from './HistoryAppState'; import ParseAppState from './ParseAppState'; import QueueAppState from './QueueAppState'; import RootFolderAppState from './RootFolderAppState'; @@ -48,6 +49,7 @@ interface AppState { commands: CommandAppState; episodeFiles: EpisodeFilesAppState; episodesSelection: EpisodesAppState; + history: HistoryAppState; interactiveImport: InteractiveImportAppState; parse: ParseAppState; queue: QueueAppState; diff --git a/frontend/src/App/State/CalendarAppState.ts b/frontend/src/App/State/CalendarAppState.ts index 304068528..de6a523b3 100644 --- a/frontend/src/App/State/CalendarAppState.ts +++ b/frontend/src/App/State/CalendarAppState.ts @@ -1,9 +1,10 @@ -import AppSectionState from 'App/State/AppSectionState'; +import AppSectionState, { + AppSectionFilterState, +} from 'App/State/AppSectionState'; import Episode from 'Episode/Episode'; -import { FilterBuilderProp } from './AppState'; -interface CalendarAppState extends AppSectionState { - filterBuilderProps: FilterBuilderProp[]; -} +interface CalendarAppState + extends AppSectionState, + AppSectionFilterState {} export default CalendarAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts new file mode 100644 index 000000000..e368ff86e --- /dev/null +++ b/frontend/src/App/State/HistoryAppState.ts @@ -0,0 +1,10 @@ +import AppSectionState, { + AppSectionFilterState, +} from 'App/State/AppSectionState'; +import History from 'typings/History'; + +interface HistoryAppState + extends AppSectionState, + AppSectionFilterState {} + +export default HistoryAppState; diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts index 05fc5a59a..05d74acac 100644 --- a/frontend/src/App/State/QueueAppState.ts +++ b/frontend/src/App/State/QueueAppState.ts @@ -1,43 +1,17 @@ -import ModelBase from 'App/ModelBase'; -import Language from 'Language/Language'; -import { QualityModel } from 'Quality/Quality'; -import CustomFormat from 'typings/CustomFormat'; -import AppSectionState, { AppSectionItemState, Error } from './AppSectionState'; - -export interface StatusMessage { - title: string; - messages: string[]; -} - -export interface Queue extends ModelBase { - languages: Language[]; - quality: QualityModel; - customFormats: CustomFormat[]; - size: number; - title: string; - sizeleft: number; - timeleft: string; - estimatedCompletionTime: string; - status: string; - trackedDownloadStatus: string; - trackedDownloadState: string; - statusMessages: StatusMessage[]; - errorMessage: string; - downloadId: string; - protocol: string; - downloadClient: string; - outputPath: string; - episodeHasFile: boolean; - seriesId?: number; - episodeId?: number; - seasonNumber?: number; -} +import Queue from 'typings/Queue'; +import AppSectionState, { + AppSectionFilterState, + AppSectionItemState, + Error, +} from './AppSectionState'; export interface QueueDetailsAppState extends AppSectionState { params: unknown; } -export interface QueuePagedAppState extends AppSectionState { +export interface QueuePagedAppState + extends AppSectionState, + AppSectionFilterState { isGrabbing: boolean; grabError: Error; isRemoving: boolean; diff --git a/frontend/src/Calendar/CalendarFilterModal.tsx b/frontend/src/Calendar/CalendarFilterModal.tsx index c09f73743..e26b2928b 100644 --- a/frontend/src/Calendar/CalendarFilterModal.tsx +++ b/frontend/src/Calendar/CalendarFilterModal.tsx @@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() { ); } -interface SeriesIndexFilterModalProps { +interface CalendarFilterModalProps { isOpen: boolean; } -export default function CalendarFilterModal( - props: SeriesIndexFilterModalProps -) { +export default function CalendarFilterModal(props: CalendarFilterModalProps) { const sectionItems = useSelector(createCalendarSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const customFilterType = 'calendar'; diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js index 546917b4a..5fc84234f 100644 --- a/frontend/src/Calendar/Legend/Legend.js +++ b/frontend/src/Calendar/Legend/Legend.js @@ -25,7 +25,7 @@ function Legend(props) { name="Finale" icon={icons.INFO} kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING} - tooltip={translate('CalendarLegendFinaleTooltip')} + tooltip={translate('CalendarLegendSeriesFinaleTooltip')} /> ); } @@ -58,7 +58,7 @@ function Legend(props) {
    {iconsToShow[0]} diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js index f7a904e21..eb64cb207 100644 --- a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -116,7 +116,7 @@ class CalendarLinkModalContent extends Component { return ( - {translate('CalendarFeed', { appName: 'Sonarr' })} + {translate('CalendarFeed')} @@ -128,7 +128,7 @@ class CalendarLinkModalContent extends Component { type={inputTypes.CHECK} name="unmonitored" value={unmonitored} - helpText={translate('ICalIncludeUnmonitoredHelpText')} + helpText={translate('ICalIncludeUnmonitoredEpisodesHelpText')} onChange={this.onInputChange} /> @@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component { type={inputTypes.TAG} name="tags" value={tags} - helpText={translate('ICalTagsHelpText')} + helpText={translate('ICalTagsSeriesHelpText')} onChange={this.onInputChange} /> diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css index b23415a76..786123fb7 100644 --- a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css @@ -1,9 +1,7 @@ -.description { - line-height: $lineHeight; -} - .description { margin-left: 0; + line-height: $lineHeight; + overflow-wrap: break-word; } @media (min-width: 768px) { diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index ee92b395d..01c24b460 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -6,10 +6,13 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; +import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; +import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; @@ -57,9 +60,15 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.DATE: return DateFilterBuilderRowValue; + case filterBuilderValueTypes.HISTORY_EVENT_TYPE: + return HistoryEventTypeFilterBuilderRowValue; + case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; + case filterBuilderValueTypes.LANGUAGE: + return LanguageFilterBuilderRowValue; + case filterBuilderValueTypes.PROTOCOL: return ProtocolFilterBuilderRowValue; @@ -69,6 +78,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.QUALITY_PROFILE: return QualityProfileFilterBuilderRowValueConnector; + case filterBuilderValueTypes.SERIES: + return SeriesFilterBuilderRowValue; + case filterBuilderValueTypes.SERIES_STATUS: return SeriesStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts new file mode 100644 index 000000000..5bf9e5785 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts @@ -0,0 +1,16 @@ +import { FilterBuilderProp } from 'App/State/AppState'; + +interface FilterBuilderRowOnChangeProps { + name: string; + value: unknown[]; +} + +interface FilterBuilderRowValueProps { + filterType?: string; + filterValue: string | number | object | string[] | number[] | object[]; + selectedFilterBuilderProp: FilterBuilderProp; + sectionItem: unknown[]; + onChange: (payload: FilterBuilderRowOnChangeProps) => void; +} + +export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx new file mode 100644 index 000000000..4ecddf646 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +const EVENT_TYPE_OPTIONS = [ + { + id: 1, + get name() { + return translate('Grabbed'); + }, + }, + { + id: 3, + get name() { + return translate('Imported'); + }, + }, + { + id: 4, + get name() { + return translate('Failed'); + }, + }, + { + id: 5, + get name() { + return translate('Deleted'); + }, + }, + { + id: 6, + get name() { + return translate('Renamed'); + }, + }, + { + id: 7, + get name() { + return translate('Ignored'); + }, + }, +]; + +function HistoryEventTypeFilterBuilderRowValue( + props: FilterBuilderRowValueProps +) { + return ; +} + +export default HistoryEventTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx new file mode 100644 index 000000000..e828fd848 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + const { items } = useSelector(createLanguagesSelector()); + + return ; +} + +export default LanguageFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx new file mode 100644 index 000000000..2eae79c80 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import Series from 'Series/Series'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import sortByName from 'Utilities/Array/sortByName'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + const allSeries: Series[] = useSelector(createAllSeriesSelector()); + + const tagList = allSeries + .map((series) => ({ id: series.id, name: series.title })) + .sort(sortByName); + + return ; +} + +export default SeriesFilterBuilderRowValue; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 6f3155f5b..49f08c90b 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -14,6 +14,7 @@ import FormInputHelpText from './FormInputHelpText'; import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput'; +import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput'; import NumberInput from './NumberInput'; import OAuthInputConnector from './OAuthInputConnector'; import PasswordInput from './PasswordInput'; @@ -49,6 +50,9 @@ function getComponent(type) { case inputTypes.MONITOR_EPISODES_SELECT: return MonitorEpisodesSelectInput; + case inputTypes.MONITOR_NEW_ITEMS_SELECT: + return MonitorNewItemsSelectInput; + case inputTypes.NUMBER: return NumberInput; diff --git a/frontend/src/Components/Form/FormLabel.css b/frontend/src/Components/Form/FormLabel.css index 074b6091d..54a4678e8 100644 --- a/frontend/src/Components/Form/FormLabel.css +++ b/frontend/src/Components/Form/FormLabel.css @@ -2,8 +2,10 @@ display: flex; justify-content: flex-end; margin-right: $formLabelRightMarginWidth; + padding-top: 8px; + min-height: 35px; + text-align: end; font-weight: bold; - line-height: 35px; } .hasError { diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js new file mode 100644 index 000000000..c704e5c1f --- /dev/null +++ b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions'; +import SelectInput from './SelectInput'; + +function MonitorNewItemsSelectInput(props) { + const { + includeNoChange, + includeMixed, + ...otherProps + } = props; + + const values = [...monitorNewItemsOptions]; + + if (includeNoChange) { + values.unshift({ + key: 'noChange', + value: 'No Change', + disabled: true + }); + } + + if (includeMixed) { + values.unshift({ + key: 'mixed', + value: '(Mixed)', + disabled: true + }); + } + + return ( + + ); +} + +MonitorNewItemsSelectInput.propTypes = { + includeNoChange: PropTypes.bool.isRequired, + includeMixed: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired +}; + +MonitorNewItemsSelectInput.defaultProps = { + includeNoChange: false, + includeMixed: false +}; + +export default MonitorNewItemsSelectInput; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index d7486245a..a184aa1ec 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.OAUTH; case 'rootFolder': return inputTypes.ROOT_FOLDER_SELECT; + case 'qualityProfile': + return inputTypes.QUALITY_PROFILE_SELECT; default: return inputTypes.TEXT; } diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index dc5930417..43581835f 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { addRootFolder } from 'Store/Actions/rootFolderActions'; +import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector'; import translate from 'Utilities/String/translate'; import RootFolderSelectInput from './RootFolderSelectInput'; @@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew'; function createMapStateToProps() { return createSelector( - (state) => state.rootFolders, + createRootFoldersSelector(), (state, { value }) => value, (state, { includeMissingValue }) => includeMissingValue, (state, { includeNoChange }) => includeNoChange, diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx index 72f0ed9ac..471d6592b 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx @@ -23,21 +23,21 @@ const seriesTypeOptions: ISeriesTypeOption[] = [ key: seriesTypes.STANDARD, value: 'Standard', get format() { - return translate('StandardTypeFormat', { format: 'S01E05' }); + return translate('StandardEpisodeTypeFormat', { format: 'S01E05' }); }, }, { key: seriesTypes.DAILY, value: 'Daily / Date', get format() { - return translate('DailyTypeFormat', { format: '2020-05-25' }); + return translate('DailyEpisodeTypeFormat', { format: '2020-05-25' }); }, }, { key: seriesTypes.ANIME, value: 'Anime / Absolute', get format() { - return translate('AnimeTypeFormat', { format: '005' }); + return translate('AnimeEpisodeTypeFormat', { format: '005' }); }, }, ]; diff --git a/frontend/src/Components/Modal/ModalContent.js b/frontend/src/Components/Modal/ModalContent.js index 8883bf2b9..1d3862a13 100644 --- a/frontend/src/Components/Modal/ModalContent.js +++ b/frontend/src/Components/Modal/ModalContent.js @@ -3,6 +3,7 @@ import React from 'react'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './ModalContent.css'; function ModalContent(props) { @@ -28,6 +29,7 @@ function ModalContent(props) { } diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js index 23d7d7f99..2af052015 100644 --- a/frontend/src/Components/Page/Header/PageHeader.js +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -81,6 +81,7 @@ class PageHeader extends Component { aria-label={translate('Donate')} to="https://sonarr.tv/donate.html" size={14} + title={translate('Donate')} /> diff --git a/frontend/src/Episode/EpisodeSearchCell.js b/frontend/src/Episode/EpisodeSearchCell.js index c09b3e65a..3ec76d365 100644 --- a/frontend/src/Episode/EpisodeSearchCell.js +++ b/frontend/src/Episode/EpisodeSearchCell.js @@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import EpisodeDetailsModal from './EpisodeDetailsModal'; import styles from './EpisodeSearchCell.css'; @@ -50,11 +51,13 @@ class EpisodeSearchCell extends Component { name={icons.SEARCH} isSpinning={isSearching} onPress={onSearchPress} + title={translate('AutomaticSearch')} /> - {translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })} + {translate('AuthenticationRequiredWarning')} { @@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) { type={inputTypes.SELECT} name="authenticationMethod" values={authenticationMethodOptions} - helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })} + helpText={translate('AuthenticationMethodHelpText')} helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined} helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication" onChange={onInputChange} @@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) { {...password} /> + + + {translate('PasswordConfirmation')} + + +
    : null } diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index a1f8f499d..1f4227779 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -2,10 +2,13 @@ export const BOOL = 'bool'; export const BYTES = 'bytes'; export const DATE = 'date'; export const DEFAULT = 'default'; +export const HISTORY_EVENT_TYPE = 'historyEventType'; export const INDEXER = 'indexer'; +export const LANGUAGE = 'language'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; +export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; export const SERIES_TYPES = 'seriesType'; export const TAG = 'tag'; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 575dc698a..126b45954 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -4,6 +4,7 @@ export const CHECK = 'check'; export const DEVICE = 'device'; export const KEY_VALUE_LIST = 'keyValueList'; export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect'; +export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const FLOAT = 'float'; export const NUMBER = 'number'; export const OAUTH = 'oauth'; @@ -31,6 +32,7 @@ export const all = [ DEVICE, KEY_VALUE_LIST, MONITOR_EPISODES_SELECT, + MONITOR_NEW_ITEMS_SELECT, FLOAT, NUMBER, OAUTH, diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index 598b64a70..c29e7ac56 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -69,8 +69,6 @@ interface SelectEpisodeModalContentProps { seasonNumber?: number; selectedDetails?: string; isAnime: boolean; - sortKey?: string; - sortDirection?: string; modalTitle: string; onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown; onModalClose(): unknown; @@ -86,8 +84,6 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { seasonNumber, selectedDetails, isAnime, - sortKey, - sortDirection, modalTitle, onEpisodesSelect, onModalClose, @@ -97,9 +93,8 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { const [selectState, setSelectState] = useSelectState(); const { allSelected, allUnselected, selectedState } = selectState; - const { isFetching, isPopulated, items, error } = useSelector( - episodesSelector() - ); + const { isFetching, isPopulated, items, error, sortKey, sortDirection } = + useSelector(episodesSelector()); const dispatch = useDispatch(); const filterEpisodeNumber = parseInt(filter); diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index b852f82c7..1961de02c 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -139,7 +139,7 @@ function InteractiveSearch(props) { { errorMessage ? - {translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })} + {translate('InteractiveSearchResultsSeriesFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })} : translate('EpisodeSearchResultsLoadError') } diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index 4d3e700e5..d95c383c0 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -309,7 +309,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) { isOpen={isConfirmGrabModalOpen} kind={kinds.WARNING} title={translate('GrabRelease')} - message={translate('GrabReleaseMessageText', { title })} + message={translate('GrabReleaseUnknownSeriesOrEpisodeMessageText', { + title, + })} confirmLabel={translate('Grab')} onConfirm={onGrabConfirm} onCancel={onGrabCancel} diff --git a/frontend/src/Series/Delete/DeleteSeriesModalContent.js b/frontend/src/Series/Delete/DeleteSeriesModalContent.js index 6a1e1effa..2af242499 100644 --- a/frontend/src/Series/Delete/DeleteSeriesModalContent.js +++ b/frontend/src/Series/Delete/DeleteSeriesModalContent.js @@ -89,7 +89,7 @@ class DeleteSeriesModalContent extends Component { type={inputTypes.CHECK} name="addImportListExclusion" value={addImportListExclusion} - helpText={translate('AddListExclusionHelpText')} + helpText={translate('AddListExclusionSeriesHelpText')} onChange={onDeleteOptionChange} /> diff --git a/frontend/src/Series/Details/SeriesDetails.css b/frontend/src/Series/Details/SeriesDetails.css index 61e6b976f..d7a26e4f8 100644 --- a/frontend/src/Series/Details/SeriesDetails.css +++ b/frontend/src/Series/Details/SeriesDetails.css @@ -156,6 +156,12 @@ .headerContent { padding: 15px; } + + .title { + font-weight: 300; + font-size: 30px; + line-height: 30px; + } } @media only screen and (max-width: $breakpointLarge) { diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 0686d9edd..8f6189cbe 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -45,11 +45,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize); const lineHeight = parseFloat(fonts.lineHeight); function getFanartUrl(images) { - const fanartImage = _.find(images, { coverType: 'fanart' }); - if (fanartImage) { - // Remove protocol - return fanartImage.url.replace(/^https?:/, ''); - } + return _.find(images, { coverType: 'fanart' })?.url; } function getExpandedState(newState) { @@ -194,7 +190,7 @@ class SeriesDetails extends Component { genres, tags, year, - previousAiring, + lastAired, isSaving, isRefreshing, isSearching, @@ -231,7 +227,7 @@ class SeriesDetails extends Component { } = this.state; const statusDetails = getSeriesStatusDetails(status); - const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(previousAiring)}` : `${year}-`; + const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(lastAired)}` : `${year}-`; let episodeFilesCountMessage = translate('SeriesDetailsNoEpisodeFiles'); @@ -715,6 +711,7 @@ SeriesDetails.propTypes = { genres: PropTypes.arrayOf(PropTypes.string).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired, year: PropTypes.number.isRequired, + lastAired: PropTypes.string, previousAiring: PropTypes.string, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.js b/frontend/src/Series/Details/SeriesDetailsSeason.js index 27c54a946..5605ad2d0 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.js +++ b/frontend/src/Series/Details/SeriesDetailsSeason.js @@ -210,12 +210,15 @@ class SeriesDetailsSeason extends Component { seasonNumber, items, columns, + sortKey, + sortDirection, statistics, isSaving, isExpanded, isSearching, seriesMonitored, isSmallScreen, + onSortPress, onTableOptionChange, onMonitorSeasonPress, onSearchPress @@ -447,6 +450,9 @@ class SeriesDetailsSeason extends Component { items.length ? @@ -530,6 +536,8 @@ SeriesDetailsSeason.propTypes = { seasonNumber: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string.isRequired, + sortDirection: PropTypes.oneOf(sortDirections.all), statistics: PropTypes.object.isRequired, isSaving: PropTypes.bool, isExpanded: PropTypes.bool, @@ -537,6 +545,7 @@ SeriesDetailsSeason.propTypes = { seriesMonitored: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired, onTableOptionChange: PropTypes.func.isRequired, + onSortPress: PropTypes.func.isRequired, onMonitorSeasonPress: PropTypes.func.isRequired, onExpandPress: PropTypes.func.isRequired, onMonitorEpisodePress: PropTypes.func.isRequired, diff --git a/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js b/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js index cdf3b30ea..2fa409765 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js +++ b/frontend/src/Series/Details/SeriesDetailsSeasonConnector.js @@ -4,8 +4,9 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import * as commandNames from 'Commands/commandNames'; import { executeCommand } from 'Store/Actions/commandActions'; -import { setEpisodesTableOption, toggleEpisodesMonitored } from 'Store/Actions/episodeActions'; +import { setEpisodesSort, setEpisodesTableOption, toggleEpisodesMonitored } from 'Store/Actions/episodeActions'; import { toggleSeasonMonitored } from 'Store/Actions/seriesActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; @@ -15,7 +16,7 @@ import SeriesDetailsSeason from './SeriesDetailsSeason'; function createMapStateToProps() { return createSelector( (state, { seasonNumber }) => seasonNumber, - (state) => state.episodes, + createClientSideCollectionSelector('episodes'), createSeriesSelector(), createCommandsSelector(), createDimensionsSelector(), @@ -27,11 +28,12 @@ function createMapStateToProps() { })); const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber); - const sortedEpisodes = episodesInSeason.sort((a, b) => b.episodeNumber - a.episodeNumber); return { - items: sortedEpisodes, + items: episodesInSeason, columns: episodes.columns, + sortKey: episodes.sortKey, + sortDirection: episodes.sortDirection, isSearching, seriesMonitored: series.monitored, path: series.path, @@ -45,6 +47,7 @@ const mapDispatchToProps = { toggleSeasonMonitored, toggleEpisodesMonitored, setEpisodesTableOption, + setEpisodesSort, executeCommand }; @@ -90,6 +93,13 @@ class SeriesDetailsSeasonConnector extends Component { }); }; + onSortPress = (sortKey, sortDirection) => { + this.props.setEpisodesSort({ + sortKey, + sortDirection + }); + }; + // // Render @@ -98,6 +108,7 @@ class SeriesDetailsSeasonConnector extends Component { + + + {translate('MonitorNewSeasons')} + + } + title={translate('MonitorNewSeasons')} + body={} + position={tooltipPositions.RIGHT} + /> + + + + + {translate('UseSeasonFolder')} diff --git a/frontend/src/Series/Edit/EditSeriesModalContentConnector.js b/frontend/src/Series/Edit/EditSeriesModalContentConnector.js index 0521f92df..b4a53685a 100644 --- a/frontend/src/Series/Edit/EditSeriesModalContentConnector.js +++ b/frontend/src/Series/Edit/EditSeriesModalContentConnector.js @@ -38,6 +38,7 @@ function createMapStateToProps() { const seriesSettings = _.pick(series, [ 'monitored', + 'monitorNewItems', 'seasonFolder', 'qualityProfileId', 'seriesType', diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx index 486b8f038..4c3c85555 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverviewInfo.tsx @@ -63,7 +63,7 @@ const rows = [ { name: 'qualityProfileId', showProp: 'showQualityProfile', - valueProp: 'qualityProfileId', + valueProp: 'qualityProfile', }, { name: 'previousAiring', diff --git a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx index b7f72116a..81e0f942d 100644 --- a/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx +++ b/frontend/src/Series/Index/Posters/Options/SeriesIndexPosterOptionsModalContent.tsx @@ -101,7 +101,7 @@ function SeriesIndexPosterOptionsModalContent( type={inputTypes.CHECK} name="showTitle" value={showTitle} - helpText={translate('ShowTitleHelpText')} + helpText={translate('ShowSeriesTitleHelpText')} onChange={onPosterOptionChange} /> diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index f66b28346..8904a24ce 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -98,7 +98,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) { type={inputTypes.CHECK} name="addImportListExclusion" value={addImportListExclusion} - helpText={translate('AddListExclusionHelpText')} + helpText={translate('AddListExclusionSeriesHelpText')} onChange={onDeleteOptionChange} /> diff --git a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx index 522d3906d..27b54f95b 100644 --- a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx @@ -14,6 +14,7 @@ import styles from './EditSeriesModalContent.css'; interface SavePayload { monitored?: boolean; + monitorNewItems?: string; qualityProfileId?: number; seriesType?: string; seasonFolder?: boolean; @@ -77,6 +78,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { const { seriesIds, onSavePress, onModalClose } = props; const [monitored, setMonitored] = useState(NO_CHANGE); + const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE); const [qualityProfileId, setQualityProfileId] = useState( NO_CHANGE ); @@ -95,6 +97,11 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { payload.monitored = monitored === 'monitored'; } + if (monitorNewItems !== NO_CHANGE) { + hasChanges = true; + payload.monitorNewItems = monitorNewItems; + } + if (qualityProfileId !== NO_CHANGE) { hasChanges = true; payload.qualityProfileId = qualityProfileId as number; @@ -124,6 +131,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { }, [ monitored, + monitorNewItems, qualityProfileId, seriesType, seasonFolder, @@ -139,6 +147,9 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { case 'monitored': setMonitored(value); break; + case 'monitorNewItems': + setMonitorNewItems(value); + break; case 'qualityProfileId': setQualityProfileId(value); break; @@ -199,6 +210,19 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) { /> + + {translate('MonitorNewItems')} + + + + {translate('QualityProfile')} diff --git a/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts b/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts index 66143ad2c..0b194161a 100644 --- a/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts +++ b/frontend/src/Series/Index/createSeriesQueueDetailsSelector.ts @@ -15,7 +15,10 @@ function createSeriesQueueDetailsSelector( (queueItems) => { return queueItems.reduce( (acc: SeriesQueueDetails, item) => { - if (item.seriesId !== seriesId) { + if ( + item.trackedDownloadState === 'imported' || + item.seriesId !== seriesId + ) { return acc; } diff --git a/frontend/src/Series/SeriesImage.js b/frontend/src/Series/SeriesImage.js index fddc158b8..b1bd738de 100644 --- a/frontend/src/Series/SeriesImage.js +++ b/frontend/src/Series/SeriesImage.js @@ -7,12 +7,10 @@ function findImage(images, coverType) { } function getUrl(image, coverType, size) { - if (image) { - // Remove protocol - let url = image.url.replace(/^https?:/, ''); - url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); + const imageUrl = image?.url; - return url; + if (imageUrl) { + return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); } } diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js index d18abcc32..efe105b20 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js @@ -152,7 +152,7 @@ class CustomFormat extends Component { isOpen={this.state.isDeleteCustomFormatModalOpen} kind={kinds.DANGER} title={translate('DeleteCustomFormat')} - message={translate('DeleteCustomFormatMessageText', [name])} + message={translate('DeleteCustomFormatMessageText', { customFormatName: name })} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteCustomFormat} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index 114061b05..52493adf5 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -147,7 +147,7 @@ class EditDownloadClientModalContent extends Component { diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js index 7de4febfd..5d599a8f4 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -61,7 +61,7 @@ function DownloadClientOptions(props) { isAdvanced={true} size={sizes.MEDIUM} > - {translate('RedownloadFailed')} + {translate('AutoRedownloadFailed')} + + { + settings.autoRedownloadFailed.value ? + + {translate('AutoRedownloadFailedFromInteractiveSearch')} + + + : + null + } diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js index 78c3c7e02..1a942a19e 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js @@ -54,7 +54,7 @@ class RemotePathMappings extends Component { > - +
    diff --git a/frontend/src/Settings/General/SecuritySettings.js b/frontend/src/Settings/General/SecuritySettings.js index 011b308d0..8e2597741 100644 --- a/frontend/src/Settings/General/SecuritySettings.js +++ b/frontend/src/Settings/General/SecuritySettings.js @@ -124,6 +124,7 @@ class SecuritySettings extends Component { authenticationRequired, username, password, + passwordConfirmation, apiKey, certificateValidation } = settings; @@ -139,8 +140,8 @@ class SecuritySettings extends Component { type={inputTypes.SELECT} name="authenticationMethod" values={authenticationMethodOptions} - helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })} - helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })} + helpText={translate('AuthenticationMethodHelpText')} + helpTextWarning={translate('AuthenticationRequiredWarning')} onChange={onInputChange} {...authenticationMethod} /> @@ -193,6 +194,21 @@ class SecuritySettings extends Component { null } + { + authenticationEnabled ? + + {translate('PasswordConfirmation')} + + + : + null + } + {translate('ApiKey')} diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 9e5cf0a6b..4558650c0 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -83,7 +83,7 @@ function UpdateSettings(props) { type={inputTypes.CHECK} name="updateAutomatically" helpText={translate('UpdateAutomaticallyHelpText')} - helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Sonarr' }) : undefined} + helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined} onChange={onInputChange} {...updateAutomatically} /> diff --git a/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js index 8a691b0bd..b5132db42 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/AddImportListModalContent.js @@ -56,7 +56,7 @@ class AddImportListModalContent extends Component {
    - {translate('SupportedLists')} + {translate('SupportedListsSeries')}
    {translate('SupportedListsMoreInfo')} diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index ec0780fff..c8020d975 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent'; +import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent'; import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent'; import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; @@ -46,9 +47,11 @@ function EditImportListModalContent(props) { implementationName, name, enableAutomaticAdd, + searchForMissingEpisodes, minRefreshInterval, shouldMonitor, rootFolderPath, + monitorNewItems, qualityProfileId, seriesType, seasonFolder, @@ -107,12 +110,24 @@ function EditImportListModalContent(props) { + + {translate('ImportListSearchForMissingEpisodes')} + + + + {translate('Monitor')} @@ -138,6 +153,31 @@ function EditImportListModalContent(props) { /> + + + {translate('MonitorNewSeasons')} + + } + title={translate('MonitorNewSeasons')} + body={} + position={tooltipPositions.RIGHT} + /> + + + + + {translate('RootFolder')} diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index d2eba8e48..4306aa2d9 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -200,7 +200,7 @@ function EditIndexerModalContent(props) { diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 1ceba5a69..e39ed837d 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -180,7 +180,7 @@ class MediaManagement extends Component { @@ -257,7 +257,7 @@ class MediaManagement extends Component { @@ -399,7 +399,7 @@ class MediaManagement extends Component { diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js index f99f42038..38cc33559 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) { { id === 1 ? - {translate('DefaultDelayProfile')} + {translate('DefaultDelayProfileSeries')} : @@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) { type={inputTypes.TAG} name="tags" {...tags} - helpText={translate('DelayProfileTagsHelpText')} + helpText={translate('DelayProfileSeriesTagsHelpText')} onChange={onInputChange} /> diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index 486aeec2f..1c129a9b3 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -203,7 +203,7 @@ class EditQualityProfileModalContent extends Component { name="cutoff" {...cutoff} values={qualities} - helpText={translate('UpgradeUntilHelpText')} + helpText={translate('UpgradeUntilEpisodeHelpText')} onChange={onCutoffChange} /> @@ -237,7 +237,7 @@ class EditQualityProfileModalContent extends Component { type={inputTypes.NUMBER} name="cutoffFormatScore" {...cutoffFormatScore} - helpText={translate('UpgradeUntilCustomFormatScoreHelpText')} + helpText={translate('UpgradeUntilCustomFormatScoreEpisodeHelpText')} onChange={onInputChange} /> @@ -281,7 +281,7 @@ class EditQualityProfileModalContent extends Component { className={styles.deleteButtonContainer} title={ isInUse ? - translate('QualityProfileInUse') : + translate('QualityProfileInUseSeriesListCollection') : undefined } > diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js index 025c8e48e..64d707b5f 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js @@ -126,7 +126,7 @@ function EditReleaseProfileModalContent(props) { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js index d7e9ed40c..76b7ca383 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js @@ -60,7 +60,7 @@ class QualityDefinitions extends Component {
    - {translate('QualityLimitsHelpText')} + {translate('QualityLimitsSeriesRuntimeHelpText')}
    diff --git a/frontend/src/Settings/Settings.js b/frontend/src/Settings/Settings.js index abc6d2fcf..2afbae783 100644 --- a/frontend/src/Settings/Settings.js +++ b/frontend/src/Settings/Settings.js @@ -110,7 +110,7 @@ function Settings() {
    - {translate('MetadataSettingsSummary')} + {translate('MetadataSettingsSeriesSummary')}
    - {translate('MetadataSourceSettingsSummary')} + {translate('MetadataSourceSettingsSeriesSummary')}
    { + return customFilter.type === section || customFilter.type === baseSection; + }); + const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data + data, + traditional: true }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index 0e0febea6..4fece9523 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -52,8 +52,6 @@ export const defaultState = { selectedFilterKey: 'monitored', - customFilters: [], - filters: [ { key: 'all', diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js index 628703752..a769913a0 100644 --- a/frontend/src/Store/Actions/episodeActions.js +++ b/frontend/src/Store/Actions/episodeActions.js @@ -40,32 +40,38 @@ export const defaultState = { { name: 'episodeNumber', label: '#', - isVisible: true + isVisible: true, + isSortable: true }, { name: 'title', label: () => translate('Title'), - isVisible: true + isVisible: true, + isSortable: true }, { name: 'path', label: () => translate('Path'), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'relativePath', label: () => translate('RelativePath'), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'airDateUtc', label: () => translate('AirDate'), - isVisible: true + isVisible: true, + isSortable: true }, { name: 'runtime', label: () => translate('Runtime'), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'languages', @@ -100,7 +106,8 @@ export const defaultState = { { name: 'size', label: () => translate('Size'), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'releaseGroup', @@ -119,7 +126,8 @@ export const defaultState = { name: icons.SCORE, title: () => translate('CustomFormatScore') }), - isVisible: false + isVisible: false, + isSortable: true }, { name: 'status', @@ -136,7 +144,9 @@ export const defaultState = { }; export const persistState = [ - 'episodes.columns' + 'episodes.columns', + 'episodes.sortDirection', + 'episodes.sortKey' ]; // diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 26a2bb8a9..3e773eca8 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -1,7 +1,7 @@ import React from 'react'; import { createAction } from 'redux-actions'; import Icon from 'Components/Icon'; -import { filterTypes, icons, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -185,6 +185,33 @@ export const defaultState = { } ] } + ], + + filterBuilderProps: [ + { + name: 'eventType', + label: () => translate('EventType'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE + }, + { + name: 'seriesIds', + label: () => translate('Series'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.SERIES + }, + { + name: 'quality', + label: () => translate('Quality'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.QUALITY + }, + { + name: 'languages', + label: () => translate('Languages'), + type: filterBuilderTypes.CONTAINS, + valueType: filterBuilderValueTypes.LANGUAGE + } ] }; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 28ad0f220..789fa7464 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -28,8 +28,8 @@ export const defaultState = { error: null, items: [], originalItems: [], - sortKey: 'quality', - sortDirection: sortDirections.DESCENDING, + sortKey: 'relativePath', + sortDirection: sortDirections.ASCENDING, recentFolders: [], importMode: 'chooseImportMode', sortPredicates: { diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 6c4ee38cc..6af498685 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -3,7 +3,7 @@ import React from 'react'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import Icon from 'Components/Icon'; -import { icons, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -144,7 +144,7 @@ export const defaultState = { name: 'size', label: () => translate('Size'), isSortable: true, - isVisibile: false + isVisible: false }, { name: 'outputPath', @@ -170,6 +170,43 @@ export const defaultState = { isVisible: true, isModifiable: false } + ], + + selectedFilterKey: 'all', + + filters: [ + { + key: 'all', + label: 'All', + filters: [] + } + ], + + filterBuilderProps: [ + { + name: 'seriesIds', + label: () => translate('Series'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.SERIES + }, + { + name: 'quality', + label: () => translate('Quality'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.QUALITY + }, + { + name: 'languages', + label: () => translate('Languages'), + type: filterBuilderTypes.CONTAINS, + valueType: filterBuilderValueTypes.LANGUAGE + }, + { + name: 'protocol', + label: () => translate('Protocol'), + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.PROTOCOL + } ] } }; @@ -179,7 +216,8 @@ export const persistState = [ 'queue.paged.pageSize', 'queue.paged.sortKey', 'queue.paged.sortDirection', - 'queue.paged.columns' + 'queue.paged.columns', + 'queue.paged.selectedFilterKey' ]; // @@ -204,6 +242,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage'; export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage'; export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage'; export const SET_QUEUE_SORT = 'queue/setQueueSort'; +export const SET_QUEUE_FILTER = 'queue/setQueueFilter'; export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption'; export const SET_QUEUE_OPTION = 'queue/setQueueOption'; export const CLEAR_QUEUE = 'queue/clearQueue'; @@ -228,6 +267,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE); export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE); export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE); export const setQueueSort = createThunk(SET_QUEUE_SORT); +export const setQueueFilter = createThunk(SET_QUEUE_FILTER); export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION); export const setQueueOption = createAction(SET_QUEUE_OPTION); export const clearQueue = createAction(CLEAR_QUEUE); @@ -279,7 +319,8 @@ export const actionHandlers = handleThunks({ [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE, [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE, - [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT + [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT, + [serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER }, fetchDataAugmenter ), diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index 971665152..828b95295 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -168,9 +168,10 @@ export const filterPredicates = { }, hasMissingSeason: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; const { seasons = [] } = item; - return seasons.some((season) => { + const hasMissingSeason = seasons.some((season) => { const { seasonNumber, statistics = {} @@ -189,6 +190,8 @@ export const filterPredicates = { episodeFileCount === 0 ); }); + + return predicate(hasMissingSeason, filterValue); } }; @@ -347,7 +350,13 @@ export const filterBuilderProps = [ { name: 'hasMissingSeason', label: () => translate('HasMissingSeason'), - type: filterBuilderTypes.EXACT + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'year', + label: () => translate('Year'), + type: filterBuilderTypes.NUMBER } ]; diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js index 31e1cf053..0a8a2e5a9 100644 --- a/frontend/src/System/Status/Health/Health.js +++ b/frontend/src/System/Status/Health/Health.js @@ -72,6 +72,7 @@ function getInternalLink(source) { function getTestLink(source, props) { switch (source) { case 'IndexerStatusCheck': + case 'IndexerLongTermStatusCheck': return ( { export default function translate( key: string, - tokens?: Record + tokens: Record = {} ) { const translation = translations[key] || key; - if (tokens) { - return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => - String(tokens[tokenMatch] ?? match) - ); - } + tokens.appName = 'Sonarr'; - return translation; + return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => + String(tokens[tokenMatch] ?? match) + ); } diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js index a03ea8b2e..29c925702 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -247,11 +247,11 @@ class CutoffUnmet extends Component {
    - {translate('SearchForCutoffUnmetConfirmationCount', { totalRecords })} + {translate('SearchForCutoffUnmetEpisodesConfirmationCount', { totalRecords })}
    {translate('MassSearchCancelWarning')} diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js index 4185e54e3..70054a73b 100644 --- a/frontend/src/Wanted/Missing/Missing.js +++ b/frontend/src/Wanted/Missing/Missing.js @@ -260,11 +260,11 @@ class Missing extends Component {
    - {translate('SearchForAllMissingConfirmationCount', { totalRecords })} + {translate('SearchForAllMissingEpisodesConfirmationCount', { totalRecords })}
    {translate('MassSearchCancelWarning')} diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts new file mode 100644 index 000000000..99fabe275 --- /dev/null +++ b/frontend/src/typings/History.ts @@ -0,0 +1,28 @@ +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from './CustomFormat'; + +export type HistoryEventType = + | 'grabbed' + | 'seriesFolderImported' + | 'downloadFolderImported' + | 'downloadFailed' + | 'episodeFileDeleted' + | 'episodeFileRenamed' + | 'downloadIgnored'; + +export default interface History { + episodeId: number; + seriesId: number; + sourceTitle: string; + languages: Language[]; + quality: QualityModel; + customFormats: CustomFormat[]; + customFormatScore: number; + qualityCutoffNotMet: boolean; + date: string; + downloadId: string; + eventType: HistoryEventType; + data: unknown; + id: number; +} diff --git a/frontend/src/typings/Queue.ts b/frontend/src/typings/Queue.ts new file mode 100644 index 000000000..855173306 --- /dev/null +++ b/frontend/src/typings/Queue.ts @@ -0,0 +1,46 @@ +import ModelBase from 'App/ModelBase'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import CustomFormat from 'typings/CustomFormat'; + +export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error'; + +export type QueueTrackedDownloadState = + | 'downloading' + | 'importPending' + | 'importing' + | 'imported' + | 'failedPending' + | 'failed' + | 'ignored'; + +export interface StatusMessage { + title: string; + messages: string[]; +} + +interface Queue extends ModelBase { + languages: Language[]; + quality: QualityModel; + customFormats: CustomFormat[]; + size: number; + title: string; + sizeleft: number; + timeleft: string; + estimatedCompletionTime: string; + status: string; + trackedDownloadStatus: QueueTrackedDownloadStatus; + trackedDownloadState: QueueTrackedDownloadState; + statusMessages: StatusMessage[]; + errorMessage: string; + downloadId: string; + protocol: string; + downloadClient: string; + outputPath: string; + episodeHasFile: boolean; + seriesId?: number; + episodeId?: number; + seasonNumber?: number; +} + +export default Queue; diff --git a/package.json b/package.json index c09c4e1b7..a3ad4b517 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@babel/preset-typescript": "7.22.11", "@types/classnames": "2.3.1", "@types/lodash": "4.14.194", + "@types/react-lazyload": "3.2.0", "@types/react-router-dom": "5.3.3", "@types/react-text-truncate": "0.14.1", "@types/react-window": "1.8.5", diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index d3c036327..6af8abb7b 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Localization; using NzbDrone.Test.Common; using Sonarr.Http.ClientSchema; @@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests [TestFixture] public class SchemaBuilderFixture : TestBase { + [SetUp] + public void Setup() + { + Mocker.GetMock() + .Setup(s => s.GetLocalizedString(It.IsAny(), It.IsAny>())) + .Returns>((s, d) => s); + + SchemaBuilder.Initialize(Mocker.Container); + } + [Test] public void should_return_field_for_every_property() { diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 388a96635..6b1ea4171 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -71,15 +71,15 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"[Info] MigrationController: *** Migrating Database=sonarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")] // Announce URLs (passkeys) Magnet & Tracker - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")] - [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")] - [TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")] - [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")] - [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")] - [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")] - [TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")] + [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")] + [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")] + [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")] + [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")] + [TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")] + [TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")] + [TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")] + [TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")] + [TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")] // Webhooks - Notifiarr [TestCase(@"https://xxx.yyy/api/v1/notification/sonarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")] diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 896eb3d91..280083e4c 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -171,7 +171,7 @@ namespace NzbDrone.Common.Extensions { if (text.IsNullOrWhiteSpace()) { - throw new ArgumentNullException("text"); + throw new ArgumentNullException(nameof(text)); } return text.IndexOfAny(Path.GetInvalidPathChars()) >= 0; diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index a9449efe7..5281d74fe 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Common.Http.Dispatchers } else { - data = responseMessage.Content.ReadAsByteArrayAsync(cts.Token).GetAwaiter().GetResult(); + data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token); } } catch (Exception ex) diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index d0150a7bc..12d027afa 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Common.Instrumentation new (@"\b(\w*)?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory - new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce"), + new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Path new (@"C:\\Users\\(?[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs index a07f6dca9..9831d1fea 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RawDiskSpecificationFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Indexers; @@ -72,15 +72,27 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); } - [TestCase("How the Earth Was Made S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")] - [TestCase("The Universe S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")] - [TestCase("HELL ON WHEELS S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")] - [TestCase("Game.of.Thrones.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")] - [TestCase("Game of Thrones S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")] + [TestCase("Series Title S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")] + [TestCase("Series Title S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")] + [TestCase("SERIES TITLE S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")] + [TestCase("Series.Title.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")] + [TestCase("Series Title S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")] + [TestCase("Series Title S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")] + [TestCase("Someone.the.Entertainer.Presents.S01.NTSC.3xDVD9.MPEG-2.DD2.0")] + [TestCase("Series.Title.S00.The.Christmas.Special.2011.PAL.DVD5.DD2.0")] + [TestCase("Series.of.Desire.2000.S1_D01.NTSC.DVD5")] public void should_return_false_if_matches_disc_format(string title) { _remoteEpisode.Release.Title = title; Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); } + + [TestCase("Series Title EP50 USLT NTSC DVDRemux DD2.0")] + [TestCase("Series.Title.S01.NTSC.DVDRip.DD2.0.x264-PLAiD")] + public void should_return_true_if_dvdrip(string title) + { + _remoteEpisode.Release.Title = title; + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 32e4a6335..804099e4a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -70,7 +71,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), - Mocker.Resolve()); + Mocker.Resolve(), + Mocker.Resolve()); } protected void VerifyIdentifiable(DownloadClientItem downloadClientItem) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs index 9cc35560f..bf609e48c 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs @@ -320,7 +320,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests var result = Subject.Test(); - result.Errors.First().ErrorMessage.Should().Be("Old Hadouken client with unsupported API, need 5.1 or higher"); + result.Errors.Count.Should().Be(1); } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index d2d206bd6..c11b1317a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Pneumatic; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -51,7 +52,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), - Mocker.Resolve()); + Mocker.Resolve(), + Mocker.Resolve()); _downloadClientItem = Builder .CreateNew().With(d => d.DownloadId = "_Droned.S01E01.Pilot.1080p.WEB-DL-DRONE_0") diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 559423f2f..3ae0edd84 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -426,7 +426,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 0.7, Eta = 8640000, - State = "stalledDL", + State = "pausedUP", Label = "", SavePath = @"C:\Torrents".AsOsAgnostic(), ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 2c955bd39..b81633b51 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -452,6 +452,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests result.OutputRootFolders.First().Should().Be(fullCategoryDir); } + [TestCase("0")] + [TestCase("15d")] + public void should_set_history_removes_completed_downloads_false(string historyRetention) + { + _config.Misc.history_retention = historyRetention; + + var downloadClientInfo = Subject.GetStatus(); + + downloadClientInfo.RemovesCompletedDownloads.Should().BeFalse(); + } + + [TestCase("-1")] + [TestCase("15")] + [TestCase("3")] + [TestCase("3d")] + public void should_set_history_removes_completed_downloads_true(string historyRetention) + { + _config.Misc.history_retention = historyRetention; + + var downloadClientInfo = Subject.GetStatus(); + + downloadClientInfo.RemovesCompletedDownloads.Should().BeTrue(); + } + [TestCase(@"Y:\nzbget\root", @"completed\downloads", @"vv", @"Y:\nzbget\root\completed\downloads", @"Y:\nzbget\root\completed\downloads\vv")] [TestCase(@"Y:\nzbget\root", @"completed", @"vv", @"Y:\nzbget\root\completed", @"Y:\nzbget\root\completed\vv")] [TestCase(@"/nzbget/root", @"completed/downloads", @"vv", @"/nzbget/root/completed/downloads", @"/nzbget/root/completed/downloads/vv")] @@ -519,6 +543,52 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests result.HasWarnings.Should().BeTrue(); } + [Test] + public void should_test_success_if_sorters_are_empty() + { + _config.Misc.enable_tv_sorting = false; + _config.Misc.tv_categories = null; + _config.Sorters = new List(); + + var result = new NzbDroneValidationResult(Subject.Test()); + + result.IsValid.Should().BeTrue(); + } + + [Test] + public void should_test_failed_if_sorter_is_enabled_for_non_tv_category() + { + _config.Misc.enable_tv_sorting = false; + _config.Misc.tv_categories = null; + _config.Sorters = Builder.CreateListOfSize(1) + .All() + .With(s => s.is_active = true) + .With(s => s.sort_cats = new List { "tv-custom" }) + .Build() + .ToList(); + + var result = new NzbDroneValidationResult(Subject.Test()); + + result.IsValid.Should().BeTrue(); + } + + [Test] + public void should_test_failed_if_sorter_is_enabled_for_tv_category() + { + _config.Misc.enable_tv_sorting = false; + _config.Misc.tv_categories = null; + _config.Sorters = Builder.CreateListOfSize(1) + .All() + .With(s => s.is_active = true) + .With(s => s.sort_cats = new List { "tv" }) + .Build() + .ToList(); + + var result = new NzbDroneValidationResult(Subject.Test()); + + result.IsValid.Should().BeFalse(); + } + [Test] public void should_test_success_if_tv_sorting_disabled() { diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheckFixture.cs new file mode 100644 index 000000000..abaad836f --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheckFixture.cs @@ -0,0 +1,77 @@ +using System; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Localization; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class DownloadClientRemovesCompletedDownloadsCheckFixture : CoreTest + { + private DownloadClientInfo _clientStatus; + private Mock _downloadClient; + + private static Exception[] DownloadClientExceptions = + { + new DownloadClientUnavailableException("error"), + new DownloadClientAuthenticationException("error"), + new DownloadClientException("error") + }; + + [SetUp] + public void Setup() + { + _clientStatus = new DownloadClientInfo + { + IsLocalhost = true, + SortingMode = null, + RemovesCompletedDownloads = true + }; + + _downloadClient = Mocker.GetMock(); + _downloadClient.Setup(s => s.Definition) + .Returns(new DownloadClientDefinition { Name = "Test" }); + + _downloadClient.Setup(s => s.GetStatus()) + .Returns(_clientStatus); + + Mocker.GetMock() + .Setup(s => s.GetDownloadClients(It.IsAny())) + .Returns(new IDownloadClient[] { _downloadClient.Object }); + + Mocker.GetMock() + .Setup(s => s.GetLocalizedString(It.IsAny())) + .Returns("Some Warning Message"); + } + + [Test] + public void should_return_warning_if_removing_completed_downloads_is_enabled() + { + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_ok_if_remove_completed_downloads_is_not_enabled() + { + _clientStatus.RemovesCompletedDownloads = false; + Subject.Check().ShouldBeOk(); + } + + [Test] + [TestCaseSource("DownloadClientExceptions")] + public void should_return_ok_if_client_throws_downloadclientexception(Exception ex) + { + _downloadClient.Setup(s => s.GetStatus()) + .Throws(ex); + + Subject.Check().ShouldBeOk(); + + ExceptionVerification.ExpectedErrors(0); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index a2b77ab7d..522d92f9d 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -568,5 +568,45 @@ namespace NzbDrone.Core.Test.IndexerSearchTests allCriteria.First().Should().BeOfType(); allCriteria.First().As().SeasonNumber.Should().Be(7); } + + [Test] + public async Task episode_search_should_use_all_available_numbering_from_services_and_xem() + { + WithEpisode(1, 12, 2, 3); + + Mocker.GetMock() + .Setup(s => s.FindByTvdbId(It.IsAny())) + .Returns(new List + { + new SceneMapping + { + TvdbId = _xemSeries.TvdbId, + SearchTerm = _xemSeries.Title, + ParseTerm = _xemSeries.Title, + FilterRegex = "(?i)-(BTN)$", + SeasonNumber = 1, + SceneSeasonNumber = 1, + SceneOrigin = "tvdb", + Type = "ServicesProvider" + } + }); + + var allCriteria = WatchForSearchCriteria(); + + await Subject.EpisodeSearch(_xemEpisodes.First(), false, false); + + Mocker.GetMock() + .Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once()); + + allCriteria.Should().HaveCount(2); + + allCriteria.First().Should().BeOfType(); + allCriteria.First().As().SeasonNumber.Should().Be(1); + allCriteria.First().As().EpisodeNumber.Should().Be(12); + + allCriteria.Last().Should().BeOfType(); + allCriteria.Last().As().SeasonNumber.Should().Be(2); + allCriteria.Last().As().EpisodeNumber.Should().Be(3); + } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs index 55ed8abfb..f836852e1 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerStatusServiceFixture.cs @@ -68,5 +68,16 @@ namespace NzbDrone.Core.Test.IndexerTests VerifyNoUpdate(); } + + [Test] + public void should_not_record_failure_for_unknown_provider() + { + Subject.RecordFailure(0); + + Mocker.GetMock() + .Verify(v => v.FindByProviderId(1), Times.Never); + + VerifyNoUpdate(); + } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs b/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs index 2b73b3b73..24fe564dd 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TestIndexer.cs @@ -2,6 +2,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Test.IndexerTests @@ -15,8 +16,8 @@ namespace NzbDrone.Core.Test.IndexerTests public int _supportedPageSize; public override int PageSize => _supportedPageSize; - public TestIndexer(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public TestIndexer(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TestTorrentRssIndexer.cs b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TestTorrentRssIndexer.cs index d969c5653..301da2da6 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TestTorrentRssIndexer.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorrentRssIndexerTests/TestTorrentRssIndexer.cs @@ -7,14 +7,15 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.TorrentRss; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests { public class TestTorrentRssIndexer : TorrentRssIndexer { - public TestTorrentRssIndexer(ITorrentRssParserFactory torrentRssParserFactory, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(torrentRssParserFactory, httpClient, indexerStatusService, configService, parsingService, logger) + public TestTorrentRssIndexer(ITorrentRssParserFactory torrentRssParserFactory, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(torrentRssParserFactory, httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs index 180292622..d43657c7f 100644 --- a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs @@ -30,29 +30,22 @@ namespace NzbDrone.Core.Test.Localization } [Test] - public void should_get_string_in_default_language_dictionary_if_no_lang_country_code_exists_and_string_exists() + public void should_get_string_in_french() { - var localizedString = Subject.GetLocalizedString("UiLanguage", "fr_fr"); + Mocker.GetMock().Setup(m => m.UILanguage).Returns((int)Language.French); - localizedString.Should().Be("UI Langue"); + var localizedString = Subject.GetLocalizedString("UiLanguage"); + + localizedString.Should().Be("Langue de l'interface utilisateur"); ExceptionVerification.ExpectedErrors(1); } [Test] - public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists() + public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists() { - var localizedString = Subject.GetLocalizedString("UiLanguage", "an"); - - localizedString.Should().Be("UI Language"); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists() - { - var localizedString = Subject.GetLocalizedString("UiLanguage", ""); + Mocker.GetMock().Setup(m => m.UILanguage).Returns(0); + var localizedString = Subject.GetLocalizedString("UiLanguage"); localizedString.Should().Be("UI Language"); } @@ -60,7 +53,7 @@ namespace NzbDrone.Core.Test.Localization [Test] public void should_return_argument_if_string_doesnt_exists() { - var localizedString = Subject.GetLocalizedString("badString", "en"); + var localizedString = Subject.GetLocalizedString("badString"); localizedString.Should().Be("badString"); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index cd9da2e05..d3b8c000f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -267,7 +267,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_series); Mocker.GetMock() - .Verify(v => v.GetFiles(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.GetFiles(It.IsAny(), It.IsAny()), Times.Exactly(2)); Mocker.GetMock() .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _series, false), Times.Once()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs index 9e276d909..3898118ef 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs @@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications GivenHistory(history); - Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue(); + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs index de3621fcd..72af3a7ca 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using FizzWare.NBuilder; using FluentAssertions; @@ -71,7 +72,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); @@ -97,7 +98,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); @@ -123,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(3)); @@ -146,7 +147,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenSuccessfulScan(); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo("media.mkv"), Times.Never()); @@ -173,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenSuccessfulScan(); GivenFailedScan(Path.Combine(_series.Path, "media2.mkv")); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(1)); @@ -203,7 +204,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new SeriesScannedEvent(_series)); + Subject.Handle(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() .Verify(v => v.GetMediaInfo(It.IsAny()), Times.Never()); diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs new file mode 100644 index 000000000..6b738f250 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs @@ -0,0 +1,86 @@ +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 CleanTitleTheFixture : CoreTest + { + private Series _series; + private Episode _episode; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder.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" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); + } + + [TestCase("The Mist", "Mist, The")] + [TestCase("A Place to Call Home", "Place to Call Home, A")] + [TestCase("An Adventure in Space and Time", "Adventure in Space and Time, An")] + [TestCase("The Flash (2010)", "Flash, The 2010")] + [TestCase("A League Of Their Own (AU)", "League Of Their Own, A AU")] + [TestCase("The Fixer (ZH) (2015)", "Fixer, The ZH 2015")] + [TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The Thai")] + [TestCase("The Amazing Race (Latin America)", "Amazing Race, The Latin America")] + [TestCase("The Rat Pack (A&E)", "Rat Pack, The AandE")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax I Almost Got Away With It, The 2016")] + public void should_get_expected_title_back(string title, string expected) + { + _series.Title = title; + _namingConfig.StandardEpisodeFormat = "{Series CleanTitleThe}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be(expected); + } + + [TestCase("A")] + [TestCase("Anne")] + [TestCase("Theodore")] + [TestCase("3%")] + public void should_not_change_title(string title) + { + _series.Title = title; + _namingConfig.StandardEpisodeFormat = "{Series CleanTitleThe}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be(title); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheWithoutYearFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheWithoutYearFixture.cs new file mode 100644 index 000000000..104f51867 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheWithoutYearFixture.cs @@ -0,0 +1,79 @@ +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 CleanTitleTheWithoutYearFixture : CoreTest + { + private Series _series; + private Episode _episode; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder.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" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); + } + + [TestCase("The Mist", 2018, "Mist, The")] + [TestCase("The Rat Pack (A&E)", 1999, "Rat Pack, The AandE")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", 2016, "Climax I Almost Got Away With It, The")] + [TestCase("A", 2017, "A")] + public void should_get_expected_title_back(string title, int year, string expected) + { + _series.Title = title; + _series.Year = year; + _namingConfig.StandardEpisodeFormat = "{Series CleanTitleTheWithoutYear}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be(expected); + } + + [Test] + public void should_not_include_0_for_year() + { + _series.Title = "The Alienist"; + _series.Year = 0; + _namingConfig.StandardEpisodeFormat = "{Series CleanTitleTheWithoutYear}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be("Alienist, The"); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheYearFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheYearFixture.cs new file mode 100644 index 000000000..bc06d4698 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheYearFixture.cs @@ -0,0 +1,81 @@ +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 CleanTitleTheYearFixture : CoreTest + { + private Series _series; + private Episode _episode; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder.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" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); + } + + [TestCase("The Mist", 2018, "Mist, The 2018")] + [TestCase("The Rat Pack (A&E)", 1999, "Rat Pack, The AandE 1999")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", 2016, "Climax I Almost Got Away With It, The 2016")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", 0, "Climax I Almost Got Away With It, The 2016")] + [TestCase("The Climax: I (Almost) Got Away With It", 0, "Climax I Almost Got Away With It, The")] + [TestCase("A", 2017, "A 2017")] + public void should_get_expected_title_back(string title, int year, string expected) + { + _series.Title = title; + _series.Year = year; + _namingConfig.StandardEpisodeFormat = "{Series CleanTitleTheYear}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be(expected); + } + + [Test] + public void should_not_include_0_for_year() + { + _series.Title = "The Alienist"; + _series.Year = 0; + _namingConfig.StandardEpisodeFormat = "{Series TitleTheYear}"; + + Subject.BuildFileName(new List { _episode }, _series, _episodeFile) + .Should().Be("Alienist, The"); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/SeriesTitleFirstCharacterFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/SeriesTitleFirstCharacterFixture.cs index 763b948a6..5b3d09d6a 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/SeriesTitleFirstCharacterFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/SeriesTitleFirstCharacterFixture.cs @@ -36,6 +36,13 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestCase("The Mist", "M\\The Mist")] [TestCase("A", "A\\A")] [TestCase("30 Rock", "3\\30 Rock")] + [TestCase("The '80s Greatest", "8\\The '80s Greatest")] + [TestCase("좀비버스", "좀\\좀비버스")] + [TestCase("¡Mucha Lucha!", "M\\¡Mucha Lucha!")] + [TestCase(".hack", "H\\hack")] + [TestCase("Ütopya", "U\\Ütopya")] + [TestCase("Æon Flux", "A\\Æon Flux")] + public void should_get_expected_folder_name_back(string title, string expected) { _series.Title = title; diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index e0cae8bf2..5dccae231 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -122,6 +122,12 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[SubsPlease] Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)] [TestCase("[CameEsp] Another Anime 100 - Another 100 Anime - 01 [720p][ESP-ENG][mkv]", "Another Anime 100 - Another 100 Anime", 1, 0, 0)] [TestCase("[SubsPlease] Another Anime 100 - Another 100 Anime - 01 (1080p) [4E6B4518].mkv", "Another Anime 100 - Another 100 Anime", 1, 0, 0)] + [TestCase("Some show 69. Blm (29.10.2023) 1080p WebDL #turkseed", "Some show", 69, 0, 0)] + [TestCase("Soap opera 01 BLM(01.11.2023) 1080p HDTV AC3 x264 TURG", "Soap opera", 1, 0, 0)] + [TestCase("Turkish show 60.Bolum (31.01.2023) 720p WebDL AAC H.264 - TURG", "Turkish show", 60, 0, 0)] + [TestCase("Different show 1. Bölüm (23.10.2023) 720p WebDL AAC H.264 - TURG", "Different show", 1, 0, 0)] + [TestCase("Dubbed show 79.BLM Sezon Finali(25.06.2023) 720p WEB-DL AAC2.0 H.264-TURG", "Dubbed show", 79, 0, 0)] + [TestCase("Exclusive BLM Documentary with no false positives EP03.1080p.AAC.x264", "Exclusive BLM Documentary with no false positives", 3, 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.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index 3fbd06f34..6af9a1733 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("13 Series Se.1 afl.2-3-4 [VTM]", "13 Series", 1, new[] { 2, 3, 4 })] [TestCase("Series T Se.3 afl.3 en 4", "Series T", 3, new[] { 3, 4 })] [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("", "", , new [] { })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 91cf0e477..db1a61d3f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -92,5 +92,12 @@ namespace NzbDrone.Core.Test.ParserTests var result = Parser.Parser.ParseTitle(path); result.ReleaseTitle.Should().Be(releaseTitle); } + + [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Босх: Спадок", "Series: Legacy")] + public void should_parse_multiple_series_titles(string postTitle, params string[] titles) + { + var seriesTitleInfo = Parser.Parser.ParseTitle(postTitle).SeriesTitleInfo; + seriesTitleInfo.AllTitles.Should().BeEquivalentTo(titles); + } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 003fb50b5..dc7c38ab5 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -70,6 +70,18 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Show Title (2021) S01 (2160p ATVP WEB-DL Hybrid H265 DV HDR10+ DDP Atmos 5.1 English - HONE)", "HONE")] [TestCase("Series.Title.S01E09.1080p.DSNP.WEB-DL.DDP2.0.H.264-VARYG (Blue Lock, Multi-Subs)", "VARYG")] [TestCase("Series.Title (2014) S09E10 (1080p AMZN WEB-DL x265 HEVC 10bit DDP 5.1 Vyndros)", "Vyndros")] + [TestCase("Series Title S02E03 Title 4k to 1080p DSNP WEBrip x265 DDP 5 1 Releaser[SEV]", "SEV")] + [TestCase("Series Title Season 01 S01 1080p AMZN UHD WebRip x265 DDP 5.1 Atmos Releaser-SEV", "SEV")] + [TestCase("Series Title - S01.E06 - Title 1080p AMZN WebRip x265 DDP 5.1 Atmos Releaser [SEV]", "SEV")] + [TestCase("Grey's Anatomy (2005) - S01E01 - A Hard Day's Night (1080p DSNP WEB-DL x265 Garshasp).mkv", "Garshasp")] + [TestCase("Marvel's Agent Carter (2015) - S02E04 - Smoke & Mirrors (1080p BluRay x265 Kappa).mkv", "Kappa")] + [TestCase("Snowpiercer (2020) - S02E03 - A Great Odyssey (1080p BluRay x265 Kappa).mkv", "Kappa")] + [TestCase("Enaaya (2019) - S01E01 - Episode 1 (1080p WEB-DL x265 Natty).mkv", "Natty")] + [TestCase("SpongeBob SquarePants (1999) - S03E01-E02 - Mermaid Man and Barnacle Boy IV & Doing Time (1080p AMZN WEB-DL x265 RCVR).mkv", "RCVR")] + [TestCase("Invincible (2021) - S01E02 - Here Goes Nothing (1080p WEB-DL x265 SAMPA).mkv", "SAMPA")] + [TestCase("The Bad Batch (2021) - S01E01 - Aftermath (1080p DSNP WEB-DL x265 YOGI).mkv", "YOGI")] + [TestCase("Line of Duty (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")] + [TestCase("Rich & Shameless - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs index 6f31129cf..9910d612f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SeasonParserFixture.cs @@ -72,6 +72,7 @@ namespace NzbDrone.Core.Test.ParserTests } [TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)] + [TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)] public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index 4ebd0f1e1..245fee1a6 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -161,6 +161,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series-S07E12-31st_Century_Fox-[Bluray-1080p].mkv", "Series", 7, 12)] [TestCase("TheTitle-S12E13-3_Acts_of_God.mkv", "TheTitle", 12, 13)] [TestCase("Series Title - Temporada 2 [HDTV 720p][Cap.408]", "Series Title", 4, 8)] + [TestCase("Series Title [HDTV][Cap.104](website.com).avi", "Series Title", 1, 4)] + [TestCase("Series Title [HDTV][Cap.402](website.com).avi", "Series Title", 4, 2)] + [TestCase("Series Title [HDTV 720p][Cap.101](website.com).mkv", "Series Title", 1, 1)] + [TestCase("Босх: Спадок (S2E1) / Series: Legacy (S2E1) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)] // [TestCase("", "", 0, 0)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core.Test/Profiles/QualityProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/QualityProfileServiceFixture.cs index 4c776af57..fae7da081 100644 --- a/src/NzbDrone.Core.Test/Profiles/QualityProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/QualityProfileServiceFixture.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.Profiles Subject.Handle(new ApplicationStartedEvent()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.Insert(It.IsAny()), Times.Exactly(6)); } @@ -42,13 +42,13 @@ namespace NzbDrone.Core.Test.Profiles // We don't want to keep adding them back if a user deleted them on purpose. public void Init_should_skip_if_any_profiles_already_exist() { - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.All()) .Returns(Builder.CreateListOfSize(2).Build().ToList()); Subject.Handle(new ApplicationStartedEvent()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.Insert(It.IsAny()), Times.Never()); } @@ -65,11 +65,11 @@ namespace NzbDrone.Core.Test.Profiles .Build().ToList(); Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); - Mocker.GetMock().Setup(c => c.Get(profile.Id)).Returns(profile); + Mocker.GetMock().Setup(c => c.Get(profile.Id)).Returns(profile); Assert.Throws(() => Subject.Delete(profile.Id)); - Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); } [Test] @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Profiles Subject.Delete(1); - Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); + Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); } [Test] @@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.Profiles .Random(1) .Build().ToList(); - Mocker.GetMock().Setup(c => c.Get(profile.Id)).Returns(profile); + Mocker.GetMock().Setup(c => c.Get(profile.Id)).Returns(profile); Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); Mocker.GetMock() @@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.Profiles Assert.Throws(() => Subject.Delete(1)); - Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); } } } diff --git a/src/NzbDrone.Core.Test/SeriesStatsTests/SeriesStatisticsFixture.cs b/src/NzbDrone.Core.Test/SeriesStatsTests/SeriesStatisticsFixture.cs index 772b084ae..f6d386364 100644 --- a/src/NzbDrone.Core.Test/SeriesStatsTests/SeriesStatisticsFixture.cs +++ b/src/NzbDrone.Core.Test/SeriesStatsTests/SeriesStatisticsFixture.cs @@ -94,20 +94,6 @@ namespace NzbDrone.Core.Test.SeriesStatsTests stats.First().NextAiring.Should().NotHaveValue(); } - [Test] - public void should_have_previous_airing_for_old_episode_with_file() - { - GivenEpisodeWithFile(); - GivenOldEpisode(); - GivenEpisode(); - - var stats = Subject.SeriesStatistics(); - - stats.Should().HaveCount(1); - stats.First().NextAiring.Should().NotHaveValue(); - stats.First().PreviousAiring.Should().BeCloseTo(_episode.AirDateUtc.Value, TimeSpan.FromMilliseconds(1000)); - } - [Test] public void should_have_previous_airing_for_old_episode_without_file_monitored() { diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs index 0ad93ce7e..23d43095a 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeMonitoredServiceTests/SetEpisodeMontitoredFixture.cs @@ -202,7 +202,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests } [Test] - public void should_not_monitor_season_when_all_episodes_are_monitored_except_latest_season() + public void should_not_monitor_season_when_all_episodes_are_monitored_except_last_season() { _series.Seasons = Builder.CreateListOfSize(2) .All() @@ -226,7 +226,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests var monitoringOptions = new MonitoringOptions { - Monitor = MonitorTypes.LatestSeason + Monitor = MonitorTypes.LastSeason }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -264,13 +264,47 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests } [Test] - public void should_not_monitor_latest_season_if_all_episodes_aired_more_than_90_days_ago() + public void should_monitor_last_season_if_all_episodes_aired_more_than_90_days_ago() + { + _series.Seasons = Builder.CreateListOfSize(2) + .All() + .With(n => n.Monitored = true) + .Build() + .ToList(); + + _episodes = Builder.CreateListOfSize(5) + .All() + .With(e => e.SeasonNumber = 1) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-200)) + .TheLast(2) + .With(e => e.SeasonNumber = 2) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-100)) + .Build() + .ToList(); + + var monitoringOptions = new MonitoringOptions + { + Monitor = MonitorTypes.LastSeason + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifySeasonMonitored(n => n.SeasonNumber == 2); + VerifyMonitored(n => n.SeasonNumber == 2); + + VerifySeasonNotMonitored(n => n.SeasonNumber == 1); + VerifyNotMonitored(n => n.SeasonNumber == 1); + } + + [Test] + public void should_not_monitor_any_recent_episodes_if_all_episodes_aired_more_than_90_days_ago() { _episodes.ForEach(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-100)); var monitoringOptions = new MonitoringOptions { - Monitor = MonitorTypes.LatestSeason + Monitor = MonitorTypes.Recent }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); @@ -279,6 +313,43 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests .Verify(v => v.UpdateEpisodes(It.Is>(l => l.All(e => !e.Monitored)))); } + [Test] + public void should_monitor_any_recent_and_future_episodes_if_all_episodes_aired_within_90_days() + { + _series.Seasons = Builder.CreateListOfSize(1) + .All() + .With(n => n.Monitored = true) + .Build() + .ToList(); + + _episodes = Builder.CreateListOfSize(5) + .All() + .With(e => e.SeasonNumber = 1) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-200)) + .TheLast(3) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(-5)) + .TheLast(1) + .With(e => e.AirDateUtc = DateTime.UtcNow.AddDays(30)) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(s => s.GetEpisodeBySeries(It.IsAny())) + .Returns(_episodes); + + var monitoringOptions = new MonitoringOptions + { + Monitor = MonitorTypes.Recent + }; + + Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); + + VerifySeasonMonitored(n => n.SeasonNumber == 1); + VerifyNotMonitored(n => n.AirDateUtc.HasValue && n.AirDateUtc.Value.Before(DateTime.UtcNow.AddDays(-90))); + VerifyMonitored(n => n.AirDateUtc.HasValue && n.AirDateUtc.Value.After(DateTime.UtcNow.AddDays(-90))); + } + [Test] public void should_monitor_latest_season_if_some_episodes_have_aired() { @@ -302,7 +373,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeMonitoredServiceTests var monitoringOptions = new MonitoringOptions { - Monitor = MonitorTypes.LatestSeason + Monitor = MonitorTypes.LastSeason }; Subject.SetEpisodeMonitoredStatus(_series, monitoringOptions); diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs index 7a154d5fa..429eef2d2 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs @@ -58,9 +58,10 @@ namespace NzbDrone.Core.Test.TvTests } [Test] - public void should_monitor_new_seasons_automatically_if_series_is_monitored() + public void should_monitor_new_seasons_automatically_if_monitor_new_items_is_all() { - _series.Monitored = true; + _series.MonitorNewItems = NewItemMonitorTypes.All; + var newSeriesInfo = _series.JsonClone(); newSeriesInfo.Seasons.Add(Builder.CreateNew() .With(s => s.SeasonNumber = 2) @@ -75,9 +76,10 @@ namespace NzbDrone.Core.Test.TvTests } [Test] - public void should_not_monitor_new_seasons_automatically_if_series_is_not_monitored() + public void should_not_monitor_new_seasons_automatically_if_monitor_new_items_is_none() { - _series.Monitored = false; + _series.MonitorNewItems = NewItemMonitorTypes.None; + var newSeriesInfo = _series.JsonClone(); newSeriesInfo.Seasons.Add(Builder.CreateNew() .With(s => s.SeasonNumber = 2) diff --git a/src/NzbDrone.Core/Analytics/AnalyticsService.cs b/src/NzbDrone.Core/Analytics/AnalyticsService.cs index 6c71576a7..a4a043e01 100644 --- a/src/NzbDrone.Core/Analytics/AnalyticsService.cs +++ b/src/NzbDrone.Core/Analytics/AnalyticsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics { get { - var lastRecord = _historyService.Paged(new PagingSpec() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }); + var lastRecord = _historyService.Paged(new PagingSpec() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null, null); var monthAgo = DateTime.UtcNow.AddMonths(-1); return lastRecord.Records.Any(v => v.Date > monthAgo); diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 0a30a9f3f..a0a1896e0 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -42,6 +42,23 @@ namespace NzbDrone.Core.Annotations public string RequestAction { get; set; } } + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class FieldTokenAttribute : Attribute + { + public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null) + { + Label = label; + Field = field; + Token = token; + Value = value?.ToString(); + } + + public string Label { get; set; } + public TokenField Field { get; set; } + public string Token { get; set; } + public string Value { get; set; } + } + public class FieldSelectOption { public int Value { get; set; } @@ -67,7 +84,8 @@ namespace NzbDrone.Core.Annotations OAuth, Device, TagSelect, - RootFolder + RootFolder, + QualityProfile } public enum HiddenType @@ -84,4 +102,11 @@ namespace NzbDrone.Core.Annotations ApiKey, UserName } + + public enum TokenField + { + Label, + HelpText, + HelpTextWarning + } } diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs index 59173f312..032813f20 100644 --- a/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs +++ b/src/NzbDrone.Core/AutoTagging/Specifications/GenreSpecification.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; @@ -17,7 +18,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications public class GenreSpecification : AutoTaggingSpecificationBase { - private static readonly GenreSpecificationValidator Validator = new GenreSpecificationValidator(); + private static readonly GenreSpecificationValidator Validator = new (); public override int Order => 1; public override string ImplementationName => "Genre"; @@ -27,7 +28,7 @@ namespace NzbDrone.Core.AutoTagging.Specifications protected override bool IsSatisfiedByWithoutNegate(Series series) { - return series.Genres.Any(genre => Value.Contains(genre)); + return series.Genres.Any(genre => Value.ContainsIgnoreCase(genre)); } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.cs new file mode 100644 index 000000000..4ac2ef14b --- /dev/null +++ b/src/NzbDrone.Core/AutoTagging/Specifications/QualityProfileSpecification.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 QualityProfileSpecificationValidator : AbstractValidator + { + public QualityProfileSpecificationValidator() + { + RuleFor(c => c.Value).GreaterThan(0); + } + } + + public class QualityProfileSpecification : AutoTaggingSpecificationBase + { + private static readonly QualityProfileSpecificationValidator Validator = new (); + + public override int Order => 1; + public override string ImplementationName => "Quality Profile"; + + [FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(Series series) + { + return Value == series.QualityProfileId; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 93203cc81..808e3dff5 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting public interface IBlocklistService { bool Blocklisted(int seriesId, ReleaseInfo release); + bool BlocklistedTorrentHash(int seriesId, string hash); PagingSpec Paged(PagingSpec pagingSpec); void Block(RemoteEpisode remoteEpisode, string message); void Delete(int id); @@ -61,6 +62,12 @@ namespace NzbDrone.Core.Blocklisting .Any(b => SameNzb(b, release)); } + public bool BlocklistedTorrentHash(int seriesId, string hash) + { + return _blocklistRepository.BlocklistedByTorrentInfoHash(seriesId, hash).Any(b => + b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase)); + } + public PagingSpec Paged(PagingSpec pagingSpec) { return _blocklistRepository.GetPaged(pagingSpec); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index e48f9c245..d2cdd6fed 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -339,6 +339,20 @@ namespace NzbDrone.Core.Configuration } } + public void MigrateConfigFile() + { + if (!File.Exists(_configFile)) + { + return; + } + + // If SSL is enabled and a cert hash is still in the config file disable SSL + if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace()) + { + SetValue("EnableSsl", false); + } + } + private void DeleteOldValues() { var xDoc = LoadConfigFile(); @@ -410,6 +424,7 @@ namespace NzbDrone.Core.Configuration public void HandleAsync(ApplicationStartedEvent message) { + MigrateConfigFile(); EnsureDefaultConfigFile(); DeleteOldValues(); } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 85186e91b..2193b182b 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -144,6 +144,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("AutoRedownloadFailed", value); } } + public bool AutoRedownloadFailedFromInteractiveSearch + { + get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); } + + set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); } + } + public bool CreateEmptySeriesFolders { get { return GetValueBoolean("CreateEmptySeriesFolders", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 2ebc262ac..2bcd7b923 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Configuration // Completed/Failed Download Handling (Download client) bool EnableCompletedDownloadHandling { get; set; } bool AutoRedownloadFailed { get; set; } + bool AutoRedownloadFailedFromInteractiveSearch { get; set; } // Media Management bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index 18adc70cd..e344c355f 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -150,7 +150,7 @@ namespace NzbDrone.Core.CustomFormats } } - return matches; + return matches.OrderBy(x => x.Name).ToList(); } private static List ParseCustomFormat(EpisodeFile episodeFile, Series series, List allCustomFormats) diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index b5e57be87..a7da9eb38 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -407,7 +407,7 @@ namespace NzbDrone.Core.Datastore return pagingSpec; } - private void AddFilters(SqlBuilder builder, PagingSpec pagingSpec) + protected void AddFilters(SqlBuilder builder, PagingSpec pagingSpec) { var filters = pagingSpec.FilterExpressions; diff --git a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs index 069b944fb..19c938737 100644 --- a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs +++ b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs @@ -44,11 +44,12 @@ namespace NzbDrone.Core.Datastore var connectionBuilder = new SQLiteConnectionStringBuilder { DataSource = dbPath, - CacheSize = (int)-10000, + CacheSize = (int)-20000, DateTimeKind = DateTimeKind.Utc, JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal, Pooling = true, - Version = 3 + Version = 3, + BusyTimeout = 100 }; if (OsInfo.IsOsx) diff --git a/src/NzbDrone.Core/Datastore/Migration/195_parse_language_tags_from_existing_subtitle_files.cs b/src/NzbDrone.Core/Datastore/Migration/195_parse_language_tags_from_existing_subtitle_files.cs index b49bfaae5..934b2b6da 100644 --- a/src/NzbDrone.Core/Datastore/Migration/195_parse_language_tags_from_existing_subtitle_files.cs +++ b/src/NzbDrone.Core/Datastore/Migration/195_parse_language_tags_from_existing_subtitle_files.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; using System.Data; -using System.Text.Json; -using System.Text.Json.Serialization; using Dapper; using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; @@ -21,7 +18,6 @@ namespace NzbDrone.Core.Datastore.Migration private void UpdateLanguageTags(IDbConnection conn, IDbTransaction tran) { var updatedLanguageTags = new List(); - var now = DateTime.Now; using (var cmd = conn.CreateCommand()) { @@ -43,16 +39,6 @@ namespace NzbDrone.Core.Datastore.Migration } } - var serializerSettings = new JsonSerializerOptions - { - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - var updateSubtitleFilesSql = "UPDATE \"SubtitleFiles\" SET \"LanguageTags\" = @LanguageTags, \"LastUpdated\" = CURRENT_TIMESTAMP WHERE \"Id\" = @Id"; conn.Execute(updateSubtitleFilesSql, updatedLanguageTags, transaction: tran); } diff --git a/src/NzbDrone.Core/Datastore/Migration/197_list_add_missing_search.cs b/src/NzbDrone.Core/Datastore/Migration/197_list_add_missing_search.cs new file mode 100644 index 000000000..3cd6e6e10 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/197_list_add_missing_search.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(197)] + public class list_add_missing_search : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("ImportLists").AddColumn("SearchForMissingEpisodes").AsBoolean().NotNullable().WithDefaultValue(true); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/199_series_last_aired.cs b/src/NzbDrone.Core/Datastore/Migration/199_series_last_aired.cs new file mode 100644 index 000000000..c048dcc69 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/199_series_last_aired.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(199)] + public class series_last_aired : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series").AddColumn("LastAired").AsDateTimeOffset().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/200_monitor_new_items.cs b/src/NzbDrone.Core/Datastore/Migration/200_monitor_new_items.cs new file mode 100644 index 000000000..d2961c06c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/200_monitor_new_items.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(200)] + public class AddNewItemMonitorType : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series").AddColumn("MonitorNewItems").AsInt32().WithDefaultValue(0); + Alter.Table("ImportLists").AddColumn("MonitorNewItems").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index 23fcaa901..ed4fb1545 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; @@ -12,7 +12,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications private static readonly Regex[] DiscRegex = new[] { new Regex(@"(?:dis[ck])(?:[-_. ]\d+[-_. ])(?:(?:(?:480|720|1080|2160)[ip]|)[-_. ])?(?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase), - new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase) + new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"(?:\d?x?M?DVD-?[R59])(?:[ ._]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase) }; private static readonly string[] _dvdContainerTypes = new[] { "vob", "iso" }; @@ -39,8 +40,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { if (regex.IsMatch(subject.Release.Title)) { - _logger.Debug("Release contains raw Bluray, rejecting."); - return Decision.Reject("Raw Bluray release"); + _logger.Debug("Release contains raw Bluray/DVD, rejecting."); + return Decision.Reject("Raw Bluray/DVD release"); } } diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index d8ba934a0..621b8937e 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -7,7 +7,9 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -27,8 +29,10 @@ namespace NzbDrone.Core.Download.Clients.Aria2 IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; } @@ -233,14 +237,19 @@ namespace NzbDrone.Core.Download.Clients.Aria2 if (new Version(version) < new Version("1.34.0")) { - return new ValidationFailure(string.Empty, "Aria2 version should be at least 1.34.0. Version reported is {0}", version); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { + { "clientName", "Aria2" }, { "requiredVersion", "1.34.0" }, { "reportedVersion", version } + })); } } catch (Exception ex) { _logger.Error(ex, "Failed to test Aria2"); - return new NzbDroneValidationFailure("Host", "Unable to connect to Aria2") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", "Aria2" } })) { DetailedDescription = ex.Message }; diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs index e88bc4cc1..088f06f9e 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Settings.cs @@ -32,13 +32,13 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [FieldDefinition(1, Label = "Port", Type = FieldType.Number)] public int Port { get; set; } - [FieldDefinition(2, Label = "XML RPC Path", Type = FieldType.Textbox)] + [FieldDefinition(2, Label = "XmlRpcPath", Type = FieldType.Textbox)] public string RpcPath { get; set; } - [FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } - [FieldDefinition(4, Label = "Secret token", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] + [FieldDefinition(4, Label = "SecretToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string SecretToken { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs index c9b45369e..282ededa1 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackhole.cs @@ -7,7 +7,9 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -29,8 +31,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _scanWatchFolder = scanWatchFolder; @@ -79,7 +83,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole return null; } - public override string Name => "Torrent Blackhole"; + public override string Name => _localizationService.GetLocalizedString("TorrentBlackhole"); public override IEnumerable GetItems() { diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs index 49673a562..ad3010a60 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs @@ -28,23 +28,24 @@ namespace NzbDrone.Core.Download.Clients.Blackhole private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Sonarr will store the .torrent file")] + [FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] + [FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")] public string TorrentFolder { get; set; } - [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Sonarr should import completed downloads")] + [FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")] public string WatchFolder { get; set; } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [FieldDefinition(2, Label = "Save Magnet Files", Type = FieldType.Checkbox, HelpText = "Save the magnet link if no .torrent file is available (only useful if the download client supports magnets saved to a file)")] + [FieldDefinition(2, Label = "TorrentBlackholeSaveMagnetFiles", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesHelpText")] public bool SaveMagnetFiles { get; set; } - [FieldDefinition(3, Label = "Save Magnet Files", Type = FieldType.Textbox, HelpText = "Extension to use for magnet links, defaults to '.magnet'")] + [FieldDefinition(3, Label = "TorrentBlackholeSaveMagnetFilesExtension", Type = FieldType.Textbox, HelpText = "TorrentBlackholeSaveMagnetFilesExtensionHelpText")] public string MagnetFileExtension { get; set; } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [FieldDefinition(4, Label = "Read Only", Type = FieldType.Checkbox, HelpText = "Instead of moving files this will instruct Sonarr to Copy or Hardlink (depending on settings/system configuration)")] + [FieldDefinition(4, Label = "TorrentBlackholeSaveMagnetFilesReadOnly", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText")] public bool ReadOnly { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 5fbc74a74..3a7105ba9 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger) + Logger logger, + ILocalizationService localizationService) + : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger, localizationService) { _scanWatchFolder = scanWatchFolder; @@ -51,7 +53,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole return null; } - public override string Name => "Usenet Blackhole"; + public override string Name => _localizationService.GetLocalizedString("UsenetBlackhole"); public override IEnumerable GetItems() { diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs index b2ff88149..ae9603b61 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs @@ -19,10 +19,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Sonarr will store the .nzb file")] + [FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] + [FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")] public string NzbFolder { get; set; } - [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Sonarr should import completed downloads")] + [FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")] public string WatchFolder { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 39a191089..10716c699 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -7,7 +7,9 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -25,8 +27,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; } @@ -155,7 +159,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge if (torrent.State == DelugeTorrentStatus.Error) { item.Status = DownloadItemStatus.Warning; - item.Message = "Deluge is reporting an error"; + item.Message = _localizationService.GetLocalizedString("DownloadClientDelugeTorrentStateError"); } else if (torrent.IsFinished && torrent.State != DelugeTorrentStatus.Checking) { @@ -248,7 +252,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Password", "Authentication failed"); + return new NzbDroneValidationFailure("Password", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")); } catch (WebException ex) { @@ -256,29 +260,29 @@ namespace NzbDrone.Core.Download.Clients.Deluge switch (ex.Status) { case WebExceptionStatus.ConnectFailure: - return new NzbDroneValidationFailure("Host", "Unable to connect") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { - DetailedDescription = "Please verify the hostname and port." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnectDetail") }; case WebExceptionStatus.ConnectionClosed: - return new NzbDroneValidationFailure("UseSsl", "Verify SSL settings") + return new NzbDroneValidationFailure("UseSsl", _localizationService.GetLocalizedString("DownloadClientValidationVerifySsl")) { - DetailedDescription = "Please verify your SSL configuration on both Deluge and Sonarr." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationVerifySslDetail", new Dictionary { { "clientName", Name } }) }; case WebExceptionStatus.SecureChannelFailure: - return new NzbDroneValidationFailure("UseSsl", "Unable to connect through SSL") + return new NzbDroneValidationFailure("UseSsl", _localizationService.GetLocalizedString("DownloadClientValidationSslConnectFailure")) { - DetailedDescription = "Sonarr is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both Sonarr and Deluge to not use SSL." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationSslConnectFailureDetail", new Dictionary { { "clientName", Name } }) }; default: - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } } catch (Exception ex) { _logger.Error(ex, "Failed to test connection"); - return new NzbDroneValidationFailure("Host", "Unable to connect to Deluge") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -298,9 +302,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge if (!enabledPlugins.Contains("Label")) { - return new NzbDroneValidationFailure("TvCategory", "Label plugin not activated") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginInactive")) { - DetailedDescription = "You must have the Label plugin enabled in Deluge to use categories." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginInactiveDetail", new Dictionary { { "clientName", Name } }) }; } @@ -313,9 +317,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge if (!labels.Contains(Settings.TvCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Configuration of label failed") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginFailure")) { - DetailedDescription = "Sonarr was unable to add the label to Deluge." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginFailureDetail", new Dictionary { { "clientName", Name } }) }; } } @@ -327,9 +331,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge if (!labels.Contains(Settings.TvImportedCategory)) { - return new NzbDroneValidationFailure("TvImportedCategory", "Configuration of label failed") + return new NzbDroneValidationFailure("TvImportedCategory", _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginFailure")) { - DetailedDescription = "Sonarr was unable to add the label to Deluge." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDelugeValidationLabelPluginFailureDetail", new Dictionary { { "clientName", Name } }) }; } } @@ -346,7 +350,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge catch (Exception ex) { _logger.Error(ex, "Unable to get torrents"); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index 97aab6a6e..03f266189 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -35,28 +35,30 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Deluge")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Deluge")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the deluge json url, see http://[host]:[port]/[urlBase]/json")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/json")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(6, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove torrents in that category even if seeding finished. Leave blank to keep same category.")] + [FieldDefinition(6, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] public string TvImportedCategory { get; set; } - [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(7, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(8, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index 1d964377b..6bb2d5a5a 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -36,7 +36,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Download Station")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Download Station")] public bool UseSsl { get; set; } [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -45,10 +46,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [FieldDefinition(4, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.. Creates a [category] subdirectory in the output directory.")] + [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")] public string TvCategory { get; set; } - [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")] + [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientDownloadStationSettingsDirectory")] public string TvDirectory { get; set; } public DownloadStationSettings() diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index 55fed1410..612be692d 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -8,8 +8,10 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -36,8 +38,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _dsInfoProxy = dsInfoProxy; _dsTaskProxySelector = dsTaskProxySelector; @@ -48,7 +52,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public override string Name => "Download Station"; - public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); + public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientDownloadStationProviderMessage"), ProviderMessageType.Warning); private IDownloadStationTaskProxy DsTaskProxy => _dsTaskProxySelector.GetProxy(Settings); @@ -222,7 +226,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { if (torrent.Status == DownloadStationTaskStatus.Extracting) { - return $"Extracting: {int.Parse(torrent.StatusExtra["unzip_progress"])}%"; + return _localizationService.GetLocalizedString("DownloadStationStatusExtracting", + new Dictionary + { { "progress", int.Parse(torrent.StatusExtra["unzip_progress"]) } }); } if (torrent.Status == DownloadStationTaskStatus.Error) @@ -308,9 +314,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (downloadDir == null) { - return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "No default destination") + return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination") { - DetailedDescription = $"You must login into your Diskstation as {Settings.Username} and manually set it up into DownloadStation settings under BT/HTTP/FTP/NZB -> Location." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary { { "username", Settings.Username } }) }; } @@ -325,17 +331,17 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (folderInfo.Additional == null) { - return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist") + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) { - DetailedDescription = $"The Diskstation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?" + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary { { "sharedFolder", sharedFolder } }) }; } if (!folderInfo.IsDir) { - return new NzbDroneValidationFailure(fieldName, $"Folder does not exist") + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) { - DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) }; } } @@ -351,7 +357,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation catch (Exception ex) { _logger.Error(ex, "Error testing Torrent Download Station"); - return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } } @@ -364,9 +370,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + return new NzbDroneValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")) { - DetailedDescription = $"Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailureDetail", new Dictionary { { "clientName", Name } }) }; } catch (WebException ex) @@ -375,19 +381,19 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (ex.Status == WebExceptionStatus.ConnectFailure) { - return new NzbDroneValidationFailure("Host", "Unable to connect") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { - DetailedDescription = "Please verify the hostname and port." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnectDetail") }; } - return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } catch (Exception ex) { _logger.Error(ex, "Error testing Torrent Download Station"); - return new NzbDroneValidationFailure("Host", "Unable to connect to Torrent Download Station") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -402,7 +408,12 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (info.MinVersion > 2 || info.MaxVersion < 2) { - return new ValidationFailure(string.Empty, $"Download Station API version not supported, should be at least 2. It supports from {info.MinVersion} to {info.MaxVersion}"); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationApiVersion", + new Dictionary + { + { "requiredVersion", 2 }, { "minVersion", info.MinVersion }, { "maxVersion", info.MaxVersion } + })); } return null; @@ -417,7 +428,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (Exception ex) { - return new NzbDroneValidationFailure(string.Empty, $"Failed to get the list of torrents: {ex.Message}"); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 27bc2bbdf..6f89845a9 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.ThingiProvider; @@ -34,8 +35,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger) + Logger logger, + ILocalizationService localizationService) + : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger, localizationService) { _dsInfoProxy = dsInfoProxy; _dsTaskProxySelector = dsTaskProxySelector; @@ -46,7 +48,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public override string Name => "Download Station"; - public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning); + public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientDownloadStationProviderMessage"), ProviderMessageType.Warning); private IDownloadStationTaskProxy DsTaskProxy => _dsTaskProxySelector.GetProxy(Settings); @@ -213,9 +215,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (downloadDir == null) { - return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "No default destination") + return new NzbDroneValidationFailure(nameof(Settings.TvDirectory), "DownloadClientDownloadStationValidationNoDefaultDestination") { - DetailedDescription = $"You must login into your Diskstation as {Settings.Username} and manually set it up into DownloadStation settings under BT/HTTP/FTP/NZB -> Location." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationNoDefaultDestinationDetail", new Dictionary { { "username", Settings.Username } }) }; } @@ -230,17 +232,17 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (folderInfo.Additional == null) { - return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist") + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissing")) { - DetailedDescription = $"The Diskstation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?" + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationSharedFolderMissingDetail", new Dictionary { { "sharedFolder", sharedFolder } }) }; } if (!folderInfo.IsDir) { - return new NzbDroneValidationFailure(fieldName, $"Folder does not exist") + return new NzbDroneValidationFailure(fieldName, _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissing")) { - DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationFolderMissingDetail", new Dictionary { { "downloadDir", downloadDir }, { "sharedFolder", sharedFolder } }) }; } } @@ -256,7 +258,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation catch (Exception ex) { _logger.Error(ex, "Error testing Usenet Download Station"); - return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } } @@ -269,9 +271,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + return new NzbDroneValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")) { - DetailedDescription = $"Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailureDetail", new Dictionary { { "clientName", Name } }) }; } catch (WebException ex) @@ -280,19 +282,19 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (ex.Status == WebExceptionStatus.ConnectFailure) { - return new NzbDroneValidationFailure("Host", "Unable to connect") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { - DetailedDescription = "Please verify the hostname and port." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnectDetail") }; } - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } catch (Exception ex) { _logger.Error(ex, "Error testing Torrent Download Station"); - return new NzbDroneValidationFailure("Host", "Unable to connect to Usenet Download Station") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -307,7 +309,12 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation if (info.MinVersion > 2 || info.MaxVersion < 2) { - return new ValidationFailure(string.Empty, $"Download Station API version not supported, should be at least 2. It supports from {info.MinVersion} to {info.MaxVersion}"); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientDownloadStationValidationApiVersion", + new Dictionary + { + { "requiredVersion", 2 }, { "minVersion", info.MinVersion }, { "maxVersion", info.MaxVersion } + })); } return null; @@ -319,7 +326,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { if (task.Status == DownloadStationTaskStatus.Extracting) { - return $"Extracting: {int.Parse(task.StatusExtra["unzip_progress"])}%"; + return _localizationService.GetLocalizedString("DownloadStationStatusExtracting", + new Dictionary + { { "progress", int.Parse(task.StatusExtra["unzip_progress"]) } }); } if (task.Status == DownloadStationTaskStatus.Error) @@ -398,7 +407,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation } catch (Exception ex) { - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of NZBs: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestNzbs", new Dictionary { { "exceptionMessage", ex.Message } })); } } diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index abbc62e6f..6e091e4e6 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -7,8 +7,10 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Flood.Models; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -28,8 +30,10 @@ namespace NzbDrone.Core.Download.Clients.Flood IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; _downloadSeedConfigProvider = downloadSeedConfigProvider; @@ -81,7 +85,7 @@ namespace NzbDrone.Core.Download.Clients.Flood } public override string Name => "Flood"; - public override ProviderMessage Message => new ProviderMessage("Sonarr will handle automatic removal of torrents based on the current seed criteria in Settings -> Indexers", ProviderMessageType.Info); + public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientFloodSettingsRemovalInfo"), ProviderMessageType.Info); protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) { diff --git a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs index 9877b9707..f26e19512 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/FloodSettings.cs @@ -40,10 +40,12 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Flood")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Flood")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, HelpText = "Optionally adds a prefix to Flood API, such as [protocol]://[host]:[port]/[urlBase]api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, HelpText = "DownloadClientFloodSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "[protocol]://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -52,19 +54,19 @@ namespace NzbDrone.Core.Download.Clients.Flood [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, HelpText = "Manually specifies download destination")] + [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDestinationHelpText")] public string Destination { get; set; } - [FieldDefinition(7, Label = "Tags", Type = FieldType.Tag, HelpText = "Initial tags of a download. To be recognized, a download must have all initial tags. This avoids conflicts with unrelated downloads.")] + [FieldDefinition(7, Label = "Tags", Type = FieldType.Tag, HelpText = "DownloadClientFloodSettingsTagsHelpText")] public IEnumerable Tags { get; set; } - [FieldDefinition(8, Label = "Post-Import Tags", Type = FieldType.Tag, HelpText = "Appends tags after a download is imported.", Advanced = true)] + [FieldDefinition(8, Label = "DownloadClientFloodSettingsPostImportTags", Type = FieldType.Tag, HelpText = "DownloadClientFloodSettingsPostImportTagsHelpText", Advanced = true)] public IEnumerable PostImportTags { get; set; } - [FieldDefinition(9, Label = "Additional Tags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "Adds properties of media as tags. Hints are examples.", Advanced = true)] + [FieldDefinition(9, Label = "DownloadClientFloodSettingsAdditionalTags", Type = FieldType.Select, SelectOptions = typeof(AdditionalTags), HelpText = "DownloadClientFloodSettingsAdditionalTagsHelpText", Advanced = true)] public IEnumerable AdditionalTags { get; set; } - [FieldDefinition(10, Label = "Start on Add", Type = FieldType.Checkbox)] + [FieldDefinition(10, Label = "DownloadClientFloodSettingsStartOnAdd", Type = FieldType.Checkbox)] public bool StartOnAdd { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs index 45de36ef4..42b645848 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/FreeboxDownloadSettings.cs @@ -46,37 +46,42 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload ApiUrl = "/api/v1/"; } - [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")] + [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsHostHelpText")] + [FieldToken(TokenField.HelpText, "Host", "url", "mafreebox.freebox.fr")] public string Host { get; set; } - [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")] + [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsPortHelpText")] + [FieldToken(TokenField.HelpText, "Port", "port", 443)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Freebox API")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")] + [FieldDefinition(3, Label = "DownloadClientFreeboxSettingsApiUrl", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientFreeboxSettingsApiUrlHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientFreeboxSettingsApiUrl", "url", "http://[host]:[port]/[api_base_url]/[api_version]/")] + [FieldToken(TokenField.HelpText, "DownloadClientFreeboxSettingsApiUrl", "defaultApiUrl", "/api/v1/")] public string ApiUrl { get; set; } - [FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")] + [FieldDefinition(4, Label = "DownloadClientFreeboxSettingsAppId", Type = FieldType.Textbox, HelpText = "DownloadClientFreeboxSettingsAppIdHelpText")] public string AppId { get; set; } - [FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")] + [FieldDefinition(5, Label = "DownloadClientFreeboxSettingsAppToken", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "DownloadClientFreeboxSettingsAppTokenHelpText")] public string AppToken { get; set; } - [FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")] + [FieldDefinition(6, Label = "Destination", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsDestinationHelpText")] public string DestinationDirectory { get; set; } - [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads (will create a [category] subdirectory in the output directory)")] + [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")] public string Category { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderPriority { get; set; } - [FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs index 449afa7b3..07f435a34 100644 --- a/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs +++ b/src/NzbDrone.Core/Download/Clients/FreeboxDownload/TorrentFreeboxDownload.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -6,8 +6,10 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -24,8 +26,10 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; } @@ -108,8 +112,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload case FreeboxDownloadTaskStatus.Unknown: default: // new status in API? default to downloading - item.Message = "Unknown download state: " + torrent.Status; - _logger.Info(item.Message); + item.Message = _localizationService.GetLocalizedString("UnknownDownloadState", + new Dictionary { { "state", torrent.Status } }); + _logger.Info($"Unknown download state: {torrent.Status}"); item.Status = DownloadItemStatus.Downloading; break; } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 9773065aa..a29be7f4c 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -5,8 +5,10 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Hadouken.Models; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -24,8 +26,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; } @@ -159,18 +163,20 @@ namespace NzbDrone.Core.Download.Clients.Hadouken if (version < new Version("5.1")) { return new ValidationFailure(string.Empty, - "Old Hadouken client with unsupported API, need 5.1 or higher"); + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { { "clientName", Name }, { "requiredVersion", "5.1" }, { "reportedVersion", version } })); } } catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Password", "Authentication failed"); + return new NzbDroneValidationFailure("Password", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")); } catch (Exception ex) { - return new NzbDroneValidationFailure("Host", "Unable to connect to Hadouken") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect")) { DetailedDescription = ex.Message }; @@ -188,7 +194,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken catch (Exception ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index 5b0603dd7..8e560720e 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -39,10 +39,13 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Hadouken")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Hadouken")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Hadouken url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Hadouken")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -51,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox)] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string Category { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index 5e5fb3acd..dbdfdb7c4 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation; @@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger) + Logger logger, + ILocalizationService localizationService) + : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger, localizationService) { _proxy = proxy; } @@ -162,7 +164,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { _logger.Error(ex, "Unable to connect to NZBVortex"); - return new NzbDroneValidationFailure("Host", "Unable to connect to NZBVortex") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -180,13 +182,16 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex if (version.Major < 2 || (version.Major == 2 && version.Minor < 3)) { - return new ValidationFailure("Host", "NZBVortex needs to be updated"); + return new ValidationFailure("Host", + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { { "clientName", Name }, { "requiredVersion", "2.3" }, { "reportedVersion", version } })); } } catch (Exception ex) { _logger.Error(ex, "Unable to connect to NZBVortex"); - return new ValidationFailure("Host", "Unable to connect to NZBVortex"); + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })); } return null; @@ -200,7 +205,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex } catch (NzbVortexAuthenticationException) { - return new ValidationFailure("ApiKey", "API Key Incorrect"); + return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyIncorrect")); } return null; @@ -214,9 +219,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex { if (Settings.TvCategory.IsNotNullOrWhiteSpace()) { - return new NzbDroneValidationFailure("TvCategory", "Group does not exist") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientValidationGroupMissing")) { - DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationGroupMissingDetail", new Dictionary { { "clientName", Name } }) }; } } @@ -243,7 +248,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex if (filesResponse.Count > 1) { - var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath); + var message = _localizationService.GetLocalizedString("DownloadClientNzbVortexMultipleFilesMessage", new Dictionary { { "outputPath", outputPath } }); queueItem.Status = DownloadItemStatus.Warning; queueItem.Message = message; diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs index 3b7187a63..cec43775a 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json; diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index d73c1853e..81a72004e 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -42,19 +42,21 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBVortex url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(2, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "NZBVortex")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } - [FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(3, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(4, Label = "Group", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(5, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(6, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 54974815c..d7956318e 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation; @@ -28,8 +29,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger) + Logger logger, + ILocalizationService localizationService) + : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger, localizationService) { _proxy = proxy; } @@ -124,7 +126,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(itemDir)); historyItem.Category = item.Category; - historyItem.Message = $"PAR Status: {item.ParStatus} - Unpack Status: {item.UnpackStatus} - Move Status: {item.MoveStatus} - Script Status: {item.ScriptStatus} - Delete Status: {item.DeleteStatus} - Mark Status: {item.MarkStatus}"; + historyItem.Message = _localizationService.GetLocalizedString("NzbgetHistoryItemMessage", + new Dictionary + { + { "parStatus", item.ParStatus }, { "unpackStatus", item.UnpackStatus }, + { "moveStatus", item.MoveStatus }, { "scriptStaus", item.ScriptStatus }, + { "deleteStatus", item.DeleteStatus }, { "markStatus", item.MarkStatus } + }); historyItem.Status = DownloadItemStatus.Completed; historyItem.RemainingTime = TimeSpan.Zero; historyItem.CanMoveFiles = true; @@ -270,18 +278,23 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (Version.Parse(version) < Version.Parse("12.0")) { - return new ValidationFailure(string.Empty, "Nzbget version too low, need 12.0 or higher"); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { { "clientName", Name }, { "requiredVersion", "12.0" }, { "reportedVersion", version } })); } } catch (Exception ex) { if (ex.Message.ContainsIgnoreCase("Authentication failed")) { - return new ValidationFailure("Username", "Authentication failed"); + return new ValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")); } _logger.Error(ex, "Unable to connect to NZBGet"); - return new ValidationFailure("Host", "Unable to connect to NZBGet"); + return new ValidationFailure("Host", + _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", + new Dictionary { { "clientName", Name } })); } return null; @@ -294,10 +307,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (!Settings.TvCategory.IsNullOrWhiteSpace() && !categories.Any(v => v.Name == Settings.TvCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Category does not exist") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientValidationCategoryMissing")) { InfoLink = _proxy.GetBaseUrl(Settings), - DetailedDescription = "The Category your entered doesn't exist in NzbGet. Go to NzbGet to create it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationCategoryMissingDetail", new Dictionary { { "clientName", Name } }) }; } @@ -311,18 +324,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var keepHistory = config.GetValueOrDefault("KeepHistory", "7"); if (!int.TryParse(keepHistory, NumberStyles.None, CultureInfo.InvariantCulture, out var value) || value == 0) { - return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0") + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientNzbgetValidationKeepHistoryZero")) { InfoLink = _proxy.GetBaseUrl(Settings), - DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientNzbgetValidationKeepHistoryZeroDetail") }; } else if (value > 25000) { - return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be less than 25000") + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientNzbgetValidationKeepHistoryOverMax")) { InfoLink = _proxy.GetBaseUrl(Settings), - DetailedDescription = "NzbGet setting KeepHistory is set too high." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientNzbgetValidationKeepHistoryOverMaxDetail") }; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index 3fe98ef9c..067867b08 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -40,10 +40,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "NZBGet")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "NZBGet")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/jsonrpc")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -52,16 +55,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(7, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(8, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(9, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")] + [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index f531778d1..920279263 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -23,8 +24,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, - Logger logger) - : base(configService, diskProvider, remotePathMappingService, logger) + Logger logger, + ILocalizationService localizationService) + : base(configService, diskProvider, remotePathMappingService, logger, localizationService) { _httpClient = httpClient; } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs index 741021a3f..6cd8a2b89 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -19,10 +19,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); - [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "This folder will need to be reachable from XBMC")] + [FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")] public string NzbFolder { get; set; } - [FieldDefinition(1, Label = "Strm Folder", Type = FieldType.Path, HelpText = ".strm files in this folder will be import by drone")] + [FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")] public string StrmFolder { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 06067c32d..c1d6f9f4f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -8,7 +8,9 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -34,8 +36,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, ICacheManager cacheManager, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxySelector = proxySelector; @@ -241,7 +245,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { case "error": // some error occurred, applies to paused torrents, warning so failed download handling isn't triggered item.Status = DownloadItemStatus.Warning; - item.Message = "qBittorrent is reporting an error"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateError"); break; case "pausedDL": // torrent is paused and has NOT finished downloading @@ -266,24 +270,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent case "stalledDL": // torrent is being downloaded, but no connection were made item.Status = DownloadItemStatus.Warning; - item.Message = "The download is stalled with no connections"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateStalled"); break; case "missingFiles": // torrent is missing files item.Status = DownloadItemStatus.Warning; - item.Message = "The download is missing files"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateMissingFiles"); break; case "metaDL": // torrent magnet is being downloaded if (config.DhtEnabled) { item.Status = DownloadItemStatus.Queued; - item.Message = "qBittorrent is downloading metadata"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateMetadata"); } else { item.Status = DownloadItemStatus.Warning; - item.Message = "qBittorrent cannot resolve magnet link with DHT disabled"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateDhtDisabled"); } break; @@ -296,22 +300,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent break; default: // new status in API? default to downloading - item.Message = "Unknown download state: " + torrent.State; - _logger.Info(item.Message); + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateUnknown", new Dictionary { { "state", torrent.State } }); + _logger.Info($"Unknown download state: {torrent.State}"); item.Status = DownloadItemStatus.Downloading; break; } - if (version >= new Version("2.6.1")) + if (version >= new Version("2.6.1") && item.Status == DownloadItemStatus.Completed) { if (torrent.ContentPath != torrent.SavePath) { item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.ContentPath)); } - else if (item.Status == DownloadItemStatus.Completed) + else { item.Status = DownloadItemStatus.Warning; - item.Message = "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?"; + item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStatePathError"); } } @@ -384,10 +388,13 @@ 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 { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) } + OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }, + RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles) }; } @@ -412,29 +419,30 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent if (version < Version.Parse("1.5")) { // API version 5 introduced the "save_path" property in /query/torrents - return new NzbDroneValidationFailure("Host", "Unsupported client version") - { - DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher." - }; + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { + { "clientName", Name }, { "requiredVersion", "3.2.4" }, { "reportedVersion", version } + })); } else if (version < Version.Parse("1.6")) { // API version 6 introduced support for labels if (Settings.TvCategory.IsNotNullOrWhiteSpace()) { - return new NzbDroneValidationFailure("Category", "Category is not supported") + return new NzbDroneValidationFailure("Category", _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryUnsupported")) { - DetailedDescription = "Labels are not supported until qBittorrent version 3.3.0. Please upgrade or try again with an empty Category." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryUnsupportedDetail") }; } } else if (Settings.TvCategory.IsNullOrWhiteSpace()) { // warn if labels are supported, but category is not provided - return new NzbDroneValidationFailure("TvCategory", "Category is recommended") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryRecommended")) { IsWarning = true, - DetailedDescription = "Sonarr will not attempt to import completed downloads without a category." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryRecommendedDetail") }; } @@ -442,18 +450,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent var config = Proxy.GetConfig(Settings); if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)) { - return new NzbDroneValidationFailure(string.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit") + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimit")) { - DetailedDescription = "Sonarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail") }; } } catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + return new NzbDroneValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")) { - DetailedDescription = "Please verify your username and password." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailureDetail", new Dictionary { { "clientName", Name } }) }; } catch (WebException ex) @@ -461,19 +469,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _logger.Error(ex, "Unable to connect to qBittorrent"); if (ex.Status == WebExceptionStatus.ConnectFailure) { - return new NzbDroneValidationFailure("Host", "Unable to connect") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { - DetailedDescription = "Please verify the hostname and port." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnectDetail") }; } - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } catch (Exception ex) { _logger.Error(ex, "Unable to test qBittorrent"); - return new NzbDroneValidationFailure("Host", "Unable to connect to qBittorrent") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -505,9 +513,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent if (!labels.ContainsKey(Settings.TvCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Configuration of label failed") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryAddFailure")) { - DetailedDescription = "Sonarr was unable to add the label to qBittorrent." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryAddFailureDetail") }; } } @@ -519,9 +527,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent if (!labels.ContainsKey(Settings.TvImportedCategory)) { - return new NzbDroneValidationFailure("TvImportedCategory", "Configuration of label failed") + return new NzbDroneValidationFailure("TvImportedCategory", _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryAddFailure")) { - DetailedDescription = "Sonarr was unable to add the label to qBittorrent." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationCategoryAddFailureDetail") }; } } @@ -547,18 +555,24 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { if (!recentPriorityDefault) { - return new NzbDroneValidationFailure(nameof(Settings.RecentTvPriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." }; + return new NzbDroneValidationFailure(nameof(Settings.RecentTvPriority), _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationQueueingNotEnabled")) + { + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationQueueingNotEnabledDetail") + }; } else if (!olderPriorityDefault) { - return new NzbDroneValidationFailure(nameof(Settings.OlderTvPriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." }; + return new NzbDroneValidationFailure(nameof(Settings.OlderTvPriority), _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationQueueingNotEnabled")) + { + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationQueueingNotEnabledDetail") + }; } } } catch (Exception ex) { _logger.Error(ex, "Failed to test qBittorrent"); - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } return null; @@ -573,7 +587,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent catch (Exception ex) { _logger.Error(ex, "Failed to get torrents"); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 8d83e9156..d092703ad 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -36,10 +36,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsUseSslHelpText")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "qBittorrent")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -48,25 +50,25 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")] + [FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] public string TvImportedCategory { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(10, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent. Note that Forced Torrents do not abide by seed restrictions")] + [FieldDefinition(10, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "DownloadClientQbittorrentSettingsInitialStateHelpText")] public int InitialState { get; set; } - [FieldDefinition(11, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")] + [FieldDefinition(11, Label = "DownloadClientQbittorrentSettingsSequentialOrder", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsSequentialOrderHelpText")] public bool SequentialOrder { get; set; } - [FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")] + [FieldDefinition(12, Label = "DownloadClientQbittorrentSettingsFirstAndLastFirst", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText")] public bool FirstAndLast { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index e0fea34dc..5d9003849 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation; @@ -26,8 +27,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger) + Logger logger, + ILocalizationService localizationService) + : base(httpClient, configService, diskProvider, remotePathMappingService, nzbValidationService, logger, localizationService) { _proxy = proxy; } @@ -276,6 +278,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; } + if (config.Misc.history_retention.IsNotNullOrWhiteSpace() && config.Misc.history_retention.EndsWith("d")) + { + int.TryParse(config.Misc.history_retention.AsSpan(0, config.Misc.history_retention.Length - 1), + out var daysRetention); + status.RemovesCompletedDownloads = daysRetention < 14; + } + else + { + status.RemovesCompletedDownloads = config.Misc.history_retention != "0"; + } + return status; } @@ -370,15 +383,15 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (version == null) { - return new ValidationFailure("Version", "Unknown Version: " + rawVersion); + return new ValidationFailure("Version", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationUnknownVersion", new Dictionary { { "rawVersion", rawVersion ?? "" } })); } if (rawVersion.Equals("develop", StringComparison.InvariantCultureIgnoreCase)) { - return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 3.0.0 or higher.") + return new NzbDroneValidationFailure("Version", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationDevelopVersion")) { IsWarning = true, - DetailedDescription = "Sonarr may not be able to support new features added to SABnzbd when running develop versions." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationDevelopVersionDetail") }; } @@ -392,12 +405,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return null; } - return new ValidationFailure("Version", "Version 0.7.0+ is required, but found: " + version); + return new ValidationFailure("Version", + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { + { "clientName", Name }, { "requiredVersion", "0.7.0" }, { "reportedVersion", version } + })); } catch (Exception ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Host", "Unable to connect to SABnzbd") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -414,12 +432,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { if (ex.Message.ContainsIgnoreCase("API Key Incorrect")) { - return new ValidationFailure("APIKey", "API Key Incorrect"); + return new ValidationFailure("APIKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyIncorrect")); } if (ex.Message.ContainsIgnoreCase("API Key Required")) { - return new ValidationFailure("APIKey", "API Key Required"); + return new ValidationFailure("APIKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyRequired")); } throw; @@ -433,10 +451,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var config = _proxy.GetConfig(Settings); if (config.Misc.pre_check && !HasVersion(1, 1)) { - return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd") + return new NzbDroneValidationFailure("", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationCheckBeforeDownload")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/switches/"), - DetailedDescription = "Using Check before download affects Sonarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationCheckBeforeDownloadDetail") }; } @@ -452,10 +470,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { if (category.Dir.EndsWith("*")) { - return new NzbDroneValidationFailure("TvCategory", "Enable Job folders") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableJobFolders")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"), - DetailedDescription = "Sonarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableJobFoldersDetail") }; } } @@ -463,38 +481,48 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { if (!Settings.TvCategory.IsNullOrWhiteSpace()) { - return new NzbDroneValidationFailure("TvCategory", "Category does not exist") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientValidationCategoryMissing")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"), - DetailedDescription = "The Category your entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationCategoryMissingDetail", new Dictionary { { "clientName", Name } }) }; } } - if (config.Misc.enable_tv_sorting && ContainsCategory(config.Misc.tv_categories, Settings.TvCategory)) + // New in SABnzbd 4.1, but on older versions this will be empty and not apply + if (config.Sorters.Any(s => s.is_active && ContainsCategory(s.sort_cats, Settings.TvCategory))) { - return new NzbDroneValidationFailure("TvCategory", "Disable TV Sorting") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableTvSorting")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"), - DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableTvSortingDetail") + }; + } + + if (config.Misc.enable_tv_sorting && ContainsCategory(config.Misc.tv_categories, Settings.TvCategory)) + { + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableTvSorting")) + { + InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"), + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableTvSortingDetail") }; } if (config.Misc.enable_movie_sorting && ContainsCategory(config.Misc.movie_categories, Settings.TvCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Disable Movie Sorting") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableMovieSorting")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"), - DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail") }; } if (config.Misc.enable_date_sorting && ContainsCategory(config.Misc.date_categories, Settings.TvCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Disable Date Sorting") + return new NzbDroneValidationFailure("TvCategory", _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableDateSorting")) { InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"), - DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientSabnzbdValidationEnableDisableDateSortingDetail") }; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index e25a91701..aa04edc5d 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs @@ -11,11 +11,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { Categories = new List(); Servers = new List(); + Sorters = new List(); } public SabnzbdConfigMisc Misc { get; set; } public List Categories { get; set; } public List Servers { get; set; } + public List Sorters { get; set; } } public class SabnzbdConfigMisc @@ -29,6 +31,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public string[] date_categories { get; set; } public bool enable_date_sorting { get; set; } public bool pre_check { get; set; } + public string history_retention { get; set; } } public class SabnzbdCategory @@ -41,4 +44,22 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public OsPath FullPath { get; set; } } + + public class SabnzbdSorter + { + public SabnzbdSorter() + { + sort_cats = new List(); + sort_type = new List(); + } + + public string name { get; set; } + public int order { get; set; } + public string min_size { get; set; } + public string multipart_label { get; set; } + public string sort_string { get; set; } + public List sort_cats { get; set; } + public List sort_type { get; set; } + public bool is_active { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 13e6b660c..33d2e3266 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -51,13 +51,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Sabnzbd")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Sabnzbd")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } - [FieldDefinition(4, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(4, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } [FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -66,13 +69,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs index 369b9f961..1cfc134c5 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -1,10 +1,13 @@ -using System; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.RemotePathMappings; @@ -18,8 +21,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { } @@ -34,7 +39,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission if (version < new Version(2, 40)) { - return new ValidationFailure(string.Empty, "Transmission version not supported, should be 2.40 or higher."); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", new Dictionary { { "clientName", Name }, { "requiredVersion", "2.40" }, { "reportedVersion", version } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index d0f5a892b..48a268275 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -6,7 +6,9 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -24,8 +26,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; } @@ -270,16 +274,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + return new NzbDroneValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")) { - DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name) + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailureDetail", new Dictionary { { "clientName", Name } }) }; } catch (DownloadClientUnavailableException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Host", "Unable to connect to Transmission") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -288,7 +292,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission { _logger.Error(ex, "Failed to test"); - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } } @@ -303,7 +307,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission catch (Exception ex) { _logger.Error(ex, "Failed to get torrents"); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 40e2528ac..7140c0f4c 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -41,10 +41,15 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Transmission")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Transmission")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, eg http://[host]:[port]/[urlBase]/rpc, defaults to '/transmission/'")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Transmission")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/rpc")] + [FieldToken(TokenField.HelpText, "UrlBase", "defaultUrl", "/transmission/")] + public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -53,19 +58,19 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.. Creates a [category] subdirectory in the output directory.")] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")] + [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")] public string TvDirectory { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)] + [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs index c52a8de88..db12ea64f 100644 --- a/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs +++ b/src/NzbDrone.Core/Download/Clients/Vuze/Vuze.cs @@ -2,8 +2,10 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.Transmission; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.RemotePathMappings; @@ -19,8 +21,10 @@ namespace NzbDrone.Core.Download.Clients.Vuze IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(proxy, torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { } @@ -57,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.Vuze if (!int.TryParse(versionString, out var version) || version < MINIMUM_SUPPORTED_PROTOCOL_VERSION) { { - return new ValidationFailure(string.Empty, "Protocol version not supported, use Vuze 5.0.0.0 or higher with Vuze Web Remote plugin."); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientVuzeValidationErrorVersion")); } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index ebfbe8559..d1e129949 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -8,9 +8,11 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download.Clients.rTorrent; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -34,8 +36,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent IRemotePathMappingService remotePathMappingService, IDownloadSeedConfigProvider downloadSeedConfigProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; @@ -115,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent public override string Name => "rTorrent"; - public override ProviderMessage Message => new ProviderMessage($"rTorrent will not pause torrents when they meet the seed criteria. Sonarr will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers only when Remove Completed is enabled. After importing it will also set \"{_imported_view}\" as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", ProviderMessageType.Info); + public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientRTorrentProviderMessage", new Dictionary { { "importedView", _imported_view } }), ProviderMessageType.Info); public override IEnumerable GetItems() { @@ -250,14 +254,19 @@ namespace NzbDrone.Core.Download.Clients.RTorrent if (new Version(version) < new Version("0.9.0")) { - return new ValidationFailure(string.Empty, "rTorrent version should be at least 0.9.0. Version reported is {0}", version); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { + { "clientName", Name }, { "requiredVersion", "0.9.0" }, { "reportedVersion", version } + })); } } catch (Exception ex) { _logger.Error(ex, "Failed to test rTorrent"); - return new NzbDroneValidationFailure("Host", "Unable to connect to rTorrent") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -275,7 +284,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent catch (Exception ex) { _logger.Error(ex, "Failed to get torrents"); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 820932cec..45822a9f0 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -37,10 +37,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to rTorrent")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "rTorrent")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. This is usually RPC2 or [path to ruTorrent]/plugins/rpc/rpc.php when using ruTorrent.")] + [FieldDefinition(3, Label = "DownloadClientRTorrentSettingsUrlPath", Type = FieldType.Textbox, HelpText = "DownloadClientRTorrentSettingsUrlPathHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientRTorrentSettingsUrlPath", "url", "http(s)://[host]:[port]/[urlPath]")] + [FieldToken(TokenField.HelpText, "DownloadClientRTorrentSettingsUrlPath", "url2", "/plugins/rpc/rpc.php")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -49,22 +52,22 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")] + [FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] public string TvImportedCategory { get; set; } - [FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] + [FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientRTorrentSettingsDirectoryHelpText")] public string TvDirectory { get; set; } - [FieldDefinition(9, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(9, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(10, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to rTorrent in a stopped state. This may break magnet files.")] + [FieldDefinition(11, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")] public bool AddStopped { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index 6a7b372e4..cecc76dd7 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -8,7 +8,9 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -28,8 +30,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, Logger logger) - : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) { _proxy = proxy; @@ -141,7 +145,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent if (torrent.Status.HasFlag(UTorrentTorrentStatus.Error)) { item.Status = DownloadItemStatus.Warning; - item.Message = "uTorrent is reporting an error"; + item.Message = _localizationService.GetLocalizedString("DownloadClientUTorrentTorrentStateError"); } else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Loaded) && torrent.Status.HasFlag(UTorrentTorrentStatus.Checked) && torrent.Remaining == 0 && torrent.Progress == 1.0) @@ -264,15 +268,20 @@ namespace NzbDrone.Core.Download.Clients.UTorrent if (version < 25406) { - return new ValidationFailure(string.Empty, "Old uTorrent client with unsupported API, need 3.0 or higher"); + return new ValidationFailure(string.Empty, + _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion", + new Dictionary + { + { "clientName", Name }, { "requiredVersion", "3.0" }, { "reportedVersion", version } + })); } } catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + return new NzbDroneValidationFailure("Username", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure")) { - DetailedDescription = "Please verify your username and password." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailureDetail", new Dictionary { { "clientName", Name } }) }; } catch (WebException ex) @@ -280,19 +289,19 @@ namespace NzbDrone.Core.Download.Clients.UTorrent _logger.Error(ex, "Unable to connect to uTorrent"); if (ex.Status == WebExceptionStatus.ConnectFailure) { - return new NzbDroneValidationFailure("Host", "Unable to connect") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { - DetailedDescription = "Please verify the hostname and port." + DetailedDescription = _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnectDetail") }; } - return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary { { "exception", ex.Message } })); } catch (Exception ex) { _logger.Error(ex, "Failed to test uTorrent"); - return new NzbDroneValidationFailure("Host", "Unable to connect to uTorrent") + return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary { { "clientName", Name } })) { DetailedDescription = ex.Message }; @@ -310,7 +319,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent catch (Exception ex) { _logger.Error(ex, "Failed to get torrents"); - return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index e6ae23df5..6c77dd873 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -34,10 +34,13 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to uTorrent")] + [FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")] + [FieldToken(TokenField.HelpText, "UseSsl", "clientName", "uTorrent")] public bool UseSsl { get; set; } - [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the uTorrent url, e.g. http://[host]:[port]/[urlBase]/api")] + [FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")] + [FieldToken(TokenField.HelpText, "UrlBase", "clientName", "uTorrent")] + [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] @@ -46,19 +49,20 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a category is optional, but strongly recommended.")] + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] public string TvCategory { get; set; } - [FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Sonarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")] + [FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] public string TvImportedCategory { get; set; } - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + [FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")] public int RecentTvPriority { get; set; } - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] + [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] public int OlderTvPriority { get; set; } - [FieldDefinition(10, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "Initial state for torrents added to uTorrent")] + [FieldDefinition(10, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(UTorrentState), HelpText = "DownloadClientSettingsInitialStateHelpText")] + [FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")] public int IntialState { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index c87e30fc0..887091891 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.ThingiProvider; @@ -20,6 +21,7 @@ namespace NzbDrone.Core.Download protected readonly IDiskProvider _diskProvider; protected readonly IRemotePathMappingService _remotePathMappingService; protected readonly Logger _logger; + protected readonly ILocalizationService _localizationService; public abstract string Name { get; } @@ -41,12 +43,14 @@ namespace NzbDrone.Core.Download protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, - Logger logger) + Logger logger, + ILocalizationService localizationService) { _configService = configService; _diskProvider = diskProvider; _remotePathMappingService = remotePathMappingService; _logger = logger; + _localizationService = localizationService; } public override string ToString() diff --git a/src/NzbDrone.Core/Download/DownloadClientInfo.cs b/src/NzbDrone.Core/Download/DownloadClientInfo.cs index c97481924..2106b895a 100644 --- a/src/NzbDrone.Core/Download/DownloadClientInfo.cs +++ b/src/NzbDrone.Core/Download/DownloadClientInfo.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Download public bool IsLocalhost { get; set; } public string SortingMode { get; set; } + public bool RemovesCompletedDownloads { get; set; } public List OutputRootFolders { get; set; } } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 757e5da97..7a42b7e18 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Common.Messaging; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Download @@ -24,5 +25,6 @@ namespace NzbDrone.Core.Download public TrackedDownload TrackedDownload { get; set; } public List Languages { get; set; } public bool SkipRedownload { get; set; } + public ReleaseSourceType ReleaseSource { get; set; } } } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 3032f4394..9f07145ca 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -104,6 +104,11 @@ namespace NzbDrone.Core.Download _logger.Trace("Release {0} no longer available on indexer.", remoteEpisode); throw; } + catch (ReleaseBlockedException) + { + _logger.Trace("Release {0} previously added to blocklist, not sending to download client again.", remoteEpisode); + throw; + } catch (DownloadClientRejectedReleaseException) { _logger.Trace("Release {0} rejected by download client, possible duplicate.", remoteEpisode); @@ -128,7 +133,7 @@ namespace NzbDrone.Core.Download episodeGrabbedEvent.DownloadClientId = downloadClient.Definition.Id; episodeGrabbedEvent.DownloadClientName = downloadClient.Definition.Name; - if (!string.IsNullOrWhiteSpace(downloadClientId)) + if (downloadClientId.IsNotNullOrWhiteSpace()) { episodeGrabbedEvent.DownloadId = downloadClientId; } diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 8e07b23fd..3e540a256 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download { @@ -127,7 +129,8 @@ namespace NzbDrone.Core.Download private void PublishDownloadFailedEvent(List historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false) { - var historyItem = historyItems.First(); + var historyItem = historyItems.Last(); + Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource); var downloadFailedEvent = new DownloadFailedEvent { @@ -141,7 +144,8 @@ namespace NzbDrone.Core.Download Data = historyItem.Data, TrackedDownload = trackedDownload, Languages = historyItem.Languages, - SkipRedownload = skipRedownload + SkipRedownload = skipRedownload, + ReleaseSource = releaseSource }; _eventAggregator.PublishEvent(downloadFailedEvent); diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 3cab28655..640e67582 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Download public interface IProcessDownloadDecisions { Task ProcessDecisions(List decisions); + Task ProcessDecision(DownloadDecision decision, int? downloadClientId); } public class ProcessDownloadDecisions : IProcessDownloadDecisions @@ -49,7 +50,6 @@ namespace NzbDrone.Core.Download foreach (var report in prioritizedDecisions) { - var remoteEpisode = report.RemoteEpisode; var downloadProtocol = report.RemoteEpisode.Release.DownloadProtocol; // Skip if already grabbed @@ -71,37 +71,48 @@ namespace NzbDrone.Core.Download continue; } - try - { - _logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteEpisode.Release.Indexer, remoteEpisode.Release.IndexerPriority); - await _downloadService.DownloadReport(remoteEpisode, null); - grabbed.Add(report); - } - catch (ReleaseUnavailableException) - { - _logger.Warn("Failed to download release from indexer, no longer available. " + remoteEpisode); - rejected.Add(report); - } - catch (Exception ex) - { - if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException) - { - _logger.Debug(ex, "Failed to send release to download client, storing until later. " + remoteEpisode); - PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable); + var result = await ProcessDecisionInternal(report); - if (downloadProtocol == DownloadProtocol.Usenet) + switch (result) + { + case ProcessedDecisionResult.Grabbed: { - usenetFailed = true; + grabbed.Add(report); + break; } - else if (downloadProtocol == DownloadProtocol.Torrent) + + case ProcessedDecisionResult.Pending: { - torrentFailed = true; + PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.Delay); + break; + } + + case ProcessedDecisionResult.Rejected: + { + rejected.Add(report); + break; + } + + case ProcessedDecisionResult.Failed: + { + PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable); + + if (downloadProtocol == DownloadProtocol.Usenet) + { + usenetFailed = true; + } + else if (downloadProtocol == DownloadProtocol.Torrent) + { + torrentFailed = true; + } + + break; + } + + case ProcessedDecisionResult.Skipped: + { + break; } - } - else - { - _logger.Warn(ex, "Couldn't add report to download queue. " + remoteEpisode); - } } } @@ -113,10 +124,44 @@ namespace NzbDrone.Core.Download return new ProcessedDecisions(grabbed, pending, rejected); } + public async Task ProcessDecision(DownloadDecision decision, int? downloadClientId) + { + if (decision == null) + { + return ProcessedDecisionResult.Skipped; + } + + if (!IsQualifiedReport(decision)) + { + return ProcessedDecisionResult.Rejected; + } + + if (decision.TemporarilyRejected) + { + _pendingReleaseService.Add(decision, PendingReleaseReason.Delay); + + return ProcessedDecisionResult.Pending; + } + + var result = await ProcessDecisionInternal(decision, downloadClientId); + + if (result == ProcessedDecisionResult.Failed) + { + _pendingReleaseService.Add(decision, PendingReleaseReason.DownloadClientUnavailable); + } + + return result; + } + internal List GetQualifiedReports(IEnumerable decisions) + { + return decisions.Where(IsQualifiedReport).ToList(); + } + + internal bool IsQualifiedReport(DownloadDecision decision) { // Process both approved and temporarily rejected - return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteEpisode.Episodes.Any()).ToList(); + return (decision.Approved || decision.TemporarilyRejected) && decision.RemoteEpisode.Episodes.Any(); } private bool IsEpisodeProcessed(List decisions, DownloadDecision report) @@ -147,5 +192,38 @@ namespace NzbDrone.Core.Download queue.Add(Tuple.Create(report, reason)); pending.Add(report); } + + private async Task ProcessDecisionInternal(DownloadDecision decision, int? downloadClientId = null) + { + var remoteEpisode = decision.RemoteEpisode; + + try + { + _logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteEpisode.Release.Indexer, remoteEpisode.Release.IndexerPriority); + await _downloadService.DownloadReport(remoteEpisode, downloadClientId); + + return ProcessedDecisionResult.Grabbed; + } + catch (ReleaseUnavailableException) + { + _logger.Warn("Failed to download release from indexer, no longer available. " + remoteEpisode); + return ProcessedDecisionResult.Rejected; + } + catch (Exception ex) + { + if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException) + { + _logger.Debug(ex, + "Failed to send release to download client, storing until later. " + remoteEpisode); + + return ProcessedDecisionResult.Failed; + } + else + { + _logger.Warn(ex, "Couldn't add report to download queue. " + remoteEpisode); + return ProcessedDecisionResult.Skipped; + } + } + } } } diff --git a/src/NzbDrone.Core/Download/ProcessedDecisionResult.cs b/src/NzbDrone.Core/Download/ProcessedDecisionResult.cs new file mode 100644 index 000000000..d1f27ed49 --- /dev/null +++ b/src/NzbDrone.Core/Download/ProcessedDecisionResult.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Core.Download +{ + public enum ProcessedDecisionResult + { + Grabbed, + Pending, + Rejected, + Failed, + Skipped + } +} diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index 20c19e15d..4a431de6c 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Messaging; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download @@ -42,6 +43,12 @@ namespace NzbDrone.Core.Download return; } + if (message.ReleaseSource == ReleaseSourceType.InteractiveSearch && !_configService.AutoRedownloadFailedFromInteractiveSearch) + { + _logger.Debug("Auto redownloading failed episodes from interactive search is disabled"); + return; + } + if (message.EpisodeIds.Count == 1) { _logger.Debug("Failed download only contains one episode, searching again"); diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index a1e921872..7f93b3395 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -6,9 +6,11 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -21,17 +23,21 @@ namespace NzbDrone.Core.Download where TSettings : IProviderConfig, new() { protected readonly IHttpClient _httpClient; + private readonly IBlocklistService _blocklistService; protected readonly ITorrentFileInfoReader _torrentFileInfoReader; protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader, - IHttpClient httpClient, - IConfigService configService, - IDiskProvider diskProvider, - IRemotePathMappingService remotePathMappingService, - Logger logger) - : base(configService, diskProvider, remotePathMappingService, logger) + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + ILocalizationService localizationService, + IBlocklistService blocklistService, + Logger logger) + : base(configService, diskProvider, remotePathMappingService, logger, localizationService) { _httpClient = httpClient; + _blocklistService = blocklistService; _torrentFileInfoReader = torrentFileInfoReader; } @@ -86,7 +92,7 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteEpisode, indexer, magnetUrl); } catch (NotSupportedException ex) { @@ -100,7 +106,7 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteEpisode, indexer, magnetUrl); } catch (NotSupportedException ex) { @@ -147,7 +153,7 @@ namespace NzbDrone.Core.Download { if (locationHeader.StartsWith("magnet:")) { - return DownloadFromMagnetUrl(remoteEpisode, locationHeader); + return DownloadFromMagnetUrl(remoteEpisode, indexer, locationHeader); } request.Url += new HttpUri(locationHeader); @@ -190,6 +196,9 @@ namespace NzbDrone.Core.Download var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteEpisode.Release.Title)); var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile); + + EnsureReleaseIsNotBlocklisted(remoteEpisode, indexer, hash); + var actualHash = AddFromTorrentFile(remoteEpisode, hash, filename, torrentFile); if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) @@ -203,7 +212,7 @@ namespace NzbDrone.Core.Download return actualHash; } - private string DownloadFromMagnetUrl(RemoteEpisode remoteEpisode, string magnetUrl) + private string DownloadFromMagnetUrl(RemoteEpisode remoteEpisode, IIndexer indexer, string magnetUrl) { string hash = null; string actualHash = null; @@ -221,6 +230,8 @@ namespace NzbDrone.Core.Download if (hash != null) { + EnsureReleaseIsNotBlocklisted(remoteEpisode, indexer, hash); + actualHash = AddFromMagnetLink(remoteEpisode, hash, magnetUrl); } @@ -234,5 +245,30 @@ namespace NzbDrone.Core.Download return actualHash; } + + private void EnsureReleaseIsNotBlocklisted(RemoteEpisode remoteEpisode, IIndexer indexer, string hash) + { + var indexerSettings = indexer?.Definition?.Settings as ITorrentIndexerSettings; + var torrentInfo = remoteEpisode.Release as TorrentInfo; + var torrentInfoHash = torrentInfo?.InfoHash; + + // If the release didn't come from an interactive search, + // the hash wasn't known during processing and the + // indexer is configured to reject blocklisted releases + // during grab check if it's already been blocklisted. + + if (torrentInfo != null && torrentInfoHash.IsNullOrWhiteSpace()) + { + // If the hash isn't known from parsing we set it here so it can be used for blocklisting. + torrentInfo.InfoHash = hash; + + if (remoteEpisode.ReleaseSource != ReleaseSourceType.InteractiveSearch && + indexerSettings?.RejectBlocklistedTorrentHashesWhileGrabbing == true && + _blocklistService.BlocklistedTorrentHash(remoteEpisode.Series.Id, hash)) + { + throw new ReleaseBlockedException(remoteEpisode.Release, "Release previously added to blocklist"); + } + } + } } } diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index 319f3ad5a..14d87ef09 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Localization; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; @@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, IValidateNzbs nzbValidationService, - Logger logger) - : base(configService, diskProvider, remotePathMappingService, logger) + Logger logger, + ILocalizationService localizationService) + : base(configService, diskProvider, remotePathMappingService, logger, localizationService) { _httpClient = httpClient; _nzbValidationService = nzbValidationService; diff --git a/src/NzbDrone.Core/Exceptions/ReleaseBlockedException.cs b/src/NzbDrone.Core/Exceptions/ReleaseBlockedException.cs new file mode 100644 index 000000000..73d8b8f8d --- /dev/null +++ b/src/NzbDrone.Core/Exceptions/ReleaseBlockedException.cs @@ -0,0 +1,28 @@ +using System; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Exceptions +{ + public class ReleaseBlockedException : ReleaseDownloadException + { + public ReleaseBlockedException(ReleaseInfo release, string message, params object[] args) + : base(release, message, args) + { + } + + public ReleaseBlockedException(ReleaseInfo release, string message) + : base(release, message) + { + } + + public ReleaseBlockedException(ReleaseInfo release, string message, Exception innerException, params object[] args) + : base(release, message, innerException, args) + { + } + + public ReleaseBlockedException(ReleaseInfo release, string message, Exception innerException) + : base(release, message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 5cfe4944f..ae17b44c6 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -2,45 +2,33 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras { - public class ExistingExtraFileService : IHandle + public interface IExistingExtraFiles + { + List ImportExtraFiles(Series series, List possibleExtraFiles); + } + + public class ExistingExtraFileService : IExistingExtraFiles, IHandle { - private readonly IDiskProvider _diskProvider; - private readonly IDiskScanService _diskScanService; private readonly List _existingExtraFileImporters; private readonly Logger _logger; - public ExistingExtraFileService(IDiskProvider diskProvider, - IDiskScanService diskScanService, - IEnumerable existingExtraFileImporters, + public ExistingExtraFileService(IEnumerable existingExtraFileImporters, Logger logger) { - _diskProvider = diskProvider; - _diskScanService = diskScanService; _existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList(); _logger = logger; } - public void Handle(SeriesScannedEvent message) + public List ImportExtraFiles(Series series, List possibleExtraFiles) { - var series = message.Series; - - if (!_diskProvider.FolderExists(series.Path)) - { - return; - } - _logger.Debug("Looking for existing extra files in {0}", series.Path); - var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path); - var possibleExtraFiles = _diskScanService.FilterPaths(series.Path, filesOnDisk); - var importedFiles = new List(); foreach (var existingExtraFileImporter in _existingExtraFileImporters) @@ -50,6 +38,15 @@ namespace NzbDrone.Core.Extras importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); } + return importedFiles; + } + + public void Handle(SeriesScannedEvent message) + { + var series = message.Series; + var possibleExtraFiles = message.PossibleExtraFiles; + var importedFiles = ImportExtraFiles(series, possibleExtraFiles); + _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); } } diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index eca970898..b29fa6d17 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Extras { public interface IExtraService { + void MoveFilesAfterRename(Series series, EpisodeFile episodeFile); void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); } @@ -139,6 +140,16 @@ namespace NzbDrone.Core.Extras } } + public void MoveFilesAfterRename(Series series, EpisodeFile episodeFile) + { + var episodeFiles = new List { episodeFile }; + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.MoveFilesAfterRename(series, episodeFiles); + } + } + public void Handle(SeriesRenamedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 765769be6..38982b579 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -198,6 +198,12 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd"))); } + // Add support for Jellyfin's "enddate" tag + if (series.Status == SeriesStatusType.Ended && series.LastAired.HasValue) + { + tvShow.Add(new XElement("enddate", series.LastAired.Value.ToString("yyyy-MM-dd"))); + } + tvShow.Add(new XElement("studio", series.Network)); foreach (var actor in series.Actors) diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index d88c0398d..1cf253e81 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -443,6 +443,7 @@ namespace NzbDrone.Core.Extras.Metadata private void DownloadImage(Series series, ImageFileResult image) { var fullPath = Path.Combine(series.Path, image.RelativePath); + var downloaded = true; try { @@ -450,12 +451,19 @@ namespace NzbDrone.Core.Extras.Metadata { _httpClient.DownloadFile(image.Url, fullPath); } - else + else if (_diskProvider.FileExists(image.Url)) { _diskProvider.CopyFile(image.Url, fullPath); } + else + { + downloaded = false; + } - _mediaFileAttributeService.SetFilePermissions(fullPath); + if (downloaded) + { + _mediaFileAttributeService.SetFilePermissions(fullPath); + } } catch (HttpException ex) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs index fa284cb31..d3198b111 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ApiKeyValidationCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; @@ -28,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { _logger.Warn("Please update your API key to be at least {0} characters long. You can do this via settings or the config file", MinimumLength); - return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage"), MinimumLength), "#invalid-api-key"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("ApiKeyValidationHealthCheckMessage", new Dictionary { { "length", MinimumLength } }), "#invalid-api-key"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs index e10ee665f..5d9cd4b7f 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Download; @@ -44,7 +45,11 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Error, - $"{string.Format(_localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateWithHealthCheckMessage"), downloadClient.Definition.Name)} {ex.Message}", + _localizationService.GetLocalizedString("DownloadClientCheckUnableToCommunicateWithHealthCheckMessage", new Dictionary + { + { "downloadClientName", downloadClient.Definition.Name }, + { "errorMessage", ex.Message } + }), "#unable-to-communicate-with-download-client"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheck.cs new file mode 100644 index 000000000..1e1be0a26 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRemovesCompletedDownloadsCheck.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Localization; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ModelEvent))] + [CheckOn(typeof(ModelEvent))] + + public class DownloadClientRemovesCompletedDownloadsCheck : HealthCheckBase, IProvideHealthCheck + { + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly Logger _logger; + + public DownloadClientRemovesCompletedDownloadsCheck(IProvideDownloadClient downloadClientProvider, + Logger logger, + ILocalizationService localizationService) + : base(localizationService) + { + _downloadClientProvider = downloadClientProvider; + _logger = logger; + } + + public override HealthCheck Check() + { + var clients = _downloadClientProvider.GetDownloadClients(true); + + foreach (var client in clients) + { + try + { + var clientName = client.Definition.Name; + var status = client.GetStatus(); + + if (status.RemovesCompletedDownloads) + { + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + _localizationService.GetLocalizedString("DownloadClientRemovesCompletedDownloadsHealthCheckMessage", new Dictionary + { + { "downloadClientName", clientName } + }), + "#download-client-removes-completed-downloads"); + } + } + catch (DownloadClientException ex) + { + _logger.Debug(ex, "Unable to communicate with {0}", client.Definition.Name); + } + catch (Exception ex) + { + _logger.Error(ex, "Unknown error occurred in DownloadClientHistoryRetentionCheck HealthCheck"); + } + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs index 4e25fdc70..4f0a6de05 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using NLog; @@ -53,7 +54,11 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("DownloadClientRootFolderHealthCheckMessage"), client.Definition.Name, folder.FullPath), + _localizationService.GetLocalizedString("DownloadClientRootFolderHealthCheckMessage", new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath } + }), "#downloads-in-root-folder"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientSortingCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientSortingCheck.cs index 37e7cc5fa..d6474bcb4 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientSortingCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientSortingCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; @@ -45,7 +46,11 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("DownloadClientSortingHealthCheckMessage"), clientName, status.SortingMode), + _localizationService.GetLocalizedString("DownloadClientSortingHealthCheckMessage", new Dictionary + { + { "downloadClientName", clientName }, + { "sortingMode", status.SortingMode } + }), "#download-folder-and-library-folder-not-different-folders"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs index acd0d5f9f..01e190475 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; @@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("DownloadClientStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("DownloadClientStatusSingleClientHealthCheckMessage", new Dictionary + { + { "downloadClientNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#download-clients-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs index 15a9675f8..fc4ee7826 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs @@ -54,13 +54,19 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("ImportListRootFolderMissingRootHealthCheckMessage"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), + _localizationService.GetLocalizedString("ImportListRootFolderMissingRootHealthCheckMessage", new Dictionary + { + { "rootFolderInfo", FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value) } + }), "#import-list-missing-root-folder"); } return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("ImportListRootFolderMultipleMissingRootsHealthCheckMessage"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value)))), + _localizationService.GetLocalizedString("ImportListRootFolderMultipleMissingRootsHealthCheckMessage", new Dictionary + { + { "rootFoldersInfo", string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value))) } + }), "#import-list-missing-root-folder"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportListStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportListStatusCheck.cs index 17c958374..e11efc27c 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportListStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.ImportLists; @@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("ImportListStatusUnavailableHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name))), + _localizationService.GetLocalizedString("ImportListStatusUnavailableHealthCheckMessage", new Dictionary + { + { "importListNames", string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name)) } + }), "#import-lists-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs index 94592426e..a7b17dbd8 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerDownloadClientCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; @@ -35,7 +36,10 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage"), string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray())), + _localizationService.GetLocalizedString("IndexerDownloadClientHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", invalidIndexers.Select(v => v.Name).ToArray()) } + }), "#invalid-indexer-download-client-setting"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs index 366404892..f01a59437 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -41,7 +42,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerJackettAllHealthCheckMessage"), string.Join(", ", jackettAllProviders.Select(i => i.Name))), + _localizationService.GetLocalizedString("IndexerJackettAllHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", jackettAllProviders.Select(i => i.Name)) } + }), "#jackett-all-endpoint-used"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs index f0c71ba38..f419cf205 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerLongTermStatusCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -48,7 +49,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("IndexerLongTermStatusUnavailableHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#indexers-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs index f3afaeba6..fa861fb94 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerStatusCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -48,7 +49,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("IndexerStatusUnavailableHealthCheckMessage", new Dictionary + { + { "indexerNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#indexers-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs index e3f3395c3..46f1ea103 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MountCheck.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - $"{_localizationService.GetLocalizedString("MountHealthCheckMessage")}{string.Join(", ", mounts.Select(m => m.Name))}", + $"{_localizationService.GetLocalizedString("MountSeriesHealthCheckMessage")}{string.Join(", ", mounts.Select(m => m.Name))}", "#series-mount-ro"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs index c9b5e2561..daf5ee725 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/NotificationStatusCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; @@ -45,7 +46,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Warning, - string.Format(_localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage"), string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), + _localizationService.GetLocalizedString("NotificationStatusSingleClientHealthCheckMessage", new Dictionary + { + { "notificationNames", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name)) } + }), "#notifications-are-unavailable-due-to-failures"); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs index 3d05a75ea..f09033370 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ProxyCheck.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Net; using NLog; @@ -42,7 +43,10 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage"), _configService.ProxyHostname), + _localizationService.GetLocalizedString("ProxyResolveIpHealthCheckMessage", new Dictionary + { + { "proxyHostName", _configService.ProxyHostname } + }), "#proxy-failed-resolve-ip"); } @@ -61,7 +65,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("ProxyBadRequestHealthCheckMessage"), response.StatusCode), + _localizationService.GetLocalizedString("ProxyBadRequestHealthCheckMessage", new Dictionary + { + { "statusCode", response.StatusCode } + }), "#proxy-failed-test"); } } @@ -71,7 +78,10 @@ namespace NzbDrone.Core.HealthCheck.Checks return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage"), request.Url), + _localizationService.GetLocalizedString("ProxyFailedToTestHealthCheckMessage", new Dictionary + { + { "url", request.Url } + }), "#proxy-failed-test"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RecyclingBinCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RecyclingBinCheck.cs index 86d3949ac..ad30a74f7 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RecyclingBinCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RecyclingBinCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -33,7 +34,10 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheckMessage"), recycleBin), + _localizationService.GetLocalizedString("RecycleBinUnableToWriteHealthCheckMessage", new Dictionary + { + { "path", recycleBin } + }), "#cannot-write-recycle-bin"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs index 2377272d0..82a7f1d35 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RemotePathMappingCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using NLog; @@ -68,30 +69,92 @@ namespace NzbDrone.Core.HealthCheck.Checks { if (!status.IsLocalhost) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingWrongOSPathHealthCheckMessage"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingWrongOSPathHealthCheckMessage", new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath }, + { "osName", _osInfo.Name } + }), + "#bad-remote-path-mapping"); } if (_osInfo.IsDocker) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingBadDockerPathHealthCheckMessage"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#docker-bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingBadDockerPathHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath }, + { "osName", _osInfo.Name } + }), + "#docker-bad-remote-path-mapping"); } - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingLocalWrongOSPathHealthCheckMessage"), client.Definition.Name, folder.FullPath, _osInfo.Name), "#bad-download-client-settings"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingLocalWrongOSPathHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath }, + { "osName", _osInfo.Name } + }), + "#bad-download-client-settings"); } if (!_diskProvider.FolderExists(folder.FullPath)) { if (_osInfo.IsDocker) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingDockerFolderMissingHealthCheckMessage"), client.Definition.Name, folder.FullPath), "#docker-bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingDockerFolderMissingHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath } + }), + "#docker-bad-remote-path-mapping"); } if (!status.IsLocalhost) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingLocalFolderMissingHealthCheckMessage"), client.Definition.Name, folder.FullPath), "#bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingLocalFolderMissingHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath } + }), + "#bad-remote-path-mapping"); } - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingGenericPermissionsHealthCheckMessage"), client.Definition.Name, folder.FullPath), "#permissions-error"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingGenericPermissionsHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", folder.FullPath } + }), + "#permissions-error"); } } } @@ -129,12 +192,28 @@ namespace NzbDrone.Core.HealthCheck.Checks if (_diskProvider.FileExists(episodePath)) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingDownloadPermissionsHealthCheckMessage"), episodePath), "#permissions-error"); + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage", + new Dictionary + { + { "path", episodePath } + }), + "#permissions-error"); } // If the file doesn't exist but EpisodeInfo is not null then the message is coming from // ImportApprovedEpisodes and the file must have been removed part way through processing - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFileRemovedHealthCheckMessage"), episodePath), "#remote-path-file-removed"); + return new HealthCheck(GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFileRemovedHealthCheckMessage", + new Dictionary + { + { "path", episodePath } + }), + "#remote-path-file-removed"); } // If the previous case did not match then the failure occured in DownloadedEpisodeImportService, @@ -156,42 +235,118 @@ namespace NzbDrone.Core.HealthCheck.Checks // that the user realises something is wrong. if (dlpath.IsNullOrWhiteSpace()) { - return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("RemotePathMappingImportFailedHealthCheckMessage"), "#remote-path-import-failed"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString("RemotePathMappingImportEpisodeFailedHealthCheckMessage"), + "#remote-path-import-failed"); } if (!dlpath.IsPathValid(PathValidationType.CurrentOs)) { if (!status.IsLocalhost) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFilesWrongOSPathHealthCheckMessage"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFilesWrongOSPathHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath }, + { "osName", _osInfo.Name } + }), + "#bad-remote-path-mapping"); } if (_osInfo.IsDocker) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFilesBadDockerPathHealthCheckMessage"), client.Definition.Name, dlpath, _osInfo.Name), "#docker-bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFilesBadDockerPathHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath }, + { "osName", _osInfo.Name } + }), + "#docker-bad-remote-path-mapping"); } - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage"), client.Definition.Name, dlpath, _osInfo.Name), "#bad-download-client-settings"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath }, + { "osName", _osInfo.Name } + }), + "#bad-download-client-settings"); } if (_diskProvider.FolderExists(dlpath)) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFolderPermissionsHealthCheckMessage"), dlpath), "#permissions-error"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFolderPermissionsHealthCheckMessage", + new Dictionary + { + { "path", dlpath } + }), + "#permissions-error"); } // if it's a remote client/docker, likely missing path mappings if (_osInfo.IsDocker) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFolderPermissionsHealthCheckMessage"), client.Definition.Name, dlpath), "#docker-bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFolderPermissionsHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath } + }), + "#docker-bad-remote-path-mapping"); } if (!status.IsLocalhost) { - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingRemoteDownloadClientHealthCheckMessage"), client.Definition.Name, dlpath), "#bad-remote-path-mapping"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingRemoteDownloadClientHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath }, + { "osName", _osInfo.Name } + }), "#bad-remote-path-mapping"); } // path mappings shouldn't be needed locally so probably a permissions issue - return new HealthCheck(GetType(), HealthCheckResult.Error, string.Format(_localizationService.GetLocalizedString("RemotePathMappingFilesGenericPermissionsHealthCheckMessage"), client.Definition.Name, dlpath), "#permissions-error"); + return new HealthCheck( + GetType(), + HealthCheckResult.Error, + _localizationService.GetLocalizedString( + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage", + new Dictionary + { + { "downloadClientName", client.Definition.Name }, + { "path", dlpath } + }), + "#permissions-error"); } catch (DownloadClientException ex) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs index 24eb63002..363d3010d 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; @@ -7,7 +8,7 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(SeriesUpdatedEvent))] - [CheckOn(typeof(SeriesDeletedEvent), CheckOnCondition.FailedOnly)] + [CheckOn(typeof(SeriesDeletedEvent))] [CheckOn(typeof(SeriesRefreshCompleteEvent))] public class RemovedSeriesCheck : HealthCheckBase, ICheckOnCondition, ICheckOnCondition { @@ -34,13 +35,19 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("RemovedSeriesSingleRemovedHealthCheckMessage"), seriesText), + _localizationService.GetLocalizedString("RemovedSeriesSingleRemovedHealthCheckMessage", new Dictionary + { + { "series", seriesText } + }), "#series-removed-from-thetvdb"); } return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("RemovedSeriesMultipleRemovedHealthCheckMessage"), seriesText), + _localizationService.GetLocalizedString("RemovedSeriesMultipleRemovedHealthCheckMessage", new Dictionary + { + { "series", seriesText } + }), "#series-removed-from-thetvdb"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs index c730be8da..0b0016f52 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -42,13 +43,23 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("RootFolderMissingHealthCheckMessage"), missingRootFolders.First()), + _localizationService.GetLocalizedString( + "RootFolderMissingHealthCheckMessage", + new Dictionary + { + { "rootFolderPath", missingRootFolders.First() } + }), "#missing-root-folder"); } return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("RootFolderMultipleMissingHealthCheckMessage"), string.Join(" | ", missingRootFolders)), + _localizationService.GetLocalizedString( + "RootFolderMultipleMissingHealthCheckMessage", + new Dictionary + { + { "rootFolderPaths", string.Join(" | ", missingRootFolders) } + }), "#missing-root-folder"); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index e4e0690a4..09b3eea1f 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -47,7 +48,12 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateStartupTranslocationHealthCheckMessage"), startupFolder), + _localizationService.GetLocalizedString( + "UpdateStartupTranslocationHealthCheckMessage", + new Dictionary + { + { "startupFolder", startupFolder } + }), "#cannot-install-update-because-startup-folder-is-in-an-app-translocation-folder."); } @@ -55,7 +61,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateStartupNotWritableHealthCheckMessage"), startupFolder, Environment.UserName), + _localizationService.GetLocalizedString( + "UpdateStartupNotWritableHealthCheckMessage", + new Dictionary + { + { "startupFolder", startupFolder }, + { "userName", Environment.UserName } + }), "#cannot-install-update-because-startup-folder-is-not-writable-by-the-user"); } @@ -63,7 +75,13 @@ namespace NzbDrone.Core.HealthCheck.Checks { return new HealthCheck(GetType(), HealthCheckResult.Error, - string.Format(_localizationService.GetLocalizedString("UpdateUINotWritableHealthCheckMessage"), uiFolder, Environment.UserName), + _localizationService.GetLocalizedString( + "UpdateUiNotWritableHealthCheckMessage", + new Dictionary + { + { "uiFolder", uiFolder }, + { "userName", Environment.UserName } + }), "#cannot-install-update-because-ui-folder-is-not-writable-by-the-user"); } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index 31aa9c199..ef906ee99 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Messaging; @@ -28,6 +29,7 @@ namespace NzbDrone.Core.HealthCheck private readonly IProvideHealthCheck[] _scheduledHealthChecks; private readonly Dictionary _eventDrivenHealthChecks; private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; private readonly ICached _healthCheckResults; private readonly HashSet _pendingHealthChecks; @@ -40,10 +42,12 @@ namespace NzbDrone.Core.HealthCheck IEventAggregator eventAggregator, ICacheManager cacheManager, IDebounceManager debounceManager, - IRuntimeInfo runtimeInfo) + IRuntimeInfo runtimeInfo, + Logger logger) { _healthChecks = healthChecks.ToArray(); _eventAggregator = eventAggregator; + _logger = logger; _healthCheckResults = cacheManager.GetCache(GetType()); _pendingHealthChecks = new HashSet(); @@ -88,7 +92,14 @@ namespace NzbDrone.Core.HealthCheck try { - var results = healthChecks.Select(c => c.Check()) + var results = healthChecks.Select(c => + { + _logger.Trace("Check health -> {0}", c.GetType().Name); + var result = c.Check(); + _logger.Trace("Check health <- {0}", c.GetType().Name); + + return result; + }) .ToList(); foreach (var result in results) diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index c5546147a..7f3da4064 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.History List FindDownloadHistory(int idSeriesId, QualityModel quality); void DeleteForSeries(List seriesIds); List Since(DateTime date, EpisodeHistoryEventType? eventType); + PagingSpec GetPaged(PagingSpec pagingSpec, int[] languages, int[] qualities); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -101,18 +102,6 @@ namespace NzbDrone.Core.History Delete(c => seriesIds.Contains(c.SeriesId)); } - protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType) - .Join((h, a) => h.SeriesId == a.Id) - .Join((h, a) => h.EpisodeId == a.Id); - - protected override IEnumerable PagedQuery(SqlBuilder builder) => - _database.QueryJoined(builder, (history, series, episode) => - { - history.Series = series; - history.Episode = episode; - return history; - }); - public List Since(DateTime date, EpisodeHistoryEventType? eventType) { var builder = Builder() @@ -132,5 +121,76 @@ namespace NzbDrone.Core.History return history; }).OrderBy(h => h.Date).ToList(); } + + public PagingSpec GetPaged(PagingSpec pagingSpec, int[] languages, int[] qualities) + { + pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery); + + var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\""; + pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(EpisodeHistory)), pagingSpec, countTemplate); + + return pagingSpec; + } + + private SqlBuilder PagedBuilder(PagingSpec pagingSpec, int[] languages, int[] qualities) + { + var builder = Builder() + .Join((h, a) => h.SeriesId == a.Id) + .Join((h, a) => h.EpisodeId == a.Id); + + AddFilters(builder, pagingSpec); + + if (languages is { Length: > 0 }) + { + builder.Where($"({BuildLanguageWhereClause(languages)})"); + } + + if (qualities is { Length: > 0 }) + { + builder.Where($"({BuildQualityWhereClause(qualities)})"); + } + + return builder; + } + + protected override IEnumerable PagedQuery(SqlBuilder builder) => + _database.QueryJoined(builder, (history, series, episode) => + { + history.Series = series; + history.Episode = episode; + return history; + }); + + private string BuildLanguageWhereClause(int[] languages) + { + var clauses = new List(); + + foreach (var language in languages) + { + // There are 4 different types of values we should see: + // - Not the last value in the array + // - When it's the last value in the array and on different OSes + // - When it was converted from a single language + + clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language},%]'"); + clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(13) || '%]'"); + clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(10) || '%]'"); + clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[{language}]'"); + } + + return $"({string.Join(" OR ", clauses)})"; + } + + private string BuildQualityWhereClause(int[] qualities) + { + var clauses = new List(); + + foreach (var quality in qualities) + { + clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'"); + } + + return $"({string.Join(" OR ", clauses)})"; + } } } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 1f1e9a668..813329908 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.History { public interface IHistoryService { - PagingSpec Paged(PagingSpec pagingSpec); + PagingSpec Paged(PagingSpec pagingSpec, int[] languages, int[] qualities); EpisodeHistory MostRecentForEpisode(int episodeId); List FindByEpisodeId(int episodeId); EpisodeHistory MostRecentForDownloadId(string downloadId); @@ -47,9 +47,9 @@ namespace NzbDrone.Core.History _logger = logger; } - public PagingSpec Paged(PagingSpec pagingSpec) + public PagingSpec Paged(PagingSpec pagingSpec, int[] languages, int[] qualities) { - return _historyRepository.GetPaged(pagingSpec); + return _historyRepository.GetPaged(pagingSpec, languages, qualities); } public EpisodeHistory MostRecentForEpisode(int episodeId) @@ -175,9 +175,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseHash", message.Episode.ParsedEpisodeInfo.ReleaseHash); } - var torrentRelease = message.Episode.Release as TorrentInfo; - - if (torrentRelease != null) + if (message.Episode.Release is TorrentInfo torrentRelease) { history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash); } diff --git a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs index 93f71e6d8..29157be63 100644 --- a/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs +++ b/src/NzbDrone.Core/ImportLists/Custom/CustomImport.cs @@ -46,8 +46,10 @@ namespace NzbDrone.Core.ImportLists.Custom _importListStatusService.RecordSuccess(Definition.Id); } - catch + catch (Exception ex) { + _logger.Debug(ex, "Failed to fetch data for list {0} ({1})", Definition.Name, Name); + _importListStatusService.RecordFailure(Definition.Id); } diff --git a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs index 48fb511d9..31b99c23c 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListDefinition.cs @@ -7,7 +7,9 @@ namespace NzbDrone.Core.ImportLists public class ImportListDefinition : ProviderDefinition { public bool EnableAutomaticAdd { get; set; } + public bool SearchForMissingEpisodes { get; set; } public MonitorTypes ShouldMonitor { get; set; } + public NewItemMonitorTypes MonitorNewItems { get; set; } public int QualityProfileId { get; set; } public SeriesTypes SeriesType { get; set; } public bool SeasonFolder { get; set; } diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index e573e10d0..12109a94a 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -90,7 +90,7 @@ namespace NzbDrone.Core.ImportLists var importList = importLists.Single(x => x.Id == item.ImportListId); - // Map by IMDbId if we have it + // Map by IMDb ID if we have it if (item.TvdbId <= 0 && item.ImdbId.IsNotNullOrWhiteSpace()) { var mappedSeries = _seriesSearchService.SearchForNewSeriesByImdbId(item.ImdbId) @@ -103,19 +103,36 @@ namespace NzbDrone.Core.ImportLists } } - // Map by AniListId if we have it - if (item.TvdbId <= 0 && item.AniListId > 0) + // Map by TMDb ID if we have it + if (item.TvdbId <= 0 && item.TmdbId > 0) { - var mappedSeries = _seriesSearchService.SearchForNewSeriesByAniListId(item.AniListId) + var mappedSeries = _seriesSearchService.SearchForNewSeriesByTmdbId(item.TmdbId) .FirstOrDefault(); if (mappedSeries != null) { item.TvdbId = mappedSeries.TvdbId; - item.Title = mappedSeries.Title; + item.Title = mappedSeries?.Title; } } + // Map by AniList ID if we have it + if (item.TvdbId <= 0 && item.AniListId > 0) + { + var mappedSeries = _seriesSearchService.SearchForNewSeriesByAniListId(item.AniListId) + .FirstOrDefault(); + + if (mappedSeries == null) + { + _logger.Debug("Rejected, unable to find matching TVDB ID for Anilist ID: {0} [{1}]", item.AniListId, item.Title); + + continue; + } + + item.TvdbId = mappedSeries.TvdbId; + item.Title = mappedSeries.Title; + } + // Map TVDb if we only have a series name if (item.TvdbId <= 0 && item.Title.IsNotNullOrWhiteSpace()) { @@ -156,6 +173,7 @@ namespace NzbDrone.Core.ImportLists Title = item.Title, Year = item.Year, Monitored = monitored, + MonitorNewItems = importList.MonitorNewItems, RootFolderPath = importList.RootFolderPath, QualityProfileId = importList.QualityProfileId, SeriesType = importList.SeriesType, @@ -163,7 +181,7 @@ namespace NzbDrone.Core.ImportLists Tags = importList.Tags, AddOptions = new AddSeriesOptions { - SearchForMissingEpisodes = monitored, + SearchForMissingEpisodes = importList.SearchForMissingEpisodes, Monitor = importList.ShouldMonitor } }); diff --git a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs index cad233559..2dda602cb 100644 --- a/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs +++ b/src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs @@ -1,5 +1,7 @@ +using System.Text.RegularExpressions; using System.Xml.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser.Model; @@ -8,6 +10,8 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex { public class PlexRssImportParser : RssImportBaseParser { + private static readonly Regex ImdbIdRegex = new (@"(tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + public PlexRssImportParser(Logger logger) : base(logger) { @@ -29,17 +33,42 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex var guid = item.TryGetValue("guid", string.Empty); - if (int.TryParse(guid.Replace("tvdb://", ""), out var tvdbId)) + if (guid.IsNotNullOrWhiteSpace()) { - info.TvdbId = tvdbId; + if (guid.StartsWith("imdb://")) + { + info.ImdbId = ParseImdbId(guid.Replace("imdb://", "")); + } + + if (int.TryParse(guid.Replace("tvdb://", ""), out var tvdbId)) + { + info.TvdbId = tvdbId; + } + + if (int.TryParse(guid.Replace("tmdb://", ""), out var tmdbId)) + { + info.TmdbId = tmdbId; + } } - if (info.TvdbId == 0) + if (info.ImdbId.IsNullOrWhiteSpace() && info.TvdbId == 0 && info.TmdbId == 0) { - throw new UnsupportedFeedException("Each item in the RSS feed must have a guid element with a TVDB ID"); + throw new UnsupportedFeedException("Each item in the RSS feed must have a guid element with a IMDB ID, TVDB ID or TMDB ID"); } return info; } + + private static string ParseImdbId(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return null; + } + + var match = ImdbIdRegex.Match(value); + + return match.Success ? match.Groups[1].Value : null; + } } } diff --git a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs index 7973eac9a..33d701781 100644 --- a/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs +++ b/src/NzbDrone.Core/ImportLists/Sonarr/SonarrImport.cs @@ -68,8 +68,10 @@ namespace NzbDrone.Core.ImportLists.Sonarr _importListStatusService.RecordSuccess(Definition.Id); } - catch + catch (Exception ex) { + _logger.Debug(ex, "Failed to fetch data for list {0} ({1})", Definition.Name, Name); + _importListStatusService.RecordFailure(Definition.Id); } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs index 006e1f7ef..ce1e9aac9 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs @@ -26,24 +26,24 @@ namespace NzbDrone.Core.ImportLists.Trakt.Popular return listItems; } - var jsonResponse = new List(); + var traktSeries = new List(); if (_settings.TraktListType == (int)TraktPopularListType.Popular) { - jsonResponse = STJson.Deserialize>(_importResponse.Content); + traktSeries = STJson.Deserialize>(_importResponse.Content); } else { - jsonResponse = STJson.Deserialize>(_importResponse.Content).SelectList(c => c.Show); + traktSeries = STJson.Deserialize>(_importResponse.Content).SelectList(c => c.Show); } - // no movies were return - if (jsonResponse == null) + // no series were returned + if (traktSeries == null) { return listItems; } - foreach (var series in jsonResponse) + foreach (var series in traktSeries) { listItems.AddIfNotNull(new ImportListItemInfo() { diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs index dcaf98f20..43318c1bc 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace NzbDrone.Core.ImportLists.Trakt { @@ -16,6 +17,8 @@ namespace NzbDrone.Core.ImportLists.Trakt public string Title { get; set; } public int? Year { get; set; } public TraktSeriesIdsResource Ids { get; set; } + [JsonPropertyName("aired_episodes")] + public int AiredEpisodes { get; set; } } public class TraktResponse @@ -23,13 +26,29 @@ namespace NzbDrone.Core.ImportLists.Trakt public TraktSeriesResource Show { get; set; } } + public class TraktWatchedEpisodeResource + { + public int? Plays { get; set; } + } + + public class TraktWatchedSeasonResource + { + public int? Number { get; set; } + public List Episodes { get; set; } + } + + public class TraktWatchedResponse : TraktResponse + { + public List Seasons { get; set; } + } + public class RefreshRequestResponse { - [JsonProperty("access_token")] + [JsonPropertyName("access_token")] public string AccessToken { get; set; } - [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } - [JsonProperty("refresh_token")] + [JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs index 313378363..ffde5a462 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs @@ -22,20 +22,20 @@ namespace NzbDrone.Core.ImportLists.Trakt return series; } - var jsonResponse = STJson.Deserialize>(_importResponse.Content); + var traktResponses = STJson.Deserialize>(_importResponse.Content); - // no series were return - if (jsonResponse == null) + // no series were returned + if (traktResponses == null) { return series; } - foreach (var show in jsonResponse) + foreach (var traktResponse in traktResponses) { series.AddIfNotNull(new ImportListItemInfo() { - Title = show.Show.Title, - TvdbId = show.Show.Ids.Tvdb.GetValueOrDefault() + Title = traktResponse.Show.Title, + TvdbId = traktResponse.Show.Ids.Tvdb.GetValueOrDefault() }); } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs index 03fde5d08..76bcf71d7 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs @@ -19,6 +19,11 @@ namespace NzbDrone.Core.ImportLists.Trakt.User public override string Name => "Trakt User"; + public override IParseImportListResponse GetParser() + { + return new TraktUserParser(Settings); + } + public override IImportListRequestGenerator GetRequestGenerator() { return new TraktUserRequestGenerator() diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserParser.cs new file mode 100644 index 000000000..3f3f27283 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserParser.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public class TraktUserParser : TraktParser + { + private readonly TraktUserSettings _settings; + private ImportListResponse _importResponse; + + public TraktUserParser(TraktUserSettings settings) + { + _settings = settings; + } + + public override IList ParseResponse(ImportListResponse importResponse) + { + _importResponse = importResponse; + + var listItems = new List(); + + if (!PreProcess(_importResponse)) + { + return listItems; + } + + var traktSeries = new List(); + + if (_settings.TraktListType == (int)TraktUserListType.UserWatchedList) + { + var jsonWatchedResponse = STJson.Deserialize>(_importResponse.Content); + + switch (_settings.TraktWatchedListType) + { + case (int)TraktUserWatchedListType.InProgress: + traktSeries = jsonWatchedResponse.Where(c => c.Seasons.Where(s => s.Number > 0).Sum(s => s.Episodes.Count) < c.Show.AiredEpisodes).SelectList(c => c.Show); + break; + case (int)TraktUserWatchedListType.CompletelyWatched: + traktSeries = jsonWatchedResponse.Where(c => c.Seasons.Where(s => s.Number > 0).Sum(s => s.Episodes.Count) == c.Show.AiredEpisodes).SelectList(c => c.Show); + break; + default: + traktSeries = jsonWatchedResponse.SelectList(c => c.Show); + break; + } + } + else + { + traktSeries = STJson.Deserialize>(_importResponse.Content).SelectList(c => c.Show); + } + + // no series were returned + if (traktSeries == null) + { + return listItems; + } + + foreach (var series in traktSeries) + { + listItems.AddIfNotNull(new ImportListItemInfo() + { + Title = series.Title, + TvdbId = series.Ids.Tvdb.GetValueOrDefault(), + }); + } + + return listItems; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs index 3bfd0d629..8f7c0396d 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.User link += $"/users/{userName}/watchlist/shows?limit={Settings.Limit}"; break; case (int)TraktUserListType.UserWatchedList: - link += $"/users/{userName}/watched/shows?limit={Settings.Limit}"; + link += $"/users/{userName}/watched/shows?extended=full&limit={Settings.Limit}"; break; case (int)TraktUserListType.UserCollectionList: link += $"/users/{userName}/collection/shows?limit={Settings.Limit}"; diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs index 9784c858b..057f3edca 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.ImportLists.Trakt.User : base() { RuleFor(c => c.TraktListType).NotNull(); + RuleFor(c => c.TraktWatchedListType).NotNull(); RuleFor(c => c.AuthUser).NotEmpty(); } } @@ -20,12 +21,16 @@ namespace NzbDrone.Core.ImportLists.Trakt.User public TraktUserSettings() { TraktListType = (int)TraktUserListType.UserWatchList; + TraktWatchedListType = (int)TraktUserWatchedListType.All; } [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktUserListType), HelpText = "Type of list you're seeking to import from")] public int TraktListType { get; set; } - [FieldDefinition(2, Label = "Username", HelpText = "Username for the List to import from (empty to use Auth User)")] + [FieldDefinition(2, Label = "Watched List Filter", Type = FieldType.Select, SelectOptions = typeof(TraktUserWatchedListType), HelpText = "If List Type is Watched. Series do you want to import from")] + public int TraktWatchedListType { get; set; } + + [FieldDefinition(3, Label = "Username", HelpText = "Username for the List to import from (empty to use Auth User)")] public string Username { get; set; } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs new file mode 100644 index 000000000..5b6526e6e --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserWatchedListType.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public enum TraktUserWatchedListType + { + [EnumMember(Value = "All")] + All = 0, + [EnumMember(Value = "In Progress")] + InProgress = 1, + [EnumMember(Value = "100% Watched")] + CompletelyWatched = 2 + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index de905efed..97420fb65 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -265,7 +265,7 @@ namespace NzbDrone.Core.IndexerSearch } } - if (sceneMapping.ParseTerm == series.CleanTitle && sceneMapping.FilterRegex.IsNotNullOrWhiteSpace()) + if (sceneMapping.ParseTerm == series.CleanTitle && sceneMapping.FilterRegex.IsNullOrWhiteSpace()) { // Disable the implied mapping if we have an explicit mapping by the same name includeGlobal = false; diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs index 2f01995ae..61b994c4e 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.BroadcastheNet @@ -14,8 +15,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet public override bool SupportsSearch => true; public override int PageSize => 100; - public BroadcastheNet(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public BroadcastheNet(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs index 35d48c94a..4adf2745b 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs @@ -25,18 +25,21 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] + [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(1, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(3)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs b/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs index f6b00e10a..5216a1ab1 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.Fanzub @@ -11,8 +12,8 @@ namespace NzbDrone.Core.Indexers.Fanzub public override DownloadProtocol Protocol => DownloadProtocol.Usenet; - public Fanzub(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public Fanzub(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index 48e4eff20..51ff19fa1 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -21,10 +21,11 @@ namespace NzbDrone.Core.Indexers.Fanzub BaseUrl = "http://fanzub.com/rss/"; } - [FieldDefinition(0, Label = "Rss URL", HelpText = "Enter to URL to an Fanzub compatible RSS feed")] + [FieldDefinition(0, Label = "IndexerSettingsRssUrl", HelpText = "IndexerSettingsRssUrlHelpText")] + [FieldToken(TokenField.HelpText, "IndexerSettingsRssUrl", "indexer", "Fanzub")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + [FieldDefinition(1, Label = "IndexerSettingsAnimeStandardFormatSearch", Type = FieldType.Checkbox, HelpText = "IndexerSettingsAnimeStandardFormatSearchHelpText")] public bool AnimeStandardFormatSearch { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/FileList/FileList.cs b/src/NzbDrone.Core/Indexers/FileList/FileList.cs index bb0da183d..7e74749de 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileList.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileList.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.FileList @@ -12,8 +13,8 @@ namespace NzbDrone.Core.Indexers.FileList public override bool SupportsRss => true; public override bool SupportsSearch => true; - public FileList(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public FileList(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs index eb22bf666..59eb7c13d 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListSettings.cs @@ -40,24 +40,27 @@ namespace NzbDrone.Core.Indexers.FileList [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(1, Label = "Passkey", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(1, Label = "IndexerSettingsPasskey", Privacy = PrivacyLevel.ApiKey)] public string Passkey { get; set; } - [FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] + [FieldDefinition(3, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] public string BaseUrl { get; set; } - [FieldDefinition(4, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "Categories for use in search and feeds, leave blank to disable standard/daily shows")] + [FieldDefinition(4, Label = "IndexerSettingsCategories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "IndexerSettingsCategoriesHelpText")] public IEnumerable Categories { get; set; } - [FieldDefinition(5, Label = "Anime Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "Categories for use in search and feeds, leave blank to disable anime")] + [FieldDefinition(5, Label = "IndexerSettingsAnimeCategories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), HelpText = "IndexerSettingsAnimeCategoriesHelpText")] public IEnumerable AnimeCategories { get; set; } - [FieldDefinition(6, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(6, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(7)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(8, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs index c52a2f496..2ac2cfb4b 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.HDBits @@ -11,16 +12,16 @@ namespace NzbDrone.Core.Indexers.HDBits public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override bool SupportsRss => true; public override bool SupportsSearch => true; - public override int PageSize => 30; + public override int PageSize => 100; - public HDBits(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public HDBits(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } public override IIndexerRequestGenerator GetRequestGenerator() { - return new HDBitsRequestGenerator() { Settings = Settings }; + return new HDBitsRequestGenerator { Settings = Settings }; } public override IParseIndexerResponse GetParser() diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs index 9bb6d624b..75c360599 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Newtonsoft.Json; namespace NzbDrone.Core.Indexers.HDBits @@ -7,20 +8,16 @@ namespace NzbDrone.Core.Indexers.HDBits { [JsonProperty(Required = Required.Always)] public string Username { get; set; } + [JsonProperty(Required = Required.Always)] public string Passkey { get; set; } public string Hash { get; set; } - public string Search { get; set; } - - public int[] Category { get; set; } - - public int[] Codec { get; set; } - - public int[] Medium { get; set; } - - public int[] Origin { get; set; } + public IEnumerable Category { get; set; } + public IEnumerable Codec { get; set; } + public IEnumerable Medium { get; set; } + public int? Origin { get; set; } [JsonProperty(PropertyName = "imdb")] public ImdbInfo ImdbInfo { get; set; } @@ -33,6 +30,7 @@ namespace NzbDrone.Core.Indexers.HDBits [JsonProperty(PropertyName = "snatched_only")] public bool? SnatchedOnly { get; set; } + public int? Limit { get; set; } public int? Page { get; set; } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index 06f986b60..4bff86c7c 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.HDBits jsonResponse.Message ?? string.Empty); } - var responseData = jsonResponse.Data as JArray; - if (responseData == null) + if (jsonResponse.Data is not JArray responseData) { throw new IndexerException(indexerResponse, "Indexer API call response missing result data"); @@ -50,9 +49,9 @@ namespace NzbDrone.Core.Indexers.HDBits foreach (var result in queryResults) { var id = result.Id; - torrentInfos.Add(new TorrentInfo() + torrentInfos.Add(new TorrentInfo { - Guid = string.Format("HDBits-{0}", id), + Guid = $"HDBits-{id}", Title = result.Name, Size = result.Size, InfoHash = result.Hash, diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 315376b72..413404d7a 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Indexers.HDBits if (TryAddSearchParameters(query, searchCriteria)) { - query.Search = string.Format("{0:yyyy}-{0:MM}-{0:dd}", searchCriteria.AirDate); + query.Search = searchCriteria.AirDate.ToString("yyyy-MM-dd"); pageableRequests.Add(GetRequest(query)); } @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Indexers.HDBits if (TryAddSearchParameters(query, searchCriteria)) { - query.Search = string.Format("{0}-", searchCriteria.Year); + query.Search = $"{searchCriteria.Year}-"; pageableRequests.Add(GetRequest(query)); } @@ -140,8 +140,9 @@ namespace NzbDrone.Core.Indexers.HDBits { if (searchCriteria.Series.TvdbId != 0) { - query.TvdbInfo = query.TvdbInfo ?? new TvdbInfo(); + query.TvdbInfo ??= new TvdbInfo(); query.TvdbInfo.Id = searchCriteria.Series.TvdbId; + return true; } @@ -162,6 +163,12 @@ namespace NzbDrone.Core.Indexers.HDBits query.Username = Settings.Username; query.Passkey = Settings.ApiKey; + query.Category = Settings.Categories.ToArray(); + query.Codec = Settings.Codecs.ToArray(); + query.Medium = Settings.Mediums.ToArray(); + + query.Limit = 100; + request.SetContent(query.ToJson()); request.ContentSummary = query.ToJson(Formatting.None); diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs index f0d2667d4..1e35ca310 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsSettings.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -17,28 +19,44 @@ namespace NzbDrone.Core.Indexers.HDBits public class HDBitsSettings : ITorrentIndexerSettings { - private static readonly HDBitsSettingsValidator Validator = new HDBitsSettingsValidator(); + private static readonly HDBitsSettingsValidator Validator = new (); public HDBitsSettings() { BaseUrl = "https://hdbits.org"; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + + Categories = new[] { (int)HdBitsCategory.Tv, (int)HdBitsCategory.Documentary }; + Codecs = Array.Empty(); + Mediums = Array.Empty(); } - [FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)] - public string Username { get; set; } - - [FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey)] - public string ApiKey { get; set; } - - [FieldDefinition(2, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] + [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] public string BaseUrl { get; set; } - [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(1, Label = "Username", Privacy = PrivacyLevel.UserName)] + public string Username { get; set; } + + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] + public string ApiKey { get; set; } + + [FieldDefinition(3, Label = "IndexerHDBitsSettingsCategories", Type = FieldType.Select, SelectOptions = typeof(HdBitsCategory), HelpText = "IndexerHDBitsSettingsCategoriesHelpText")] + public IEnumerable Categories { get; set; } + + [FieldDefinition(4, Label = "IndexerHDBitsSettingsCodecs", Type = FieldType.Select, SelectOptions = typeof(HdBitsCodec), Advanced = true, HelpText = "IndexerHDBitsSettingsCodecsHelpText")] + public IEnumerable Codecs { get; set; } + + [FieldDefinition(5, Label = "IndexerHDBitsSettingsMediums", Type = FieldType.Select, SelectOptions = typeof(HdBitsMedium), Advanced = true, HelpText = "IndexerHDBitsSettingsMediumsHelpText")] + public IEnumerable Mediums { get; set; } + + [FieldDefinition(6, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(7)] + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); + + [FieldDefinition(8, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } public NzbDroneValidationResult Validate() { @@ -48,31 +66,49 @@ namespace NzbDrone.Core.Indexers.HDBits public enum HdBitsCategory { + [FieldOption(label: "Movie")] Movie = 1, + [FieldOption(label: "TV")] Tv = 2, + [FieldOption(label: "Documentary")] Documentary = 3, + [FieldOption(label: "Music")] Music = 4, + [FieldOption(label: "Sport")] Sport = 5, + [FieldOption(label: "Audio Track")] Audio = 6, + [FieldOption(label: "XXX")] Xxx = 7, + [FieldOption(label: "Misc/Demo")] MiscDemo = 8 } public enum HdBitsCodec { + [FieldOption(label: "H.264")] H264 = 1, + [FieldOption(label: "MPEG-2")] Mpeg2 = 2, + [FieldOption(label: "VC-1")] Vc1 = 3, + [FieldOption(label: "XviD")] Xvid = 4, + [FieldOption(label: "HEVC")] Hevc = 5 } public enum HdBitsMedium { + [FieldOption(label: "Blu-ray/HD DVD")] Bluray = 1, + [FieldOption(label: "Encode")] Encode = 3, + [FieldOption(label: "Capture")] Capture = 4, + [FieldOption(label: "Remux")] Remux = 5, + [FieldOption(label: "WEB-DL")] WebDl = 6 } } diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index f22b1d223..42d790bf5 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Http.CloudFlare; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -34,8 +35,8 @@ namespace NzbDrone.Core.Indexers public abstract IIndexerRequestGenerator GetRequestGenerator(); public abstract IParseIndexerResponse GetParser(); - public HttpIndexerBase(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(indexerStatusService, configService, parsingService, logger) + public HttpIndexerBase(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(indexerStatusService, configService, parsingService, logger, localizationService) { _httpClient = httpClient; } @@ -307,9 +308,17 @@ namespace NzbDrone.Core.Indexers protected virtual bool IsValidRelease(ReleaseInfo release) { + if (release.Title.IsNullOrWhiteSpace()) + { + _logger.Trace("Invalid Release: '{0}' from indexer: {1}. No title provided.", release.InfoUrl, Definition.Name); + + return false; + } + if (release.DownloadUrl.IsNullOrWhiteSpace()) { - _logger.Trace("Invalid Release: '{0}' from indexer: {1}. No Download URL provided.", release.Title, release.Indexer); + _logger.Trace("Invalid Release: '{0}' from indexer: {1}. No Download URL provided.", release.Title, Definition.Name); + return false; } @@ -368,50 +377,50 @@ namespace NzbDrone.Core.Indexers if (firstRequest == null) { - return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings."); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationJackettNoRssFeedQueryAvailable")); } var releases = await FetchPage(firstRequest, parser); if (releases.Empty()) { - return new ValidationFailure(string.Empty, "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings."); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationJackettNoResultsInConfiguredCategories")); } } catch (ApiKeyException ex) { _logger.Warn("Indexer returned result for RSS URL, API Key appears to be invalid: " + ex.Message); - return new ValidationFailure("ApiKey", "Invalid API Key"); + return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("IndexerValidationInvalidApiKey")); } catch (RequestLimitReachedException ex) { _logger.Warn("Request limit reached: " + ex.Message); - return new ValidationFailure(string.Empty, "Request limit reached: " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationRequestLimitReached", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (CloudFlareCaptchaException ex) { if (ex.IsExpired) { - return new ValidationFailure("CaptchaToken", "CloudFlare CAPTCHA token expired, please Refresh."); + return new ValidationFailure("CaptchaToken", _localizationService.GetLocalizedString("IndexerValidationCloudFlareCaptchaExpired")); } else { - return new ValidationFailure("CaptchaToken", "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required."); + return new ValidationFailure("CaptchaToken", _localizationService.GetLocalizedString("IndexerValidationCloudFlareCaptchaRequired")); } } catch (UnsupportedFeedException ex) { _logger.Warn(ex, "Indexer feed is not supported"); - return new ValidationFailure(string.Empty, "Indexer feed is not supported: " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationFeedNotSupported", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (IndexerException ex) { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, "Unable to connect to indexer. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (HttpException ex) { @@ -419,33 +428,33 @@ namespace NzbDrone.Core.Indexers ex.Response.Content.Contains("not support the requested query")) { _logger.Warn(ex, "Indexer does not support the query"); - return new ValidationFailure(string.Empty, "Indexer does not support the current query. Check if the categories and or searching for seasons/episodes are supported. Check the log for more details."); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationQuerySeasonEpisodesNotSupported")); } _logger.Warn(ex, "Unable to connect to indexer"); if (ex.Response.HasHttpServerError) { - return new ValidationFailure(string.Empty, "Unable to connect to indexer, indexer's server is unavailable. Try again later. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectServerUnavailable", new Dictionary { { "exceptionMessage", ex.Message } })); } if (ex.Response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized) { - return new ValidationFailure(string.Empty, "Unable to connect to indexer, invalid credentials. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectInvalidCredentials", new Dictionary { { "exceptionMessage", ex.Message } })); } - return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (HttpRequestException ex) { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, "Unable to connect to indexer, please check your DNS settings and ensure IPv6 is working or disabled. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectHttpError", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (TaskCanceledException ex) { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, "Unable to connect to indexer, possibly due to a timeout. Try again or check your network settings. " + ex.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectTimeout", new Dictionary { { "exceptionMessage", ex.Message } })); } catch (WebException webException) { @@ -453,20 +462,20 @@ namespace NzbDrone.Core.Indexers if (webException.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure) { - return new ValidationFailure(string.Empty, "Unable to connect to indexer connection failure. Check your connection to the indexer's server and DNS." + webException.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectResolutionFailure", new Dictionary { { "exceptionMessage", webException.Message } })); } if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("504") || webException.Message.Contains("timed out")) { - return new ValidationFailure(string.Empty, "Unable to connect to indexer, indexer's server is unavailable. Try again later. " + webException.Message); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnectServerUnavailable", new Dictionary { { "exceptionMessage", webException.Message } })); } } catch (Exception ex) { _logger.Warn(ex, "Unable to connect to indexer"); - return new ValidationFailure(string.Empty, $"Unable to connect to indexer: {ex.Message}. Check the log surrounding this error for details"); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrents.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrents.cs index 6b74949ba..ab7f9ee7d 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrents.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrents.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.IPTorrents @@ -13,8 +14,8 @@ namespace NzbDrone.Core.Indexers.IPTorrents public override bool SupportsSearch => false; public override int PageSize => 0; - public IPTorrents(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public IPTorrents(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index e2b433539..f1c3bb490 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -31,15 +31,18 @@ namespace NzbDrone.Core.Indexers.IPTorrents MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(0, Label = "Feed URL", HelpText = "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)")] + [FieldDefinition(0, Label = "IndexerIPTorrentsSettingsFeedUrl", HelpText = "IndexerIPTorrentsSettingsFeedUrlHelpText")] public string BaseUrl { get; set; } - [FieldDefinition(1, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(2)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(3, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs index 6a9070475..eb507c0c9 100644 --- a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs @@ -6,5 +6,6 @@ namespace NzbDrone.Core.Indexers // TODO: System.Text.Json requires setter be public for sub-object deserialization in 3.0. https://github.com/dotnet/corefx/issues/42515 SeedCriteriaSettings SeedCriteria { get; set; } + bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 07680e0c3..dbb9916c0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -20,6 +21,7 @@ namespace NzbDrone.Core.Indexers protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; protected readonly Logger _logger; + protected readonly ILocalizationService _localizationService; public abstract string Name { get; } public abstract DownloadProtocol Protocol { get; } @@ -29,12 +31,13 @@ namespace NzbDrone.Core.Indexers public abstract bool SupportsRss { get; } public abstract bool SupportsSearch { get; } - public IndexerBase(IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + public IndexerBase(IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) { _indexerStatusService = indexerStatusService; _configService = configService; _parsingService = parsingService; _logger = logger; + _localizationService = localizationService; } public Type ConfigContract => typeof(TSettings); @@ -105,7 +108,7 @@ namespace NzbDrone.Core.Indexers catch (Exception ex) { _logger.Error(ex, "Test aborted due to exception"); - failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); + failures.Add(new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationTestAbortedDueToError", new Dictionary { { "exceptionMessage", ex.Message } }))); } return new ValidationResult(failures); diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 16bbe72c6..344932210 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -22,8 +23,8 @@ namespace NzbDrone.Core.Indexers.Newznab public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override int PageSize => GetProviderPageSize(); - public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { _capabilitiesProvider = capabilitiesProvider; } @@ -129,13 +130,13 @@ namespace NzbDrone.Core.Indexers.Newznab return null; } - return new ValidationFailure(string.Empty, "Indexer does not support required search parameters"); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationSearchParametersNotSupported")); } catch (Exception ex) { _logger.Warn(ex, "Unable to connect to indexer: " + ex.Message); - return new ValidationFailure(string.Empty, $"Unable to connect to indexer: {ex.Message}. Check the log surrounding this error for details"); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } })); } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs index 25599f67c..d84bd49c9 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; +using NzbDrone.Core.Indexers.Exceptions; namespace NzbDrone.Core.Indexers.Newznab { @@ -73,6 +74,13 @@ namespace NzbDrone.Core.Indexers.Newznab _logger.Debug(ex, "Failed to parse newznab api capabilities for {0}", indexerSettings.BaseUrl); throw; } + catch (ApiKeyException ex) + { + ex.WithData(response, 128 * 1024); + _logger.Trace("Unexpected Response content ({0} bytes): {1}", response.ResponseData.Length, response.Content); + _logger.Debug(ex, "Failed to parse newznab api capabilities for {0}, invalid API key", indexerSettings.BaseUrl); + throw; + } catch (Exception ex) { ex.WithData(response, 128 * 1024); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index 688be0d1f..a38229560 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -60,22 +60,23 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(0, Label = "URL")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Path", HelpText = "Path to the api, usually /api", Advanced = true)] + [FieldDefinition(1, Label = "IndexerSettingsApiPath", HelpText = "IndexerSettingsApiPathHelpText", Advanced = true)] + [FieldToken(TokenField.HelpText, "IndexerSettingsApiPath", "url", "/api")] public string ApiPath { get; set; } - [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list, leave blank to disable standard/daily shows")] + [FieldDefinition(3, Label = "IndexerSettingsCategories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "IndexerSettingsCategoriesHelpText")] public IEnumerable Categories { get; set; } - [FieldDefinition(4, Label = "Anime Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list, leave blank to disable anime")] + [FieldDefinition(4, Label = "IndexerSettingsAnimeCategories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "IndexerSettingsAnimeCategoriesHelpText")] public IEnumerable AnimeCategories { get; set; } - [FieldDefinition(5, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + [FieldDefinition(5, Label = "IndexerSettingsAnimeStandardFormatSearch", Type = FieldType.Checkbox, HelpText = "IndexerSettingsAnimeStandardFormatSearchHelpText")] public bool AnimeStandardFormatSearch { get; set; } - [FieldDefinition(6, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)] + [FieldDefinition(6, Label = "IndexerSettingsAdditionalParameters", HelpText = "IndexerSettingsAdditionalNewznabParametersHelpText", Advanced = true)] public string AdditionalParameters { get; set; } // Field 7 is used by TorznabSettings MinimumSeeders diff --git a/src/NzbDrone.Core/Indexers/Nyaa/Nyaa.cs b/src/NzbDrone.Core/Indexers/Nyaa/Nyaa.cs index af169ecf5..391f2ed0b 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/Nyaa.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/Nyaa.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.Nyaa @@ -12,8 +13,8 @@ namespace NzbDrone.Core.Indexers.Nyaa public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override int PageSize => 100; - public Nyaa(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public Nyaa(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index e13a73c50..f6c40e713 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -27,21 +27,24 @@ namespace NzbDrone.Core.Indexers.Nyaa MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(0, Label = "Website URL")] + [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + [FieldDefinition(1, Label = "IndexerSettingsAnimeStandardFormatSearch", Type = FieldType.Checkbox, HelpText = "IndexerSettingsAnimeStandardFormatSearchHelpText")] public bool AnimeStandardFormatSearch { get; set; } - [FieldDefinition(2, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] + [FieldDefinition(2, Label = "IndexerSettingsAdditionalParameters", Advanced = true, HelpText = "IndexerSettingsAdditionalNewznabParametersHelpText")] public string AdditionalParameters { get; set; } - [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(4)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs index aa13f6b1a..af9fad86f 100644 --- a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -48,13 +48,13 @@ namespace NzbDrone.Core.Indexers public class SeedCriteriaSettings { - [FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default. Ratio should be at least 1.0 and follow the indexers rules")] + [FieldDefinition(0, Type = FieldType.Textbox, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")] public double? SeedRatio { get; set; } - [FieldDefinition(1, Type = FieldType.Number, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] + [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)] public int? SeedTime { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "IndexerSettingsSeasonPackSeedTimeHelpText", Advanced = true)] public int? SeasonPackSeedTime { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexer.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexer.cs index 5f467807a..9f69a02b8 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexer.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexer.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.TorrentRss @@ -15,8 +16,8 @@ namespace NzbDrone.Core.Indexers.TorrentRss private readonly ITorrentRssParserFactory _torrentRssParserFactory; - public TorrentRssIndexer(ITorrentRssParserFactory torrentRssParserFactory, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public TorrentRssIndexer(ITorrentRssParserFactory torrentRssParserFactory, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { _torrentRssParserFactory = torrentRssParserFactory; } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 1c9ace66e..6543b5a7f 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -25,21 +25,24 @@ namespace NzbDrone.Core.Indexers.TorrentRss MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(0, Label = "Full RSS Feed URL")] + [FieldDefinition(0, Label = "IndexerSettingsRssUrl")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "Cookie", HelpText = "If you site requires a login cookie to access the rss, you'll have to retrieve it via a browser.")] + [FieldDefinition(1, Label = "IndexerSettingsCookie", HelpText = "IndexerSettingsCookieHelpText")] public string Cookie { get; set; } - [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText="Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.")] + [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText="IndexerSettingsAllowZeroSizeHelpText")] public bool AllowZeroSize { get; set; } - [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(4)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(5, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs b/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs index fc3a46bc2..4cad693e7 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Indexers.Torrentleech @@ -13,8 +14,8 @@ namespace NzbDrone.Core.Indexers.Torrentleech public override bool SupportsSearch => false; public override int PageSize => 0; - public Torrentleech(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public Torrentleech(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { } diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index d621b818c..51e21b31c 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -25,18 +25,21 @@ namespace NzbDrone.Core.Indexers.Torrentleech MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(0, Label = "Website URL")] + [FieldDefinition(0, Label = "IndexerSettingsWebsiteUrl")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(1, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey)] public string ApiKey { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(2, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(3)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 978d4b186..e77d669d3 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Newznab; +using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -23,8 +24,8 @@ namespace NzbDrone.Core.Indexers.Torznab public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override int PageSize => GetProviderPageSize(); - public Torznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) + public Torznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger, ILocalizationService localizationService) + : base(httpClient, indexerStatusService, configService, parsingService, logger, localizationService) { _capabilitiesProvider = capabilitiesProvider; } @@ -123,13 +124,13 @@ namespace NzbDrone.Core.Indexers.Torznab return null; } - return new ValidationFailure(string.Empty, "Indexer does not support required search parameters"); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationSearchParametersNotSupported")); } catch (Exception ex) { _logger.Warn(ex, "Unable to connect to indexer: " + ex.Message); - return new ValidationFailure(string.Empty, $"Unable to connect to indexer: {ex.Message}. Check the log surrounding this error for details"); + return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("IndexerValidationUnableToConnect", new Dictionary { { "exceptionMessage", ex.Message } })); } } @@ -140,10 +141,10 @@ namespace NzbDrone.Core.Indexers.Torznab Settings.BaseUrl.Contains("/torznab/all") || Settings.BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab")) { - return new NzbDroneValidationFailure("ApiPath", "Jackett's all endpoint is not supported, please add indexers individually") + return new NzbDroneValidationFailure("ApiPath", _localizationService.GetLocalizedString("IndexerValidationJackettAllNotSupported")) { IsWarning = true, - DetailedDescription = "Jackett's all endpoint is not supported, please add indexers individually" + DetailedDescription = _localizationService.GetLocalizedString("IndexerValidationJackettAllNotSupportedHelpText") }; } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index ff5f59f65..7e3ce1295 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -51,12 +51,15 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(7, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(7, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] public int MinimumSeeders { get; set; } [FieldDefinition(8)] public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); + [FieldDefinition(9, Type = FieldType.Checkbox, Label = "Reject Blocklisted Torrent Hashes While Grabbing", HelpText = "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.", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index 0967ef424..5e2d116df 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -1 +1,4 @@ -{} +{ + "AddAutoTag": "أضف كلمات دلالية تلقائيا", + "AddCondition": "إضافة شرط" +} diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 746bdf439..d11d3f779 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -1,25 +1,25 @@ { "Added": "Přidáno", - "ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {0} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru", + "ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {length} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru", "BlocklistRelease": "Blocklist pro vydání", "AgeWhenGrabbed": "Stáří (kdy bylo získáno)", "Always": "Vždy", "AnalyseVideoFiles": "Analyzovat video soubory", - "AnalyseVideoFilesHelpText": "Extrahujte ze souborů informace o videu, jako je rozlišení, doba běhu a informace o kodeku. To vyžaduje, aby Sonarr četl části souboru, což může způsobit vysokou aktivitu disku nebo sítě během skenování.", + "AnalyseVideoFilesHelpText": "Extrahujte ze souborů informace o videu, jako je rozlišení, doba běhu a informace o kodeku. To vyžaduje, aby {appName} četl části souboru, což může způsobit vysokou aktivitu disku nebo sítě během skenování.", "ApplicationURL": "URL aplikace", "ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základní adresy URL", - "AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k Sonarru", + "AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k {appName}u", "AuthenticationRequired": "Vyžadované ověření", "AuthenticationRequiredHelpText": "Změnit, pro které požadavky je vyžadováno ověření. Pokud nerozumíte rizikům, neměňte je.", - "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní Sonarr povolení ověření. Ověřování z místních adres můžete volitelně zakázat.", + "AuthenticationRequiredWarning": "Aby se zabránilo vzdálenému přístupu bez ověření, vyžaduje nyní {appName} povolení ověření. Ověřování z místních adres můžete volitelně zakázat.", "AutoRedownloadFailedHelpText": "Automatické vyhledání a pokus o stažení jiného vydání", "AutoTaggingLoadError": "Nelze načíst automatické označování", "AutomaticAdd": "Přidat automaticky", "BackupIntervalHelpText": "Interval mezi automatickými zálohami", "BackupRetentionHelpText": "Automatické zálohy starší než doba uchovávání budou automaticky vyčištěny", "BackupsLoadError": "Nelze načíst zálohy", - "BlocklistReleaseHelpText": "Znovu spustí vyhledávání této epizody a zabrání tomu, aby bylo toto vydání získáno znovu", - "BranchUpdate": "Větev, která se použije k aktualizaci Sonarru", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Znovu spustí vyhledávání této epizody a zabrání tomu, aby bylo toto vydání získáno znovu", + "BranchUpdate": "Větev, která se použije k aktualizaci {appName}u", "BranchUpdateMechanism": "Větev používaná externím aktualizačním mechanismem", "BrowserReloadRequired": "Vyžaduje se opětovné načtení prohlížeče", "BuiltIn": "Vestavěný", @@ -73,11 +73,11 @@ "Clone": "Klonovat", "CloneIndexer": "Klonovat indexátor", "ColonReplacement": "Nahrazení dvojtečky", - "ColonReplacementFormatHelpText": "Změna způsobu, jakým Sonarr zpracovává náhradu dvojtečky", + "ColonReplacementFormatHelpText": "Změna způsobu, jakým {appName} zpracovává náhradu dvojtečky", "CollectionsLoadError": "Nelze načíst sbírky", "CompletedDownloadHandling": "Zpracování stahování bylo dokončeno", "Condition": "Stav", - "UpdateMechanismHelpText": "Použijte vestavěný nástroj Sonarru pro aktualizaci nebo skript", + "UpdateMechanismHelpText": "Použijte vestavěný nástroj {appName}u pro aktualizaci nebo skript", "AddCondition": "Přidat podmínku", "AutoTagging": "Automatické označování", "AddAutoTag": "Přidat automatické tagy", @@ -130,7 +130,7 @@ "BackupNow": "Ihned zálohovat", "AppDataDirectory": "Adresář AppData", "ApplyTagsHelpTextHowToApplySeries": "Jak použít značky na vybrané seriály", - "BackupFolderHelpText": "Relativní cesty se budou nacházet v adresáři AppData systému Sonarr", + "BackupFolderHelpText": "Relativní cesty se budou nacházet v adresáři AppData systému {appName}", "BlocklistReleases": "Blocklist pro vydání", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Epizoda se stahuje", @@ -171,11 +171,11 @@ "CalendarOptions": "Možnosti kalendáře", "AllFiles": "Všechny soubory", "Analytics": "Analýzy", - "AnalyticsEnabledHelpText": "Odesílání anonymních informací o používání a chybách na servery společnosti Sonarr. To zahrnuje informace o vašem prohlížeči, o tom, které stránky webového rozhraní Sonarr používáte, hlášení chyb a také informace o verzi operačního systému a běhového prostředí. Tyto informace použijeme k určení priorit funkcí a oprav chyb.", + "AnalyticsEnabledHelpText": "Odesílání anonymních informací o používání a chybách na servery společnosti {appName}. To zahrnuje informace o vašem prohlížeči, o tom, které stránky webového rozhraní {appName} používáte, hlášení chyb a také informace o verzi operačního systému a běhového prostředí. Tyto informace použijeme k určení priorit funkcí a oprav chyb.", "Anime": "Anime", "AnimeEpisodeFormat": "Formát epizod pro Anime", - "AnimeTypeDescription": "Epizody vydané s použitím absolutního čísla epizody", - "AnimeTypeFormat": "Absolutní číslo epizody ({format})", + "AnimeEpisodeTypeDescription": "Epizody vydané s použitím absolutního čísla epizody", + "AnimeEpisodeTypeFormat": "Absolutní číslo epizody ({format})", "Any": "Jakákoliv", "ApiKey": "Klíč API", "AppUpdated": "{appName} aktualizován", @@ -189,14 +189,14 @@ "BypassDelayIfAboveCustomFormatScore": "Obejít, pokud je vyšší než skóre vlastního formátu", "Calendar": "Kalendář", "CalendarFeed": "{appName} zdroj kalendáře", - "CalendarLegendDownloadedTooltip": "Epizoda byla stažena a roztříděna", - "CalendarLegendDownloadingTooltip": "Epizoda se právě stahuje", - "CalendarLegendFinaleTooltip": "Finále seriálu nebo řady", - "CalendarLegendMissingTooltip": "Epizoda byla odvysílána a na disku chybí", - "CalendarLegendOnAirTooltip": "Epizoda se právě vysílá", - "CalendarLegendPremiereTooltip": "Premiéra seriálu nebo řady", - "CalendarLegendUnairedTooltip": "Epizoda ještě nebyla odvysílána", - "CalendarLegendUnmonitoredTooltip": "Epizoda je nemonitorovaná", + "CalendarLegendEpisodeDownloadedTooltip": "Epizoda byla stažena a roztříděna", + "CalendarLegendEpisodeDownloadingTooltip": "Epizoda se právě stahuje", + "CalendarLegendSeriesFinaleTooltip": "Finále seriálu nebo řady", + "CalendarLegendEpisodeMissingTooltip": "Epizoda byla odvysílána a na disku chybí", + "CalendarLegendEpisodeOnAirTooltip": "Epizoda se právě vysílá", + "CalendarLegendSeriesPremiereTooltip": "Premiéra seriálu nebo řady", + "CalendarLegendEpisodeUnairedTooltip": "Epizoda ještě nebyla odvysílána", + "CalendarLegendEpisodeUnmonitoredTooltip": "Epizoda je nemonitorovaná", "ChmodFolder": "Složka chmod", "ClickToChangeReleaseGroup": "Kliknutím změníte skupinu vydání", "ClickToChangeSeason": "Kliknutím změníte řadu", @@ -213,8 +213,8 @@ "CutoffUnmet": "Vynechat nevyhovující", "Cutoff": "Odříznout", "DailyEpisodeFormat": "Formát denní epizody", - "DailyTypeDescription": "Epizody vydávané denně nebo méně často, které používají rok-měsíc-den (2023-08-04)", - "CopyUsingHardlinksHelpTextWarning": "Příležitostně mohou zámky souborů bránit přejmenování souborů, které jsou odesílány. Můžete dočasně zakázat odesílání a použít funkci přejmenování Sonarr jako řešení.", + "DailyEpisodeTypeDescription": "Epizody vydávané denně nebo méně často, které používají rok-měsíc-den (2023-08-04)", + "CopyUsingHardlinksHelpTextWarning": "Příležitostně mohou zámky souborů bránit přejmenování souborů, které jsou odesílány. Můžete dočasně zakázat odesílání a použít funkci přejmenování {appName} jako řešení.", "CreateEmptySeriesFoldersHelpText": "Vytvoření chybějících složek pro seriály během skenování disku", "CreateEmptySeriesFolders": "Vytvoření prázdných složek pro seriály", "CreateGroup": "Vytvořit skupinu", @@ -223,8 +223,8 @@ "CustomFormatUnknownConditionOption": "Neznámá možnost '{key}' pro podmínku '{implementation}'", "CustomFormatsLoadError": "Nelze načíst vlastní formáty", "CustomFormatsSettingsSummary": "Vlastní formáty a nastavení", - "CopyUsingHardlinksHelpText": "Pevné odkazy umožňují aplikaci Sonarr importovat odesílané torrenty do složky seriálu, aniž by zabíraly další místo na disku nebo kopírovaly celý obsah souboru. Hardlinky budou fungovat pouze v případě, že zdrojový a cílový soubor jsou na stejném svazku", - "CustomFormatHelpText": "Sonarr hodnotí každé vydání pomocí součtu bodů za odpovídající vlastní formáty. Pokud by nové vydání zlepšilo skóre při stejné nebo lepší kvalitě, Sonarr jej získá.", + "CopyUsingHardlinksSeriesHelpText": "Pevné odkazy umožňují aplikaci {appName} importovat odesílané torrenty do složky seriálu, aniž by zabíraly další místo na disku nebo kopírovaly celý obsah souboru. Hardlinky budou fungovat pouze v případě, že zdrojový a cílový soubor jsou na stejném svazku", + "CustomFormatHelpText": "{appName} hodnotí každé vydání pomocí součtu bodů za odpovídající vlastní formáty. Pokud by nové vydání zlepšilo skóre při stejné nebo lepší kvalitě, {appName} jej získá.", "Daily": "Denní", "ContinuingOnly": "Pouze pokračující", "CurrentlyInstalled": "Aktuálně nainstalováno", @@ -240,13 +240,13 @@ "CustomFilters": "Vlastní filtry", "DelayProfiles": "Profily zpoždění", "DelayProfilesLoadError": "Nelze načíst profily zpoždění", - "DelayProfileTagsHelpText": "Platí pro seriály s alespoň jednou odpovídající značkou", + "DelayProfileSeriesTagsHelpText": "Platí pro seriály s alespoň jednou odpovídající značkou", "DelayMinutes": "{delay} Minuty", - "DefaultDelayProfile": "Toto je výchozí profil. Platí pro všechny seriály, které nemají explicitní profil.", + "DefaultDelayProfileSeries": "Toto je výchozí profil. Platí pro všechny seriály, které nemají explicitní profil.", "CustomFormatJson": "Vlastní JSON formát", "Debug": "Ladit", "Day": "Den", - "DeleteCustomFormatMessageText": "Opravdu chcete odstranit vlastní formát '{0}'?", + "DeleteCustomFormatMessageText": "Opravdu chcete odstranit vlastní formát '{customFormatName}'?", "DefaultNameCopiedProfile": "{name} - Kopírovat", "DefaultNameCopiedSpecification": "{name} - Kopírovat", "DefaultNotFoundMessage": "Asi jsi se ztratil, není tu nic k vidění.", @@ -269,6 +269,7 @@ "Date": "Datum", "Dates": "Termíny", "DefaultCase": "Výchozí případ", - "DailyTypeFormat": "Datum ({format})", - "Default": "Výchozí" + "DailyEpisodeTypeFormat": "Datum ({format})", + "Default": "Výchozí", + "IndexerDownloadClientHelpText": "Zvolte, který klient pro stahování bude použit pro zachytávání z toho indexeru" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 93591ee5e..76e249266 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -1,6 +1,6 @@ { "Added": "Hinzugefügt", - "ApiKeyValidationHealthCheckMessage": "Bitte den API Schlüssel korrigieren, dieser muss mindestens {0} Zeichen lang sein. Die Änderung kann über die Einstellungen oder die Konfigurationsdatei erfolgen", + "ApiKeyValidationHealthCheckMessage": "Bitte den API Schlüssel korrigieren, dieser muss mindestens {length} Zeichen lang sein. Die Änderung kann über die Einstellungen oder die Konfigurationsdatei erfolgen", "AppDataLocationHealthCheckMessage": "Ein Update ist nicht möglich, um das Löschen von AppData beim Update zu verhindern", "RemoveCompletedDownloads": "Entferne abgeschlossene Downloads", "RemoveFailedDownloads": "Entferne fehlgeschlagene Downloads", @@ -8,10 +8,10 @@ "AutomaticAdd": "Automatisch hinzufügen", "CountSeasons": "{Anzahl} Staffeln", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Es ist kein Download-Client verfügbar", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {0} nicht möglich.", - "DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {0} legt Downloads im Stammordner {1} ab. Sie sollten nicht in einen Stammordner herunterladen.", - "DownloadClientSortingHealthCheckMessage": "Im Download-Client {0} ist die Sortierung {1} für die Kategorie von Sonarr aktiviert. Sie sollten die Sortierung in Ihrem Download-Client deaktivieren, um Importprobleme zu vermeiden.", - "DownloadClientStatusSingleClientHealthCheckMessage": "Download-Clients sind aufgrund von Fehlern nicht verfügbar: {0}", + "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.", + "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}", "DownloadClientStatusAllClientHealthCheckMessage": "Alle Download-Clients sind aufgrund von Fehlern nicht verfügbar", "EditSelectedDownloadClients": "Ausgewählte Download Clienten bearbeiten", "EditSelectedImportLists": "Ausgewählte Einspiel-Liste bearbeten", @@ -22,10 +22,10 @@ "Language": "Sprache", "CloneCondition": "Bedingung klonen", "DeleteCondition": "Bedingung löschen", - "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{0}' löschen willst?", - "DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{0}' löschen willst?", + "DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?", + "DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{customFormatName}' löschen willst?", "RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?", - "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {0} Einträge aus der Warteschlange entfernen willst?", + "RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?", "DeleteSelectedDownloadClients": "Lösche Download Client(s)", "DeleteSelectedIndexers": "Lösche Indexer", "DeleteSelectedImportLists": "Lösche Einspiel Liste", @@ -37,5 +37,26 @@ "ManageClients": "Verwalte Clienten", "ManageDownloadClients": "Verwalte Download Clienten", "ManageImportLists": "Verwalte Einspiel-Listen", - "NoDownloadClientsFound": "Keine Download Clienten gefunden" + "NoDownloadClientsFound": "Keine Download Clienten gefunden", + "SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen", + "AbsoluteEpisodeNumber": "Exakte Folgennummer", + "AddConnection": "Verbindung hinzufügen", + "AddAutoTagError": "Neues automatisches Tag kann nicht hinzugefügt werden, bitte versuche es erneut.", + "AddConditionError": "Neue Bedingung kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.", + "AddCustomFormat": "Eigenes Format hinzufügen", + "AddCustomFormatError": "Neues eigenes Format kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.", + "AddDelayProfile": "Verzögerungsprofil hinzufügen", + "AddDownloadClientError": "Neuer Downloadmanager kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.", + "AddExclusion": "Ausschluss hinzufügen", + "AddDownloadClient": "Downloadmanager hinzufügen", + "AddCondition": "Bedingung hinzufügen", + "AddAutoTag": "Automatisches Tag hinzufügen", + "AbsoluteEpisodeNumbers": "Exakte Folgennummer(n)", + "Add": "Hinzufügen", + "Activity": "Aktivität", + "About": "Über", + "Actions": "Aktionen", + "Absolute": "Exakte", + "AddANewPath": "Neuen Pfad hinzufügen", + "AddCustomFilter": "Eigenen Filter hinzufügen" } diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index ffadb1f73..d7f64dbef 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -1,6 +1,6 @@ { "AppDataLocationHealthCheckMessage": "Η ενημέρωση δεν θα είναι δυνατή για να αποτραπεί η διαγραφή των δεδομένων εφαρμογής κατά την ενημέρωση", - "ApiKeyValidationHealthCheckMessage": "Παρακαλούμε ενημερώστε το κλείδι API ώστε να έχει τουλάχιστον {0} χαρακτήρες. Μπορείτε να το κάνετε αυτό μέσα από τις ρυθμίσεις ή το αρχείο ρυθμίσεων", + "ApiKeyValidationHealthCheckMessage": "Παρακαλούμε ενημερώστε το κλείδι API ώστε να έχει τουλάχιστον {length} χαρακτήρες. Μπορείτε να το κάνετε αυτό μέσα από τις ρυθμίσεις ή το αρχείο ρυθμίσεων", "Added": "Προστέθηκε", "ApplyChanges": "Εφαρμογή Αλλαγών", "AutomaticAdd": "Αυτόματη Προσθήκη", @@ -10,9 +10,13 @@ "RemoveCompletedDownloads": "Αφαίρεση Ολοκληρωμένων Λήψεων", "RemoveFailedDownloads": "Αφαίρεση Αποτυχημένων Λήψεων", "DeleteCondition": "Διαγραφή συνθήκης", - "DeleteConditionMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη συνθήκη '{0}';", - "DeleteCustomFormatMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη προσαρμοσμένη μορφή '{0}';", - "RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {0} αντικείμενα από την ουρά;", + "DeleteConditionMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη συνθήκη '{name}';", + "DeleteCustomFormatMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε τη προσαρμοσμένη μορφή '{customFormatName}';", + "RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {selectedCount} αντικείμενα από την ουρά;", "CloneCondition": "Κλωνοποίηση συνθήκης", - "RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;" + "RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;", + "AddConditionImplementation": "Προσθήκη", + "AppUpdated": "{appName} Ενημερώθηκε", + "AutoAdd": "Προσθήκη", + "AddConnectionImplementation": "Προσθήκη" } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 761324961..2ad0187ad 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -33,7 +33,7 @@ "AddListError": "Unable to add a new list, please try again.", "AddListExclusion": "Add List Exclusion", "AddListExclusionError": "Unable to add a new list exclusion, please try again.", - "AddListExclusionHelpText": "Prevent series from being added to Sonarr by lists", + "AddListExclusionSeriesHelpText": "Prevent series from being added to {appName} by lists", "AddNew": "Add New", "AddNewRestriction": "Add new restriction", "AddNewSeries": "Add New Series", @@ -76,16 +76,16 @@ "Always": "Always", "AnEpisodeIsDownloading": "An Episode is downloading", "AnalyseVideoFiles": "Analyse video files", - "AnalyseVideoFilesHelpText": "Extract video information such as resolution, runtime and codec information from files. This requires Sonarr to read parts of the file which may cause high disk or network activity during scans.", + "AnalyseVideoFilesHelpText": "Extract video information such as resolution, runtime and codec information from files. This requires {appName} to read parts of the file which may cause high disk or network activity during scans.", "Analytics": "Analytics", - "AnalyticsEnabledHelpText": "Send anonymous usage and error information to Sonarr's servers. This includes information on your browser, which Sonarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.", + "AnalyticsEnabledHelpText": "Send anonymous usage and error information to {appName}'s servers. This includes information on your browser, which {appName} WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.", "Anime": "Anime", "AnimeEpisodeFormat": "Anime Episode Format", - "AnimeTypeDescription": "Episodes released using an absolute episode number", - "AnimeTypeFormat": "Absolute episode number ({format})", + "AnimeEpisodeTypeDescription": "Episodes released using an absolute episode number", + "AnimeEpisodeTypeFormat": "Absolute episode number ({format})", "Any": "Any", "ApiKey": "API Key", - "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", + "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file", "AppDataDirectory": "AppData directory", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppUpdated": "{appName} Updated", @@ -113,10 +113,14 @@ "AuthenticationMethodHelpTextWarning": "Please select a valid authentication method", "AuthenticationRequired": "Authentication Required", "AuthenticationRequiredHelpText": "Change which requests authentication is required for. Do not change unless you understand the risks.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirm new password", "AuthenticationRequiredPasswordHelpTextWarning": "Enter a new password", "AuthenticationRequiredUsernameHelpTextWarning": "Enter a new username", "AuthenticationRequiredWarning": "To prevent remote access without authentication, {appName} now requires authentication to be enabled. You can optionally disable authentication from local addresses.", "AutoAdd": "Auto Add", + "AutoRedownloadFailed": "Redownload Failed", + "AutoRedownloadFailedFromInteractiveSearch": "Redownload Failed from Interactive Search", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Automatically search for and attempt to download a different release when failed release was grabbed from interactive search", "AutoRedownloadFailedHelpText": "Automatically search for and attempt to download a different release", "AutoTagging": "Auto Tagging", "AutoTaggingLoadError": "Unable to load auto tagging", @@ -127,7 +131,7 @@ "AutomaticSearch": "Automatic Search", "AutomaticUpdatesDisabledDocker": "Automatic updates are not directly supported when using the Docker update mechanism. You will need to update the container image outside of {appName} or use a script", "Backup": "Backup", - "BackupFolderHelpText": "Relative paths will be under Sonarr's AppData directory", + "BackupFolderHelpText": "Relative paths will be under {appName}'s AppData directory", "BackupIntervalHelpText": "Interval between automatic backups", "BackupNow": "Backup Now", "BackupRetentionHelpText": "Automatic backups older than the retention period will be cleaned up automatically", @@ -136,13 +140,16 @@ "BeforeUpdate": "Before update", "BindAddress": "Bind Address", "BindAddressHelpText": "Valid IP address, localhost or '*' for all interfaces", + "BlackholeFolderHelpText": "Folder in which {appName} will store the {extension} file", + "BlackholeWatchFolder": "Watch Folder", + "BlackholeWatchFolderHelpText": "Folder from which {appName} should import completed downloads", "Blocklist": "Blocklist", "BlocklistLoadError": "Unable to load blocklist", "BlocklistRelease": "Blocklist Release", - "BlocklistReleaseHelpText": "Starts a search for this episode again and prevents this release from being grabbed again", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Starts a search for this episode again and prevents this release from being grabbed again", "BlocklistReleases": "Blocklist Releases", "Branch": "Branch", - "BranchUpdate": "Branch to use to update Sonarr", + "BranchUpdate": "Branch to use to update {appName}", "BranchUpdateMechanism": "Branch used by external update mechanism", "BrowserReloadRequired": "Browser Reload Required", "BuiltIn": "Built-In", @@ -155,19 +162,20 @@ "BypassProxyForLocalAddresses": "Bypass Proxy for Local Addresses", "Calendar": "Calendar", "CalendarFeed": "{appName} Calendar Feed", - "CalendarLegendDownloadedTooltip": "Episode was downloaded and sorted", - "CalendarLegendDownloadingTooltip": "Episode is currently downloading", - "CalendarLegendFinaleTooltip": "Series or season finale", - "CalendarLegendMissingTooltip": "Episode has aired and is missing from disk", - "CalendarLegendOnAirTooltip": "Episode is currently airing", - "CalendarLegendPremiereTooltip": "Series or season premiere", - "CalendarLegendUnairedTooltip": "Episode hasn't aired yet", - "CalendarLegendUnmonitoredTooltip": "Episode is unmonitored", + "CalendarLegendEpisodeDownloadedTooltip": "Episode was downloaded and sorted", + "CalendarLegendEpisodeDownloadingTooltip": "Episode is currently downloading", + "CalendarLegendEpisodeMissingTooltip": "Episode has aired and is missing from disk", + "CalendarLegendEpisodeOnAirTooltip": "Episode is currently airing", + "CalendarLegendEpisodeUnairedTooltip": "Episode hasn't aired yet", + "CalendarLegendEpisodeUnmonitoredTooltip": "Episode is unmonitored", + "CalendarLegendSeriesFinaleTooltip": "Series or season finale", + "CalendarLegendSeriesPremiereTooltip": "Series or season premiere", "CalendarLoadError": "Unable to load the calendar", "CalendarOptions": "Calendar Options", "Cancel": "Cancel", "CancelPendingTask": "Are you sure you want to cancel this pending task?", "CancelProcessing": "Cancel Processing", + "Category": "Category", "CertificateValidation": "Certificate Validation", "CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.", "Certification": "Certification", @@ -176,13 +184,15 @@ "CheckDownloadClientForDetails": "check download client for more details", "ChmodFolder": "chmod Folder", "ChmodFolderHelpText": "Octal, applied during import/rename to media folders and files (without execute bits)", - "ChmodFolderHelpTextWarning": "This only works if the user running sonarr is the owner of the file. It's better to ensure the download client sets the permissions properly.", + "ChmodFolderHelpTextWarning": "This only works if the user running {appName} is the owner of the file. It's better to ensure the download client sets the permissions properly.", "ChooseAnotherFolder": "Choose another folder", "ChooseImportMode": "Choose Import Mode", "ChownGroup": "chown Group", "ChownGroupHelpText": "Group name or gid. Use gid for remote file systems.", - "ChownGroupHelpTextWarning": "This only works if the user running sonarr is the owner of the file. It's better to ensure the download client uses the same group as sonarr.", + "ChownGroupHelpTextWarning": "This only works if the user running {appName} is the owner of the file. It's better to ensure the download client uses the same group as {appName}.", "Clear": "Clear", + "ClearBlocklist": "Clear blocklist", + "ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?", "ClickToChangeEpisode": "Click to change episode", "ClickToChangeLanguage": "Click to change language", "ClickToChangeQuality": "Click to change quality", @@ -202,7 +212,7 @@ "CollapseMultipleEpisodesHelpText": "Collapse multiple episodes airing on the same day", "CollectionsLoadError": "Unable to load collections", "ColonReplacement": "Colon Replacement", - "ColonReplacementFormatHelpText": "Change how Sonarr handles colon replacement", + "ColonReplacementFormatHelpText": "Change how {appName} handles colon replacement", "CompletedDownloadHandling": "Completed Download Handling", "Component": "Component", "Condition": "Condition", @@ -220,8 +230,8 @@ "ContinuingOnly": "Continuing Only", "ContinuingSeriesDescription": "More episodes/another season is expected", "CopyToClipboard": "Copy to Clipboard", - "CopyUsingHardlinksHelpText": "Hardlinks allow Sonarr to import seeding torrents to the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", - "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Sonarr's rename function as a work around.", + "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use {appName}'s rename function as a work around.", + "CopyUsingHardlinksSeriesHelpText": "Hardlinks allow {appName} to import seeding torrents to the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", "CouldNotFindResults": "Couldn't find any results for '{term}'", "CountDownloadClientsSelected": "{count} download client(s) selected", "CountImportListsSelected": "{count} import list(s) selected", @@ -237,7 +247,7 @@ "Custom": "Custom", "CustomFilters": "Custom Filters", "CustomFormat": "Custom Format", - "CustomFormatHelpText": "Sonarr scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then Sonarr will grab it.", + "CustomFormatHelpText": "{appName} scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then {appName} will grab it.", "CustomFormatJson": "Custom Format JSON", "CustomFormatScore": "Custom Format Score", "CustomFormatUnknownCondition": "Unknown Custom Format condition '{implementation}'", @@ -252,8 +262,8 @@ "CutoffUnmetNoItems": "No cutoff unmet items", "Daily": "Daily", "DailyEpisodeFormat": "Daily Episode Format", - "DailyTypeDescription": "Episodes released daily or less frequently that use year-month-day (2023-08-04)", - "DailyTypeFormat": "Date ({format})", + "DailyEpisodeTypeDescription": "Episodes released daily or less frequently that use year-month-day (2023-08-04)", + "DailyEpisodeTypeFormat": "Date ({format})", "Dash": "Dash", "Database": "Database", "Date": "Date", @@ -262,14 +272,14 @@ "Debug": "Debug", "Default": "Default", "DefaultCase": "Default Case", - "DefaultDelayProfile": "This is the default profile. It applies to all series that don't have an explicit profile.", + "DefaultDelayProfileSeries": "This is the default profile. It applies to all series that don't have an explicit profile.", "DefaultNameCopiedProfile": "{name} - Copy", "DefaultNameCopiedSpecification": "{name} - Copy", "DefaultNotFoundMessage": "You must be lost, nothing to see here.", "DelayMinutes": "{delay} Minutes", "DelayProfile": "Delay Profile", "DelayProfileProtocol": "Protocol: {preferredProtocol}", - "DelayProfileTagsHelpText": "Applies to series with at least one matching tag", + "DelayProfileSeriesTagsHelpText": "Applies to series with at least one matching tag", "DelayProfiles": "Delay Profiles", "DelayProfilesLoadError": "Unable to load Delay Profiles", "DelayingDownloadUntil": "Delaying download until {date} at {time}", @@ -281,13 +291,13 @@ "DeleteCondition": "Delete Condition", "DeleteConditionMessageText": "Are you sure you want to delete the condition '{name}'?", "DeleteCustomFormat": "Delete Custom Format", - "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{0}'?", + "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{customFormatName}'?", "DeleteDelayProfile": "Delete Delay Profile", "DeleteDelayProfileMessageText": "Are you sure you want to delete this delay profile?", "DeleteDownloadClient": "Delete Download Client", "DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{name}'?", "DeleteEmptyFolders": "Delete Empty Folders", - "DeleteEmptyFoldersHelpText": "Delete empty series and season folders during disk scan and when episode files are deleted", + "DeleteEmptySeriesFoldersHelpText": "Delete empty series and season folders during disk scan and when episode files are deleted", "DeleteEpisodeFile": "Delete Episode File", "DeleteEpisodeFileMessage": "Are you sure you want to delete '{path}'?", "DeleteEpisodeFromDisk": "Delete episode from disk", @@ -332,15 +342,17 @@ "DeleteTag": "DeleteTag", "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", - "DeletedReasonMissingFromDisk": "Sonarr was unable to find the file on disk so the file was unlinked from the episode in the database", "DeletedReasonUpgrade": "File was deleted to import an upgrade", "DeletedSeriesDescription": "Series was deleted from TheTVDB", + "Destination": "Destination", "DestinationPath": "Destination Path", "DestinationRelativePath": "Destination Relative Path", "DetailedProgressBar": "Detailed Progress Bar", "DetailedProgressBarHelpText": "Show text on progress bar", "Details": "Details", + "Directory": "Directory", "Disabled": "Disabled", "DisabledForLocalAddresses": "Disabled for Local Addresses", "Discord": "Discord", @@ -356,25 +368,149 @@ "Download": "Download", "DownloadClient": "Download Client", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {0}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}", + "DownloadClientDelugeTorrentStateError": "Deluge is reporting an error", + "DownloadClientDelugeValidationLabelPluginFailure": "Configuration of label failed", + "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.", + "DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", + "DownloadClientDownloadStationSettingsDirectory": "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}", + "DownloadClientDownloadStationValidationFolderMissing": "Folder does not exist", + "DownloadClientDownloadStationValidationFolderMissingDetail": "The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'.", + "DownloadClientDownloadStationValidationNoDefaultDestination": "No default destination", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "You must login into your Diskstation as {username} and manually set it up into DownloadStation settings under BT/HTTP/FTP/NZB -> Location.", + "DownloadClientDownloadStationValidationSharedFolderMissing": "Shared folder does not exist", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "The Diskstation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?", + "DownloadClientFloodSettingsAdditionalTags": "Additional Tags", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adds properties of media as tags. Hints are examples.", + "DownloadClientFloodSettingsPostImportTags": "Post-Import Tags", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Appends tags after a download is imported.", + "DownloadClientFloodSettingsRemovalInfo": "{appName} will handle automatic removal of torrents based on the current seed criteria in Settings -> Indexers", + "DownloadClientFloodSettingsStartOnAdd": "Start on Add", + "DownloadClientFloodSettingsTagsHelpText": "Initial tags of a download. To be recognized, a download must have all initial tags. This avoids conflicts with unrelated downloads.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Adds a prefix to the Flood API, such as {url}", + "DownloadClientFreeboxApiError": "Freebox API returned error: {errorDescription}", + "DownloadClientFreeboxAuthenticationError": "Authentication to Freebox API failed. Reason: {errorDescription}", + "DownloadClientFreeboxNotLoggedIn": "Not logged in", + "DownloadClientFreeboxSettingsApiUrl": "API URL", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Define Freebox API base URL with API version, eg '{url}', defaults to '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "App ID", + "DownloadClientFreeboxSettingsAppIdHelpText": "App ID given when creating access to Freebox API (ie 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "App Token", + "DownloadClientFreeboxSettingsAppTokenHelpText": "App token retrieved when creating access to Freebox API (ie 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Hostname or host IP address of the Freebox, defaults to '{url}' (will only work if on same network)", + "DownloadClientFreeboxSettingsPortHelpText": "Port used to access Freebox interface, defaults to '{port}'", + "DownloadClientFreeboxUnableToReachFreebox": "Unable to reach Freebox API. Verify 'Host', 'Port' or 'Use SSL' settings. (Error: {exceptionMessage})", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Unable to reach Freebox API. Verify 'API URL' setting for base URL and version.", + "DownloadClientNzbVortexMultipleFilesMessage": "Download contains multiple files and is not in a job folder: {outputPath}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "This option requires at least NzbGet version 16.0", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGet setting KeepHistory should be less than 25000", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGet setting KeepHistory is set too high.", + "DownloadClientNzbgetValidationKeepHistoryZero": "NzbGet setting KeepHistory should be greater than 0", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet setting KeepHistory is set to 0. Which prevents {appName} from seeing completed downloads.", "DownloadClientOptionsLoadError": "Unable to load download client options", - "DownloadClientRootFolderHealthCheckMessage": "Download client {0} places downloads in the root folder {1}. You should not download to a root folder.", + "DownloadClientPneumaticSettingsNzbFolder": "Nzb Folder", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "This folder will need to be reachable from XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Strm Folder", + "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Download first and last pieces first (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Initial state for torrents added to qBittorrent. Note that Forced Torrents do not abide by seed restrictions", + "DownloadClientQbittorrentSettingsSequentialOrder": "Sequential Order", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Download in sequential order (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent cannot resolve magnet link with DHT disabled", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent is reporting an error", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent is downloading metadata", + "DownloadClientQbittorrentTorrentStatePathError": "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?", + "DownloadClientQbittorrentTorrentStateStalled": "The download is stalled with no connections", + "DownloadClientQbittorrentTorrentStateUnknown": "Unknown download state: {state}", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Configuration of category failed", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} was unable to add the label to qBittorrent.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Category is recommended", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} will not attempt to import completed downloads without a category.", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Category is not supported", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Categories are not supported until qBittorrent version 3.3.0. Please upgrade or try again with an empty Category.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Queueing Not Enabled", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'", + "DownloadClientRTorrentProviderMessage": "rTorrent will not pause torrents when they meet the seed criteria. {appName} will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers only when Remove Completed is enabled. After importing it will also set {importedView} as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", + "DownloadClientRTorrentSettingsAddStopped": "Add Stopped", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Enabling will add torrents and magnets to rTorrent in a stopped state. This may break magnet files.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default rTorrent location", + "DownloadClientRTorrentSettingsUrlPath": "Url Path", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Path to the XMLRPC endpoint, see {url}. This is usually RPC2 or [path to ruTorrent]{url2} when using ruTorrent.", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Download client {downloadClientName} is set to remove completed downloads. This can result in downloads being removed from your client before {appName} can import them.", + "DownloadClientRootFolderHealthCheckMessage": "Download client {downloadClientName} places downloads in the root folder {rootFolderPath}. You should not download to a root folder.", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Disable 'Check before download' option in Sabnbzd", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Using 'Check before download' affects {appName} ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective.", + "DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd develop version, assuming version 3.0.0 or higher.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} may not be able to support new features added to SABnzbd when running develop versions.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Disable Date Sorting", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "You must disable Date sorting for the category {appName} uses to prevent import issues. Go to Sabnzbd to fix it.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Disable Movie Sorting", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "You must disable Movie sorting for the category {appName} uses to prevent import issues. Go to Sabnzbd to fix it.", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Disable TV Sorting", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "You must disable TV sorting for the category {appName} uses to prevent import issues. Go to Sabnzbd to fix it.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Enable Job folders", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it.", + "DownloadClientSabnzbdValidationUnknownVersion": "Unknown Version: {rawVersion}", + "DownloadClientSeriesTagHelpText": "Only use this download client for series with at least one matching tag. Leave blank to use with all series.", "DownloadClientSettings": "Download Client Settings", - "DownloadClientSortingHealthCheckMessage": "Download client {0} has {1} sorting enabled for Sonarr's category. You should disable sorting in your download client to avoid import issues.", + "DownloadClientSettingsAddPaused": "Add Paused", + "DownloadClientSettingsCategoryHelpText": "Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended.", + "DownloadClientSettingsCategorySubFolderHelpText": "Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended. Creates a [category] subdirectory in the output directory.", + "DownloadClientSettingsDestinationHelpText": "Manually specifies download destination, leave blank to use the default", + "DownloadClientSettingsInitialState": "Initial State", + "DownloadClientSettingsInitialStateHelpText": "Initial state for torrents added to {clientName}", + "DownloadClientSettingsOlderPriority": "Older Priority", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Priority to use when grabbing episodes that aired over 14 days ago", + "DownloadClientSettingsPostImportCategoryHelpText": "Category for {appName} to set after it has imported the download. {appName} will not remove torrents in that category even if seeding finished. Leave blank to keep same category.", + "DownloadClientSettingsRecentPriority": "Recent Priority", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Priority to use when grabbing episodes that aired within the last 14 days", + "DownloadClientSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} url, such as {url}", + "DownloadClientSettingsUseSslHelpText": "Use secure connection when connection to {clientName}", + "DownloadClientSortingHealthCheckMessage": "Download client {downloadClientName} has {sortingMode} sorting enabled for {appName}'s category. You should disable sorting in your download client to avoid import issues.", "DownloadClientStatusAllClientHealthCheckMessage": "All download clients are unavailable due to failures", - "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {0}", - "DownloadClientTagHelpText": "Only use this download client for series with at least one matching tag. Leave blank to use with all series.", + "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", + "DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error", + "DownloadClientValidationApiKeyIncorrect": "API Key Incorrect", + "DownloadClientValidationApiKeyRequired": "API Key Required", + "DownloadClientValidationAuthenticationFailure": "Authentication Failure", + "DownloadClientValidationAuthenticationFailureDetail": "Please verify your username and password. Also verify if the host running {appName} isn't blocked from accessing {clientName} by WhiteList limitations in the {clientName} configuration.", + "DownloadClientValidationCategoryMissing": "Category does not exist", + "DownloadClientValidationCategoryMissingDetail": "The category you entered doesn't exist in {clientName}. Create it in {clientName} first.", + "DownloadClientValidationErrorVersion": "{clientName} version should be at least {requiredVersion}. Version reported is {reportedVersion}", + "DownloadClientValidationGroupMissing": "Group does not exist", + "DownloadClientValidationGroupMissingDetail": "The group you entered doesn't exist in {clientName}. Create it in {clientName} first.", + "DownloadClientValidationSslConnectFailure": "Unable to connect through SSL", + "DownloadClientValidationSslConnectFailureDetail": "{appName} is unable to connect to {clientName} using SSL. This problem could be computer related. Please try to configure both {appName} and {clientName} to not use SSL.", + "DownloadClientValidationTestNzbs": "Failed to get the list of NZBs: {exceptionMessage}", + "DownloadClientValidationTestTorrents": "Failed to get the list of torrents: {exceptionMessage}", + "DownloadClientValidationUnableToConnect": "Unable to connect to {clientName}", + "DownloadClientValidationUnableToConnectDetail": "Please verify the hostname and port.", + "DownloadClientValidationUnknownException": "Unknown exception: {exception}", + "DownloadClientValidationVerifySsl": "Verify SSL settings", + "DownloadClientValidationVerifySslDetail": "Please verify your SSL configuration on both {clientName} and {appName}", + "DownloadClientVuzeValidationErrorVersion": "Protocol version not supported, use Vuze 5.0.0.0 or higher with Vuze Web Remote plugin.", "DownloadClients": "Download Clients", "DownloadClientsLoadError": "Unable to load download clients", "DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings", "DownloadFailed": "Download Failed", - "DownloadFailedTooltip": "Episode download failed", + "DownloadFailedEpisodeTooltip": "Episode download failed", "DownloadIgnored": "Download Ignored", - "DownloadIgnoredTooltip": "Episode Download Ignored", + "DownloadIgnoredEpisodeTooltip": "Episode Download Ignored", "DownloadPropersAndRepacks": "Propers and Repacks", "DownloadPropersAndRepacksHelpText": "Whether or not to automatically upgrade to Propers/Repacks", "DownloadPropersAndRepacksHelpTextCustomFormat": "Use 'Do not Prefer' to sort by custom format score over Propers/Repacks", "DownloadPropersAndRepacksHelpTextWarning": "Use custom formats for automatic upgrades to Propers/Repacks", + "DownloadStationStatusExtracting": "Extracting: {progress}%", "DownloadWarning": "Download warning: {warningMessage}", "Downloaded": "Downloaded", "Downloading": "Downloading", @@ -405,9 +541,9 @@ "EditSeriesModalHeader": "Edit - {title}", "Enable": "Enable", "EnableAutomaticAdd": "Enable Automatic Add", - "EnableAutomaticAddHelpText": "Add series from this list to Sonarr when syncs are performed via the UI or by Sonarr", + "EnableAutomaticAddSeriesHelpText": "Add series from this list to {appName} when syncs are performed via the UI or by {appName}", "EnableAutomaticSearch": "Enable Automatic Search", - "EnableAutomaticSearchHelpText": "Will be used when automatic searches are performed via the UI or by Sonarr", + "EnableAutomaticSearchHelpText": "Will be used when automatic searches are performed via the UI or by {appName}", "EnableAutomaticSearchHelpTextWarning": "Will be used when interactive search is used", "EnableColorImpairedMode": "Enable Color-Impaired Mode", "EnableColorImpairedModeHelpText": "Altered style to allow color-impaired users to better distinguish color coded information", @@ -416,12 +552,12 @@ "EnableInteractiveSearch": "Enable Interactive Search", "EnableInteractiveSearchHelpText": "Will be used when interactive search is used", "EnableInteractiveSearchHelpTextWarning": "Search is not supported with this indexer", - "EnableMediaInfoHelpText": "Extract video information such as resolution, runtime and codec information from files. This requires Sonarr to read parts of the file which may cause high disk or network activity during scans.", + "EnableMediaInfoHelpText": "Extract video information such as resolution, runtime and codec information from files. This requires {appName} to read parts of the file which may cause high disk or network activity during scans.", "EnableMetadataHelpText": "Enable metadata file creation for this metadata type", "EnableProfile": "Enable Profile", "EnableProfileHelpText": "Check to enable release profile", "EnableRss": "Enable RSS", - "EnableRssHelpText": "Will be used when Sonarr periodically looks for releases via RSS Sync", + "EnableRssHelpText": "Will be used when {appName} periodically looks for releases via RSS Sync", "EnableSsl": "Enable SSL", "EnableSslHelpText": "Requires restart running as administrator to take effect", "Enabled": "Enabled", @@ -437,6 +573,7 @@ "EpisodeFileRenamed": "Episode File Renamed", "EpisodeFileRenamedTooltip": "Episode file renamed", "EpisodeFilesLoadError": "Unable to load episode files", + "EpisodeGrabbedTooltip": "Episode grabbed from {indexer} and sent to {downloadClient}", "EpisodeHasNotAired": "Episode has not aired", "EpisodeHistoryLoadError": "Unable to load episode history", "EpisodeImported": "Episode Imported", @@ -472,7 +609,7 @@ "ExportCustomFormat": "Export Custom Format", "Extend": "Extend", "External": "External", - "ExternalUpdater": "Sonarr is configured to use an external update mechanism", + "ExternalUpdater": "{appName} is configured to use an external update mechanism", "ExtraFileExtensionsHelpText": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)", "ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'", "Failed": "Failed", @@ -480,7 +617,7 @@ "FailedToLoadCustomFiltersFromApi": "Failed to load custom filters from API", "FailedToLoadQualityProfilesFromApi": "Failed to load quality profiles from API", "FailedToLoadSeriesFromApi": "Failed to load series from API", - "FailedToLoadSonarr": "Failed to load Sonarr", + "FailedToLoadSonarr": "Failed to load {appName}", "FailedToLoadSystemStatusFromApi": "Failed to load system status from API", "FailedToLoadTagsFromApi": "Failed to load tags from API", "FailedToLoadTranslationsFromApi": "Failed to load translations from API", @@ -531,8 +668,8 @@ "FormatAgeHours": "hours", "FormatAgeMinute": "minute", "FormatAgeMinutes": "minutes", - "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "FormatRuntimeHours": "{hours}h", "FormatRuntimeMinutes": "{minutes}m", "FormatShortTimeSpanHours": "{hours} hour(s)", @@ -555,10 +692,9 @@ "Grab": "Grab", "GrabId": "Grab ID", "GrabRelease": "Grab Release", - "GrabReleaseMessageText": "Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '{title}'?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} was unable to determine which series and episode this release was for. {appName} may be unable to automatically import this release. Do you want to grab '{title}'?", "GrabSelected": "Grab Selected", "Grabbed": "Grabbed", - "GrabbedHistoryTooltip": "Episode grabbed from {indexer} and sent to {downloadClient}", "Group": "Group", "HardlinkCopyFiles": "Hardlink/Copy Files", "HasMissingSeason": "Has Missing Season", @@ -579,12 +715,12 @@ "HttpHttps": "HTTP(S)", "ICalFeed": "iCal Feed", "ICalFeedHelpText": "Copy this URL to your client(s) or click to subscribe if your browser supports webcal", - "ICalIncludeUnmonitoredHelpText": "Include unmonitored episodes in the iCal feed", + "ICalIncludeUnmonitoredEpisodesHelpText": "Include unmonitored episodes in the iCal feed", "ICalLink": "iCal Link", "ICalSeasonPremieresOnlyHelpText": "Only the first episode in a season will be in the feed", "ICalShowAsAllDayEvents": "Show as All-Day Events", "ICalShowAsAllDayEventsHelpText": "Events will appear as all-day events in your calendar", - "ICalTagsHelpText": "Feed will only contain series with at least one matching tag", + "ICalTagsSeriesHelpText": "Feed will only contain series with at least one matching tag", "IRC": "IRC", "IRCLinkText": "#sonarr on Libera", "IconForCutoffUnmet": "Icon for Cutoff Unmet", @@ -604,19 +740,21 @@ "ImportErrors": "Import Errors", "ImportExistingSeries": "Import Existing Series", "ImportExtraFiles": "Import Extra Files", - "ImportExtraFilesHelpText": "Import matching extra files (subtitles, nfo, etc) after importing an episode file", + "ImportExtraFilesEpisodeHelpText": "Import matching extra files (subtitles, nfo, etc) after importing an episode file", "ImportFailed": "Import Failed: {sourceTitle}", "ImportList": "Import List", "ImportListExclusions": "Import List Exclusions", "ImportListExclusionsLoadError": "Unable to load Import List Exclusions", - "ImportListRootFolderMissingRootHealthCheckMessage": "Missing root folder for import list(s): {0}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Multiple root folders are missing for import lists: {0}", + "ImportListRootFolderMissingRootHealthCheckMessage": "Missing root folder for import list(s): {rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Multiple root folders are missing for import lists: {rootFolderInfo}", + "ImportListSearchForMissingEpisodes": "Search for Missing Episodes", + "ImportListSearchForMissingEpisodesHelpText": "After series is added to {appName} automatically search for missing episodes", "ImportListSettings": "Import List Settings", "ImportListStatusAllUnavailableHealthCheckMessage": "All lists are unavailable due to failures", - "ImportListStatusUnavailableHealthCheckMessage": "Lists unavailable due to failures: {0}", + "ImportListStatusUnavailableHealthCheckMessage": "Lists unavailable due to failures: {importListNames}", "ImportLists": "Import Lists", "ImportListsLoadError": "Unable to load Import Lists", - "ImportListsSettingsSummary": "Import from another Sonarr instance or Trakt lists and manage list exclusions", + "ImportListsSettingsSummary": "Import from another {appName} instance or Trakt lists and manage list exclusions", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Enable Completed Download Handling if possible", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling", @@ -633,23 +771,78 @@ "IncludeHealthWarnings": "Include Health Warnings", "IncludeUnmonitored": "Include Unmonitored", "Indexer": "Indexer", - "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {0}.", + "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.", "IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer", - "IndexerJackettAllHealthCheckMessage": "Indexers using the unsupported Jackett 'all' endpoint: {0}", + "IndexerHDBitsSettingsCategories": "Categories", + "IndexerHDBitsSettingsCategoriesHelpText": "If unspecified, all options are used.", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerHDBitsSettingsCodecsHelpText": "If unspecified, all options are used.", + "IndexerHDBitsSettingsMediums": "Mediums", + "IndexerHDBitsSettingsMediumsHelpText": "If unspecified, all options are used.", + "IndexerIPTorrentsSettingsFeedUrl": "Feed URL", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "The full RSS feed url generated by IPTorrents, using only the categories you selected (HD, SD, x264, etc ...)", + "IndexerJackettAllHealthCheckMessage": "Indexers using the unsupported Jackett 'all' endpoint: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures for more than 6 hours", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures for more than 6 hours: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures for more than 6 hours: {indexerNames}", "IndexerOptionsLoadError": "Unable to load indexer options", "IndexerPriority": "Indexer Priority", - "IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Sonarr will still use all enabled indexers for RSS Sync and Searching", + "IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, {appName} will still use all enabled indexers for RSS Sync and Searching", "IndexerRssNoIndexersAvailableHealthCheckMessage": "All rss-capable indexers are temporarily unavailable due to recent indexer errors", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "No indexers available with RSS sync enabled, Sonarr will not grab new releases automatically", - "IndexerSearchNoAutomaticHealthCheckMessage": "No indexers available with Automatic Search enabled, Sonarr will not provide any automatic search results", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "No indexers available with RSS sync enabled, {appName} will not grab new releases automatically", + "IndexerSearchNoAutomaticHealthCheckMessage": "No indexers available with Automatic Search enabled, {appName} will not provide any automatic search results", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "All search-capable indexers are temporarily unavailable due to recent indexer errors", - "IndexerSearchNoInteractiveHealthCheckMessage": "No indexers available with Interactive Search enabled, Sonarr will not provide any interactive search results", + "IndexerSearchNoInteractiveHealthCheckMessage": "No indexers available with Interactive Search enabled, {appName} will not provide any interactive search results", "IndexerSettings": "Indexer Settings", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.", + "IndexerSettingsAdditionalParameters": "Additional Parameters", + "IndexerSettingsAdditionalParametersNyaa": "Additional Parameters", + "IndexerSettingsAllowZeroSize": "Allow Zero Size", + "IndexerSettingsAllowZeroSizeHelpText": "Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.", + "IndexerSettingsAnimeCategories": "Anime Categories", + "IndexerSettingsAnimeCategoriesHelpText": "Drop down list, leave blank to disable anime", + "IndexerSettingsAnimeStandardFormatSearch": "Anime Standard Format Search", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Also search for anime using the standard numbering", + "IndexerSettingsApiPath": "API Path", + "IndexerSettingsApiPathHelpText": "Path to the api, usually {url}", + "IndexerSettingsApiUrl": "API URL", + "IndexerSettingsApiUrlHelpText": "Do not change this unless you know what you're doing. Since your API key will be sent to that host.", + "IndexerSettingsCategories": "Categories", + "IndexerSettingsCategoriesHelpText": "Drop down list, leave blank to disable standard/daily shows", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.", + "IndexerSettingsMinimumSeeders": "Minimum Seeders", + "IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.", + "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsRssUrl": "RSS URL", + "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", + "IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", + "IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season-pack torrent should be seeded before stopping, empty uses the download client's default", + "IndexerSettingsSeedRatio": "Seed Ratio", + "IndexerSettingsSeedRatioHelpText": "The ratio a torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules", + "IndexerSettingsSeedTime": "Seed Time", + "IndexerSettingsSeedTimeHelpText": "The time a torrent should be seeded before stopping, empty uses the download client's default", + "IndexerSettingsWebsiteUrl": "Website URL", "IndexerStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures", - "IndexerStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures: {0}", - "IndexerTagHelpText": "Only use this indexer for series with at least one matching tag. Leave blank to use with all series.", + "IndexerStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures: {indexerNames}", + "IndexerTagSeriesHelpText": "Only use this indexer for series with at least one matching tag. Leave blank to use with all series.", + "IndexerValidationCloudFlareCaptchaExpired": "CloudFlare CAPTCHA token expired, please refresh it.", + "IndexerValidationCloudFlareCaptchaRequired": "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required.", + "IndexerValidationFeedNotSupported": "Indexer feed is not supported: {exceptionMessage}", + "IndexerValidationInvalidApiKey": "Invalid API Key", + "IndexerValidationJackettAllNotSupported": "Jackett's all endpoint is not supported, please add indexers individually", + "IndexerValidationJackettAllNotSupportedHelpText": "Jackett's all endpoint is not supported, please add indexers individually", + "IndexerValidationJackettNoResultsInConfiguredCategories": "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings.", + "IndexerValidationJackettNoRssFeedQueryAvailable": "No RSS feed query available. This may be an issue with the indexer or your indexer category settings.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Indexer does not support the current query. Check if the categories and or searching for seasons/episodes are supported. Check the log for more details.", + "IndexerValidationRequestLimitReached": "Request limit reached: {exceptionMessage}", + "IndexerValidationSearchParametersNotSupported": "Indexer does not support required search parameters", + "IndexerValidationTestAbortedDueToError": "Test was aborted due to an error: {exceptionMessage}", + "IndexerValidationUnableToConnect": "Unable to connect to indexer: {exceptionMessage}. Check the log surrounding this error for details", + "IndexerValidationUnableToConnectHttpError": "Unable to connect to indexer, please check your DNS settings and ensure that IPv6 is working or disabled. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "Unable to connect to indexer, invalid credentials. {exceptionMessage}.", + "IndexerValidationUnableToConnectResolutionFailure": "Unable to connect to indexer connection failure. Check your connection to the indexer's server and DNS. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Unable to connect to indexer, indexer's server is unavailable. Try again later. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Unable to connect to indexer, possibly due to a timeout. Try again or check your network settings. {exceptionMessage}.", "Indexers": "Indexers", "IndexersLoadError": "Unable to load Indexers", "IndexersSettingsSummary": "Indexers and indexer options", @@ -670,7 +863,7 @@ "InteractiveSearch": "Interactive Search", "InteractiveSearchModalHeader": "Interactive Search", "InteractiveSearchModalHeaderSeason": "Interactive Search - {season}", - "InteractiveSearchResultsFailedErrorMessage": "Search failed because its {message}. Try refreshing the series info and verify the necessary information is present before searching again.", + "InteractiveSearchResultsSeriesFailedErrorMessage": "Search failed because its {message}. Try refreshing the series info and verify the necessary information is present before searching again.", "InteractiveSearchSeason": "Interactive search for all episodes in this season", "Interval": "Interval", "InvalidFormat": "Invalid Format", @@ -693,11 +886,11 @@ "Level": "Level", "LiberaWebchat": "Libera Webchat", "LibraryImport": "Library Import", - "LibraryImportHeader": "Import series you already have", + "LibraryImportSeriesHeader": "Import series you already have", "LibraryImportTips": "Some tips to ensure the import goes smoothly:", "LibraryImportTipsDontUseDownloadsFolder": "Do not use for importing downloads from your download client, this is only for existing organized libraries, not unsorted files.", - "LibraryImportTipsQualityInFilename": "Make sure that your files include the quality in their filenames. eg. `episode.s02e15.bluray.mkv`", - "LibraryImportTipsUseRootFolder": "Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. \"`{goodFolderExample}`\" and not \"`{badFolderExample}`\". Additionally, each series must be in its own folder within the root/library folder.", + "LibraryImportTipsQualityInEpisodeFilename": "Make sure that your files include the quality in their filenames. eg. `episode.s02e15.bluray.mkv`", + "LibraryImportTipsSeriesUseRootFolder": "Point {appName} to the folder containing all of your tv shows, not a specific one. eg. \"`{goodFolderExample}`\" and not \"`{badFolderExample}`\". Additionally, each series must be in its own folder within the root/library folder.", "Links": "Links", "ListExclusionsLoadError": "Unable to load List Exclusions", "ListOptionsLoadError": "Unable to load list options", @@ -736,7 +929,7 @@ "Mapping": "Mapping", "MarkAsFailed": "Mark as Failed", "MarkAsFailedConfirmation": "Are you sure you want to mark '{sourceTitle}' as failed?", - "MassSearchCancelWarning": "This cannot be cancelled once started without restarting Sonarr or disabling all of your indexers.", + "MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.", "MatchedToEpisodes": "Matched to Episodes", "MatchedToSeason": "Matched to Season", "MatchedToSeries": "Matched to Series", @@ -760,10 +953,10 @@ "MetadataLoadError": "Unable to load Metadata", "MetadataProvidedBy": "Metadata is provided by {provider}", "MetadataSettings": "Metadata Settings", - "MetadataSettingsSummary": "Create metadata files when episodes are imported or series are refreshed", + "MetadataSettingsSeriesSummary": "Create metadata files when episodes are imported or series are refreshed", "MetadataSource": "Metadata Source", "MetadataSourceSettings": "Metadata Source Settings", - "MetadataSourceSettingsSummary": "Information on where Sonarr gets series and episode information", + "MetadataSourceSettingsSeriesSummary": "Information on where {appName} gets series and episode information", "MidseasonFinale": "Midseason Finale", "Min": "Min", "MinimumAge": "Minimum Age", @@ -786,25 +979,34 @@ "Monitor": "Monitor", "MonitorAllEpisodes": "All Episodes", "MonitorAllEpisodesDescription": "Monitor all episodes except specials", + "MonitorAllSeasons": "All Seasons", + "MonitorAllSeasonsDescription": "Monitor all new seasons automatically", "MonitorExistingEpisodes": "Existing Episodes", "MonitorExistingEpisodesDescription": "Monitor episodes that have files or have not aired yet", "MonitorFirstSeason": "First Season", "MonitorFirstSeasonDescription": "Monitor all episodes of the first season. All other seasons will be ignored", "MonitorFutureEpisodes": "Future Episodes", "MonitorFutureEpisodesDescription": "Monitor episodes that have not aired yet", - "MonitorLatestSeason": "Latest Season", - "MonitorLatestSeasonDescription": "Monitor all episodes of the latest season that aired within the last 90 days and all future seasons", + "MonitorLastSeason": "Last Season", + "MonitorLastSeasonDescription": "Monitor all episodes of the last season", "MonitorMissingEpisodes": "Missing Episodes", "MonitorMissingEpisodesDescription": "Monitor episodes that do not have files or have not aired yet", - "MonitorNone": "None", - "MonitorNoneDescription": "No episodes will be monitored", + "MonitorNewSeasons": "Monitor New Seasons", + "MonitorNewSeasonsHelpText": "Which new seasons should be monitored automatically", + "MonitorNoEpisodes": "None", + "MonitorNoEpisodesDescription": "No episodes will be monitored", + "MonitorNoNewSeasons": "No New Seasons", + "MonitorNoNewSeasonsDescription": "Do not monitor any new seasons automatically", "MonitorPilotEpisode": "Pilot Episode", + "MonitorPilotEpisodeDescription": "Only monitor the first episode of the first season", + "MonitorRecentEpisodes": "Recent Episodes", + "MonitorRecentEpisodesDescription": "Monitor episodes aired within the last 90 days and future episodes", "MonitorSelected": "Monitor Selected", "MonitorSeries": "Monitor Series", - "MonitorSpecials": "Monitor Specials", - "MonitorSpecialsDescription": "Monitor all special episodes without changing the monitored status of other episodes", + "MonitorSpecialEpisodes": "Monitor Specials", + "MonitorSpecialEpisodesDescription": "Monitor all special episodes without changing the monitored status of other episodes", "Monitored": "Monitored", - "MonitoredHelpText": "Download monitored episodes in this series", + "MonitoredEpisodesHelpText": "Download monitored episodes in this series", "MonitoredOnly": "Monitored Only", "MonitoredStatus": "Monitored/Status", "Monitoring": "Monitoring", @@ -813,7 +1015,7 @@ "More": "More", "MoreDetails": "More details", "MoreInfo": "More Info", - "MountHealthCheckMessage": "Mount containing a series path is mounted read-only: ", + "MountSeriesHealthCheckMessage": "Mount containing a series path is mounted read-only: ", "MoveAutomatically": "Move Automatically", "MoveFiles": "Move Files", "MoveSeriesFoldersDontMoveFiles": "No, I'll Move the Files Myself", @@ -876,11 +1078,12 @@ "None": "None", "NotSeasonPack": "Not Season Pack", "NotificationStatusAllClientHealthCheckMessage": "All notifications are unavailable due to failures", - "NotificationStatusSingleClientHealthCheckMessage": "Notifications unavailable due to failures: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notifications unavailable due to failures: {notificationNames}", "NotificationTriggers": "Notification Triggers", "NotificationTriggersHelpText": "Select which events should trigger this notification", "NotificationsLoadError": "Unable to load Notifications", - "NotificationsTagsHelpText": "Only send notifications for series with at least one matching tag", + "NotificationsTagsSeriesHelpText": "Only send notifications for series with at least one matching tag", + "NzbgetHistoryItemMessage": "PAR Status: {parStatus} - Unpack Status: {unpackStatus} - Move Status: {moveStatus} - Script Status: {scriptStatus} - Delete Status: {deleteStatus} - Mark Status: {markStatus}", "Ok": "Ok", "OnApplicationUpdate": "On Application Update", "OnEpisodeFileDelete": "On Episode File Delete", @@ -889,7 +1092,7 @@ "OnHealthIssue": "On Health Issue", "OnHealthRestored": "On Health Restored", "OnImport": "On Import", - "OnLatestVersion": "The latest version of Sonarr is already installed", + "OnLatestVersion": "The latest version of {appName} is already installed", "OnManualInteractionRequired": "On Manual Interaction Required", "OnRename": "On Rename", "OnSeriesAdd": "On Series Add", @@ -901,7 +1104,7 @@ "OnlyTorrent": "Only Torrent", "OnlyUsenet": "Only Usenet", "OpenBrowserOnStart": "Open browser on start", - "OpenBrowserOnStartHelpText": " Open a web browser and navigate to the Sonarr homepage on app start.", + "OpenBrowserOnStartHelpText": " Open a web browser and navigate to the {appName} homepage on app start.", "OpenSeries": "Open Series", "OptionalName": "Optional name", "Options": "Options", @@ -934,10 +1137,11 @@ "Parse": "Parse", "ParseModalErrorParsing": "Error parsing, please try again.", "ParseModalHelpText": "Enter a release title in the input above", - "ParseModalHelpTextDetails": "Sonarr will attempt to parse the title and show you details about it", + "ParseModalHelpTextDetails": "{appName} will attempt to parse the title and show you details about it", "ParseModalUnableToParse": "Unable to parse the provided title, please try again.", "PartialSeason": "Partial Season", "Password": "Password", + "PasswordConfirmation": "Password Confirmation", "Path": "Path", "Paused": "Paused", "Peers": "Peers", @@ -950,6 +1154,7 @@ "Permissions": "Permissions", "Port": "Port", "PortNumber": "Port Number", + "PostImportCategory": "Post-Import Category", "PosterOptions": "Poster Options", "PosterSize": "Poster Size", "Posters": "Posters", @@ -979,11 +1184,11 @@ "Protocol": "Protocol", "ProtocolHelpText": "Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases", "Proxy": "Proxy", - "ProxyBadRequestHealthCheckMessage": "Failed to test proxy. Status Code: {0}", + "ProxyBadRequestHealthCheckMessage": "Failed to test proxy. Status Code: {statusCode}", "ProxyBypassFilterHelpText": "Use ',' as a separator, and '*.' as a wildcard for subdomains", - "ProxyFailedToTestHealthCheckMessage": "Failed to test proxy: {0}", + "ProxyFailedToTestHealthCheckMessage": "Failed to test proxy: {url}", "ProxyPasswordHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.", - "ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}", + "ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {proxyHostName}", "ProxyType": "Proxy Type", "ProxyUsernameHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.", "PublishedDate": "Published Date", @@ -994,14 +1199,15 @@ "QualityCutoffNotMet": "Quality cutoff has not been met", "QualityDefinitions": "Quality Definitions", "QualityDefinitionsLoadError": "Unable to load Quality Definitions", - "QualityLimitsHelpText": "Limits are automatically adjusted for the series runtime and number of episodes in the file.", + "QualityLimitsSeriesRuntimeHelpText": "Limits are automatically adjusted for the series runtime and number of episodes in the file.", "QualityProfile": "Quality Profile", - "QualityProfileInUse": "Can't delete a quality profile that is attached to a series, list, or collection", + "QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection", "QualityProfiles": "Quality Profiles", "QualityProfilesLoadError": "Unable to load Quality Profiles", "QualitySettings": "Quality Settings", "QualitySettingsSummary": "Quality sizes and naming", "Queue": "Queue", + "QueueFilterHasNoItems": "Selected queue filter has no items", "QueueIsEmpty": "Queue is empty", "QueueLoadError": "Failed to load Queue", "Queued": "Queued", @@ -1012,13 +1218,12 @@ "Real": "Real", "Reason": "Reason", "RecentChanges": "Recent Changes", - "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {0}. Ensure this path exists and is writable by the user running Sonarr", + "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {path}. Ensure this path exists and is writable by the user running {appName}", "RecyclingBin": "Recycling Bin", "RecyclingBinCleanup": "Recycling Bin Cleanup", "RecyclingBinCleanupHelpText": "Set to 0 to disable automatic cleanup", "RecyclingBinCleanupHelpTextWarning": "Files in the recycle bin older than the selected number of days will be cleaned up automatically", - "RecyclingBinHelpText": "Episode files will go here when deleted instead of being permanently deleted", - "RedownloadFailed": "Redownload Failed", + "RecyclingBinHelpText": "Files will go here when deleted instead of being permanently deleted", "Refresh": "Refresh", "RefreshAndScan": "Refresh & Scan", "RefreshAndScanTooltip": "Refresh information and scan disk", @@ -1036,7 +1241,7 @@ "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", - "ReleaseProfileTagHelpText": "Release profiles will apply to series with at least one matching tag. Leave blank to apply to all series", + "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", "ReleaseRejected": "Release Rejected", @@ -1049,26 +1254,26 @@ "ReleaseTitle": "Release Title", "Reload": "Reload", "RemotePath": "Remote Path", - "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {0} places downloads in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "You are using docker; download client {0} places downloads in {1} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", - "RemotePathMappingDownloadPermissionsHealthCheckMessage": "Sonarr can see but not access downloaded episode {0}. Likely permissions error.", - "RemotePathMappingFileRemovedHealthCheckMessage": "File {0} was removed part way through processing.", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "You are using docker; download client {0} reported files in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Download client {0} reported files in {1} but Sonarr cannot see this directory. You may need to adjust the folder's permissions.", - "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Local download client {0} reported files in {1} but this is not a valid {2} path. Review your download client settings.", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Remote download client {0} reported files in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "Sonarr can see but not access download directory {0}. Likely permissions error.", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "Download client {0} places downloads in {1} but Sonarr cannot see this directory. You may need to adjust the folder's permissions.", + "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "You are using docker; download client {downloadClientName} places downloads in {path} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} can see but not access downloaded episode {path}. Likely permissions error.", + "RemotePathMappingFileRemovedHealthCheckMessage": "File {path} was removed part way through processing.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "You are using docker; download client {downloadClientName} reported files in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Download client {downloadClientName} reported files in {path} but {appName} cannot see this directory. You may need to adjust the folder's permissions.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Local download client {downloadClientName} reported files in {path} but this is not a valid {osName} path. Review your download client settings.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Remote download client {downloadClientName} reported files in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} can see but not access download directory {downloadPath}. Likely permissions error.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Download client {downloadClientName} places downloads in {path} but {appName} cannot see this directory. You may need to adjust the folder's permissions.", "RemotePathMappingHostHelpText": "The same host you specified for the remote Download Client", - "RemotePathMappingImportFailedHealthCheckMessage": "Sonarr failed to import (an) episode(s). Check your logs for details.", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Remote download client {0} places downloads in {1} but this directory does not appear to exist. Likely missing or incorrect remote path mapping.", - "RemotePathMappingLocalPathHelpText": "Path that Sonarr should use to access the remote path locally", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Local download client {0} places downloads in {1} but this is not a valid {2} path. Review your download client settings.", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Remote download client {0} reported files in {1} but this directory does not appear to exist. Likely missing remote path mapping.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} failed to import (an) episode(s). Check your logs for details.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Remote download client {downloadClientName} places downloads in {path} but this directory does not appear to exist. Likely missing or incorrect remote path mapping.", + "RemotePathMappingLocalPathHelpText": "Path that {appName} should use to access the remote path locally", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Local download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your download client settings.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Remote download client {downloadClientName} reported files in {path} but this directory does not appear to exist. Likely missing remote path mapping.", "RemotePathMappingRemotePathHelpText": "Root path to the directory that the Download Client accesses", - "RemotePathMappingWrongOSPathHealthCheckMessage": "Remote download client {0} places downloads in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Remote download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", "RemotePathMappings": "Remote Path Mappings", - "RemotePathMappingsInfo": "Remote Path Mappings are very rarely required, if {app} and your download client are on the same system it is better to match your paths. For more information see the [wiki]({wikiLink})", + "RemotePathMappingsInfo": "Remote Path Mappings are very rarely required, if {appName} and your download client are on the same system it is better to match your paths. For more information see the [wiki]({wikiLink})", "RemotePathMappingsLoadError": "Unable to load Remote Path Mappings", "Remove": "Remove", "RemoveCompleted": "Remove Completed", @@ -1095,11 +1300,11 @@ "RemoveTagsAutomatically": "Remove Tags Automatically", "RemoveTagsAutomaticallyHelpText": "Remove tags automatically if conditions are not met", "RemovedFromTaskQueue": "Removed from task queue", - "RemovedSeriesMultipleRemovedHealthCheckMessage": "Series {0} were removed from TheTVDB", - "RemovedSeriesSingleRemovedHealthCheckMessage": "Series {0} was removed from TheTVDB", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Series {series} were removed from TheTVDB", + "RemovedSeriesSingleRemovedHealthCheckMessage": "Series {series} was removed from TheTVDB", "RemovingTag": "Removing tag", "RenameEpisodes": "Rename Episodes", - "RenameEpisodesHelpText": "Sonarr will use the existing file name if renaming is disabled", + "RenameEpisodesHelpText": "{appName} will use the existing file name if renaming is disabled", "RenameFiles": "Rename Files", "Renamed": "Renamed", "Reorder": "Reorder", @@ -1107,14 +1312,14 @@ "Repeat": "Repeat", "Replace": "Replace", "ReplaceIllegalCharacters": "Replace Illegal Characters", - "ReplaceIllegalCharactersHelpText": "Replace illegal characters. If unchecked, Sonarr will remove them instead", + "ReplaceIllegalCharactersHelpText": "Replace illegal characters. If unchecked, {appName} will remove them instead", "ReplaceWithDash": "Replace with Dash", "ReplaceWithSpaceDash": "Replace with Space Dash", "ReplaceWithSpaceDashSpace": "Replace with Space Dash Space", "Required": "Required", "RequiredHelpText": "This {implementationName} condition must match for the custom format to apply. Otherwise a single {implementationName} match is sufficient.", - "RescanAfterRefreshHelpText": "Rescan the series folder after refreshing the series", - "RescanAfterRefreshHelpTextWarning": "Sonarr will not automatically detect changes to files when not set to 'Always'", + "RescanAfterRefreshHelpTextWarning": "{appName} will not automatically detect changes to files when not set to 'Always'", + "RescanAfterRefreshSeriesHelpText": "Rescan the series folder after refreshing the series", "RescanSeriesFolderAfterRefresh": "Rescan Series Folder after Refresh", "Reset": "Reset", "ResetAPIKey": "Reset API Key", @@ -1127,11 +1332,11 @@ "Restart": "Restart", "RestartLater": "I'll restart later", "RestartNow": "Restart Now", - "RestartReloadNote": "Note: Sonarr will automatically restart and reload the UI during the restore process.", + "RestartReloadNote": "Note: {appName} will automatically restart and reload the UI during the restore process.", "RestartRequiredHelpTextWarning": "Requires restart to take effect", - "RestartRequiredToApplyChanges": "Sonarr requires a restart to apply changes, do you want to restart now?", - "RestartRequiredWindowsService": "Depending which user is running the Sonarr service you may need to restart Sonarr as admin once before the service will start automatically.", - "RestartSonarr": "Restart Sonarr", + "RestartRequiredToApplyChanges": "{appName} requires a restart to apply changes, do you want to restart now?", + "RestartRequiredWindowsService": "Depending which user is running the {appName} service you may need to restart {appName} as admin once before the service will start automatically.", + "RestartSonarr": "Restart {appName}", "Restore": "Restore", "RestoreBackup": "Restore Backup", "RestrictionsLoadError": "Unable to load Restrictions", @@ -1141,8 +1346,8 @@ "RetryingDownloadOn": "Retrying download on {date} at {time}", "RootFolder": "Root Folder", "RootFolderLoadError": "Unable to add root folder", - "RootFolderMissingHealthCheckMessage": "Missing root folder: {0}", - "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {0}", + "RootFolderMissingHealthCheckMessage": "Missing root folder: {rootFolderPath}", + "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {rootFolderPaths}", "RootFolderPath": "Root Folder Path", "RootFolderSelectFreeSpace": "{freeSpace} Free", "RootFolders": "Root Folders", @@ -1170,10 +1375,10 @@ "SearchAll": "Search All", "SearchByTvdbId": "You can also search using TVDB ID of a show. eg. tvdb:71663", "SearchFailedError": "Search failed, please try again later.", - "SearchForAllMissing": "Search for all missing episodes", - "SearchForAllMissingConfirmationCount": "Are you sure you want to search for all {totalRecords} missing episodes?", - "SearchForCutoffUnmet": "Search for all Cutoff Unmet episodes", - "SearchForCutoffUnmetConfirmationCount": "Are you sure you want to search for all {totalRecords} Cutoff Unmet episodes?", + "SearchForAllMissingEpisodes": "Search for all missing episodes", + "SearchForAllMissingEpisodesConfirmationCount": "Are you sure you want to search for all {totalRecords} missing episodes?", + "SearchForCutoffUnmetEpisodes": "Search for all Cutoff Unmet episodes", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Are you sure you want to search for all {totalRecords} Cutoff Unmet episodes?", "SearchForMissing": "Search for Missing", "SearchForMonitoredEpisodes": "Search for monitored episodes", "SearchForMonitoredEpisodesSeason": "Search for monitored episodes in this season", @@ -1196,6 +1401,7 @@ "SeasonPremiere": "Season Premiere", "SeasonPremieresOnly": "Season Premieres Only", "Seasons": "Seasons", + "SecretToken": "Secret Token", "Security": "Security", "Seeders": "Seeders", "SelectAll": "Select All", @@ -1273,11 +1479,11 @@ "ShowSearch": "Show Search", "ShowSearchHelpText": "Show search button on hover", "ShowSeasonCount": "Show Season Count", + "ShowSeriesTitleHelpText": "Show series title under poster", "ShowSizeOnDisk": "Show Size on Disk", "ShowTitle": "Show Title", - "ShowTitleHelpText": "Show series title under poster", "ShowUnknownSeriesItems": "Show Unknown Series Items", - "ShowUnknownSeriesItemsHelpText": "Show items without a series in the queue, this could include removed series, movies or anything else in Sonarr's category", + "ShowUnknownSeriesItemsHelpText": "Show items without a series in the queue, this could include removed series, movies or anything else in {appName}'s category", "ShownClickToHide": "Shown, click to hide", "Shutdown": "Shutdown", "SingleEpisode": "Single Episode", @@ -1286,16 +1492,16 @@ "SizeLimit": "Size Limit", "SizeOnDisk": "Size on disk", "SkipFreeSpaceCheck": "Skip Free Space Check", - "SkipFreeSpaceCheckWhenImportingHelpText": "Use when Sonarr is unable to detect free space from your series root folder", + "SkipFreeSpaceCheckWhenImportingHelpText": "Use when {appName} is unable to detect free space of your root folder during file import", "SkipRedownload": "Skip Redownload", - "SkipRedownloadHelpText": "Prevents Sonarr from trying to download an alternative release for this item", + "SkipRedownloadHelpText": "Prevents {appName} from trying to download an alternative release for this item", "Small": "Small", "SmartReplace": "Smart Replace", "SmartReplaceHint": "Dash or Space Dash depending on name", "Socks4": "Socks4", "Socks5": "Socks5 (Support TOR)", "SomeResultsAreHiddenByTheAppliedFilter": "Some results are hidden by the applied filter", - "SonarrTags": "Sonarr Tags", + "SonarrTags": "{appName} Tags", "Sort": "Sort", "Source": "Source", "SourcePath": "Source Path", @@ -1313,8 +1519,8 @@ "SslPort": "SSL Port", "Standard": "Standard", "StandardEpisodeFormat": "Standard Episode Format", - "StandardTypeDescription": "Episodes released with SxxEyy pattern", - "StandardTypeFormat": "Season and episode numbers ({format})", + "StandardEpisodeTypeDescription": "Episodes released with SxxEyy pattern", + "StandardEpisodeTypeFormat": "Season and episode numbers ({format})", "StartImport": "Start Import", "StartProcessing": "Start Processing", "Started": "Started", @@ -1324,15 +1530,15 @@ "Style": "Style", "SubtitleLanguages": "Subtitle Languages", "Sunday": "Sunday", - "SupportedAutoTaggingProperties": "Sonarr supports the follow properties for auto tagging rules", - "SupportedCustomConditions": "Sonarr supports custom conditions against the release properties below.", - "SupportedDownloadClients": "Sonarr supports many popular torrent and usenet download clients.", + "SupportedAutoTaggingProperties": "{appName} supports the follow properties for auto tagging rules", + "SupportedCustomConditions": "{appName} supports custom conditions against the release properties below.", + "SupportedDownloadClients": "{appName} supports many popular torrent and usenet download clients.", "SupportedDownloadClientsMoreInfo": "For more information on the individual download clients, click the more info buttons.", "SupportedImportListsMoreInfo": "For more information on the individual import lists, click on the more info buttons.", - "SupportedIndexers": "Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.", + "SupportedIndexers": "{appName} supports any indexer that uses the Newznab standard, as well as other indexers listed below.", "SupportedIndexersMoreInfo": "For more information on the individual indexers, click on the more info buttons.", - "SupportedLists": "Sonarr supports multiple lists for importing Series into the database.", "SupportedListsMoreInfo": "For more information on the individual lists, click on the more info buttons.", + "SupportedListsSeries": "{appName} supports multiple lists for importing Series into the database.", "System": "System", "SystemTimeHealthCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", "Table": "Table", @@ -1373,6 +1579,14 @@ "ToggleMonitoredToUnmonitored": "Monitored, click to unmonitor", "ToggleUnmonitoredToMonitored": "Unmonitored, click to monitor", "Tomorrow": "Tomorrow", + "TorrentBlackhole": "Torrent Blackhole", + "TorrentBlackholeSaveMagnetFiles": "Save Magnet Files", + "TorrentBlackholeSaveMagnetFilesExtension": "Save Magnet Files Extension", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension to use for magnet links, defaults to '.magnet'", + "TorrentBlackholeSaveMagnetFilesHelpText": "Save the magnet link if no .torrent file is available (only useful if the download client supports magnets saved to a file)", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Read Only", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Instead of moving files this will instruct {appName} to Copy or Hardlink (depending on settings/system configuration)", + "TorrentBlackholeTorrentFolder": "Torrent Folder", "TorrentDelay": "Torrent Delay", "TorrentDelayHelpText": "Delay in minutes to wait before grabbing a torrent", "TorrentDelayTime": "Torrent Delay: {torrentDelay}", @@ -1391,7 +1605,7 @@ "TypeOfList": "{typeOfList} List", "Ui": "UI", "UiLanguage": "UI Language", - "UiLanguageHelpText": "Language that Sonarr will use for UI", + "UiLanguageHelpText": "Language that {appName} will use for UI", "UiSettings": "UI Settings", "UiSettingsLoadError": "Unable to load UI settings", "UiSettingsSummary": "Calendar, date and color impaired options", @@ -1404,20 +1618,21 @@ "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", "UnableToLoadRootFolders": "Unable to load root folders", - "UnableToUpdateSonarrDirectly": "Unable to update Sonarr directly,", + "UnableToUpdateSonarrDirectly": "Unable to update {appName} directly,", "Unavailable": "Unavailable", "Underscore": "Underscore", "Ungroup": "Ungroup", "Unknown": "Unknown", + "UnknownDownloadState": "Unknown download state: {state}", "UnknownEventTooltip": "Unknown event", "Unlimited": "Unlimited", "UnmappedFilesOnly": "Unmapped Files Only", "UnmappedFolders": "Unmapped Folders", "UnmonitorDeletedEpisodes": "Unmonitor Deleted Episodes", - "UnmonitorDeletedEpisodesHelpText": "Episodes deleted from disk are automatically unmonitored in Sonarr", + "UnmonitorDeletedEpisodesHelpText": "Episodes deleted from disk are automatically unmonitored in {appName}", "UnmonitorSelected": "Unmonitor Selected", - "UnmonitorSpecials": "Unmonitor Specials", - "UnmonitorSpecialsDescription": "Unmonitor all special episodes without changing the monitored status of other episodes", + "UnmonitorSpecialEpisodes": "Unmonitor Specials", + "UnmonitorSpecialsEpisodesDescription": "Unmonitor all special episodes without changing the monitored status of other episodes", "Unmonitored": "Unmonitored", "UnmonitoredOnly": "Unmonitored Only", "UnsavedChanges": "Unsaved Changes", @@ -1428,21 +1643,20 @@ "UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates", "UpdateAvailableHealthCheckMessage": "New update is available", "UpdateFiltered": "Update Filtered", - "UpdateMechanismHelpText": "Use Sonarr's built-in updater or a script", + "UpdateMechanismHelpText": "Use {appName}'s built-in updater or a script", "UpdateMonitoring": "Update Monitoring", "UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process", "UpdateSelected": "Update Selected", - "UpdateSonarrDirectlyLoadError": "Unable to update Sonarr directly,", - "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", - "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.", - "UpdateUINotWritableHealthCheckMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", - "UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", + "UpdateSonarrDirectlyLoadError": "Unable to update {appName} directly,", + "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is not writable by the user '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{startupFolder}' is in an App Translocation folder.", + "UpdateUiNotWritableHealthCheckMessage": "Cannot install update because UI folder '{uiFolder}' is not writable by the user '{userName}'.", "UpdaterLogFiles": "Updater Log Files", "Updates": "Updates", "UpgradeUntil": "Upgrade Until", "UpgradeUntilCustomFormatScore": "Upgrade Until Custom Format Score", - "UpgradeUntilCustomFormatScoreHelpText": "Once this custom format score is reached Sonarr will no longer grab episode releases", - "UpgradeUntilHelpText": "Once this quality is reached Sonarr will no longer download episodes", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Once this custom format score is reached {appName} will no longer grab episode releases", + "UpgradeUntilEpisodeHelpText": "Once this quality is reached {appName} will no longer download episodes", "UpgradeUntilThisQualityIsMetOrExceeded": "Upgrade until this quality is met or exceeded", "UpgradesAllowed": "Upgrades Allowed", "UpgradesAllowedHelpText": "If disabled qualities will not be upgraded", @@ -1454,7 +1668,10 @@ "UseProxy": "Use Proxy", "UseSeasonFolder": "Use Season Folder", "UseSeasonFolderHelpText": "Sort episodes into season folders", + "UseSsl": "Use SSL", "Usenet": "Usenet", + "UsenetBlackhole": "Usenet Blackhole", + "UsenetBlackholeNzbFolder": "Nzb Folder", "UsenetDelay": "Usenet Delay", "UsenetDelayHelpText": "Delay in minutes to wait before grabbing a release from Usenet", "UsenetDelayTime": "Usenet Delay: {usenetDelay}", @@ -1480,6 +1697,7 @@ "Wiki": "Wiki", "WithFiles": "With Files", "WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?", + "XmlRpcPath": "XML RPC Path", "Year": "Year", "Yes": "Yes", "YesCancel": "Yes, Cancel", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 548885bcc..cd266a6ca 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -8,7 +8,7 @@ "Failed": "Fallido", "Example": "Ejemplo", "Ignored": "Ignorado", - "IndexerTagHelpText": "Solo utilizar este indexador para series que coincidan con al menos una etiqueta. Déjelo en blanco para utilizarlo con todas las series.", + "IndexerTagSeriesHelpText": "Solo utilizar este indexador para series que coincidan con al menos una etiqueta. Déjelo en blanco para utilizarlo con todas las series.", "Options": "Opciones", "Paused": "Pausado", "Runtime": "Tiempo de duración", @@ -43,8 +43,8 @@ "ApplicationURL": "URL de la aplicación", "Authentication": "Autenticación", "AuthForm": "Formularios (página de inicio de sesión)", - "AuthenticationMethodHelpText": "Requerir nombre de usuario y contraseña para acceder Sonarr", - "AuthenticationRequired": "Autenticación Requerida", + "AuthenticationMethodHelpText": "Requerir nombre de usuario y contraseña para acceder {appName}", + "AuthenticationRequired": "Autenticación requerida", "AutoTaggingLoadError": "No se pudo cargar el etiquetado automático", "AutoRedownloadFailedHelpText": "Buscar e intentar descargar automáticamente una versión diferente", "Backup": "Copia de seguridad", @@ -73,12 +73,12 @@ "Style": "Estilo", "System": "Sistema", "Tasks": "Tareas", - "Trace": "Trace", + "Trace": "Rastro", "TvdbId": "TVDB ID", "Torrents": "Torrents", "Ui": "UI", "Underscore": "Guion bajo", - "UpdateMechanismHelpText": "Usar el actualizador de Sonarr o un script", + "UpdateMechanismHelpText": "Usar el actualizador de {appName} o un script", "Warn": "Advertencia", "AutoTagging": "Etiquetado Automático", "AddAutoTag": "Añadir Etiqueta Automática", @@ -129,7 +129,7 @@ "Episode": "Episodio", "Activity": "Actividad", "AddNew": "Añadir Nuevo", - "ApplyTagsHelpTextAdd": "Añadir: Añadir a las etiquetas la lista existente de etiquetas", + "ApplyTagsHelpTextAdd": "Añadir: Añadir las etiquetas la lista existente de etiquetas", "ApplyTagsHelpTextRemove": "Eliminar: Eliminar las etiquetas introducidas", "Blocklist": "Bloqueadas", "Grabbed": "Añadido", @@ -165,16 +165,16 @@ "AddRemotePathMappingError": "No se pudo añadir una nueva asignación de ruta remota, inténtelo de nuevo.", "AgeWhenGrabbed": "Antigüedad (cuando se añadió)", "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 Sonarr lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.", - "AnimeTypeDescription": "Episodios lanzados usando un número de episodio absoluto", - "ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {0} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración", + "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", "Scheduled": "Programado", "Season": "Temporada", "Clone": "Clonar", "Connections": "Conexiones", "Dash": "Guión", - "AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de Sonarr. Esto incluye información sobre su navegador, qué páginas de Sonarr WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", + "AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.", "BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas", "BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente", "AddNewSeries": "Añadir Nueva Serie", @@ -209,14 +209,63 @@ "ManageImportLists": "Gestionar Listas de Importación", "ManageDownloadClients": "Gestionar Clientes de Descarga", "MoveAutomatically": "Mover Automáticamente", - "IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {0}.", + "IndexerDownloadClientHealthCheckMessage": "Indexadores con clientes de descarga inválidos: {indexerNames}.", "ManageLists": "Gestionar Listas", "DeleteSelectedImportLists": "Eliminar Lista(s) de Importación", "EditSelectedIndexers": "Editar Indexadores Seleccionados", "ImportScriptPath": "Importar Ruta de Script", "Absolute": "Absoluto", "AddANewPath": "Añadir una nueva ruta", - "AddConditionImplementation": "", - "AppUpdated": "{appName} Actualizado", - "AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no son compatibles directamente al usar el mecanismo de actualización de Docker. Deberás actualizar la imagen del contenedor fuera de {appName} o utilizar un script" + "AddConditionImplementation": "Añadir Condición - {implementationName}", + "AppUpdated": "{appName} Actualizada", + "AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script", + "AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.", + "AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación esté habilitada. Opcionalmente puede desactivar la autenticación desde una dirección local.", + "AuthenticationRequiredPasswordHelpTextWarning": "Introduzca una nueva contraseña", + "AuthenticationRequiredUsernameHelpTextWarning": "Introduzca un nuevo nombre de usuario", + "AuthenticationMethod": "Método de autenticación", + "AddConnectionImplementation": "Añadir Conexión - {implementationName}", + "AddDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}", + "VideoDynamicRange": "Video de Rango Dinámico", + "AuthenticationMethodHelpTextWarning": "Por favor selecciona un método válido de autenticación", + "AddCustomFilter": "Añadir filtro personalizado", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Puntuación mínima de formato personalizado", + "CountIndexersSelected": "{count} indexador(es) seleccionado(s)", + "CouldNotFindResults": "No se pudieron encontrar resultados para '{term}'", + "CountImportListsSelected": "{count} lista(s) de importación seleccionada(s)", + "DelayingDownloadUntil": "Retrasar la descarga hasta {date} a {time}", + "DeleteIndexerMessageText": "Seguro que quieres eliminar el indexer '{name}'?", + "BlocklistLoadError": "No se han podido cargar las bloqueadas", + "BypassDelayIfAboveCustomFormatScore": "Omitir si está por encima de la puntuación del formato personalizado", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para evitar el retraso del protocolo preferido", + "DeleteDownloadClientMessageText": "¿Seguro que quieres eliminar el cliente de descargas '{name}'?", + "DeleteNotificationMessageText": "¿Seguro que quieres eliminiar la notificación '{name}'?", + "DefaultNameCopiedSpecification": "{name} - Copia", + "DefaultNameCopiedProfile": "{name} - Copia", + "DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{name}'?", + "DeleteQualityProfileMessageText": "¿Seguro que quieres eliminar el perfil de calidad {name}?", + "DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{path}'?", + "DeleteSelectedDownloadClientsMessageText": "¿Estas seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?", + "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y necesitará ser recargada para restaurar su funcionalidad.", + "CalendarOptions": "Opciones de Calendario", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado", + "Default": "Por defecto", + "DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{name}'?", + "DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?", + "AddImportListImplementation": "Añadir lista de importación - {implementationName}", + "AddIndexerImplementation": "Añadir Indexador - {implementationName}", + "AutoRedownloadFailed": "La descarga ha fallado", + "ConnectionLostReconnect": "Radarr intentará conectarse automáticamente, o haz clic en el botón de recarga abajo.", + "CustomFormatJson": "Formato JSON personalizado", + "CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)", + "DeleteImportList": "Eliminar Lista(s) de Importación", + "DeleteImportListMessageText": "Seguro que quieres eliminar la lista '{name}'?", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "La búsqueda automática para intentar descargar una versión diferente cuando en la búsqueda interactiva se obtiene una versión fallida", + "AutoRedownloadFailedFromInteractiveSearch": "Fallo al volver a descargar desde la búsqueda interactiva", + "DeleteSelectedIndexersMessageText": "Seguro que quieres eliminar {count} indexer seleccionado(s)?", + "DeleteSelectedImportListsMessageText": "Seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?", + "DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada", + "DeleteTagMessageText": "¿Seguro que quieres eliminar la etiqueta '{label}'?", + "DisabledForLocalAddresses": "Desactivado para direcciones locales", + "DeletedReasonManual": "El archivo fue borrado por vía UI" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index 505f3f1bd..3806de293 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1,15 +1,70 @@ { - "BlocklistReleaseHelpText": "Estää julkaisun automaattisen uudelleensieppauksen.", - "RecycleBinUnableToWriteHealthCheckMessage": "Määritettyyn roskakorikansioon ei voi tallentaa: {0}. Varmista, että sijainti on olemassa ja että käyttäjällä on kirjoitusoikeus kansioon.", - "RemotePathMappingDownloadPermissionsHealthCheckMessage": "Ladattu jakso \"{0}\" näkyy, mutta sitä ei voida käyttää. Todennäköinen syy on sijainnin käyttöoikeusvirhe.", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Etsii kohdetta uudelleen ja estää {appName}ia sieppaamasta tätä julkaisua automaattisesti uudelleen.", + "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}\", muttei voi käyttää sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "Added": "Lisätty", "AppDataLocationHealthCheckMessage": "Päivitystä ei sallita, jotta AppData-kansion poisto päivityksen yhteydessä voidaan estää.", "DownloadClientSortingHealthCheckMessage": "", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS-synkronointia käyttäviä tietolähteitä määritetty, jonka vuoksi uusia julkaisuja ei siepata automaattisesti.", - "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalista hakua varten ei ole määritetty tietolähteitä, jonka vuoksi haku ei löydä tuloksia.", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{0}\" ilmoitti tiedostosijainniksi \"{1}\", mutta kansiota ei nähdä. Saatat joutua muokkaamaan kansion käyttöoikeuksia.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "Ladatauskansio \"{0}\" näkyy, mutta sitä ei voida käyttää. Todennäköinen syy on sijainnin käyttöoikeusvirhe.", - "RemotePathMappingImportFailedHealthCheckMessage": "Jaksojen tuonti epäonnistui. Katso tarkemmat tiedot lokista.", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{0}\" tallentaa latauksen sijaintiin \"{1}\", mutta kansiota ei nähdä. Saatat joutua muokkaamaan kansion käyttöoikeuksia.", - "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattista hakua varten ei ole määritetty tietolähteitä, jonka vuoksi haku ei löydä tuloksia." + "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS-synkronointia käyttäviä tietolähteitä ei ole määritetty, jonka vuoksi uusia julkaisuja ei siepata automaattisesti.", + "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalista hakua varten ei ole määritetty tietolähteitä, eikä manuaalinen haku sen vuoksi löydä tuloksia.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta {appName} ei näe kansiota. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} näkee ladatauskansion \"{downloadPath}\" näkyy, muttei voi käyttää sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "Jaksojen tuonti epäonnistui. Katso tarkemmat tiedot lokista.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Lataustyökalu \"{downloadClientName}\" tallentaa latauksen sijaintiin \"{path}\", mutta {appName} ei näe sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", + "IndexerSearchNoAutomaticHealthCheckMessage": "Automaattista hakua varten ei ole määritetty tietolähteitä, eikä automaattinen haku sen vuoksi löydä tuloksia.", + "AgeWhenGrabbed": "Ikä (sieppaushetkellä)", + "GrabId": "Sieppaustunniste", + "BindAddressHelpText": "Toimiva IP-osoite, \"localhost\" tai \"*\" (tähti) kaikille verkkoliitännöille.", + "BrowserReloadRequired": "Käyttöönotto vaatii selaimen sivupäivityksen.", + "CustomFormatHelpText": "Julkaisut pisteteytetään täsmäävien mukautettujen muotojen pisteiden summaa. Jos uusi julkaisu parantaisi pisteytystä samalla tai paremmalla laadulla, Radarr sieppaa sen.", + "RemotePathMappingHostHelpText": "Sama osoite, joka on määritty etälataustyökalulle.", + "AudioLanguages": "Äänen kielet", + "Grabbed": "Siepattu", + "GrabSelected": "Sieppaa valitut", + "GrabRelease": "Sieppaa julkaisu", + "Hostname": "Osoite", + "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": "Käänteisen välityspalvelimen tuki (esim. \"http://[host]:[port]/[urlBase]\"). Käytä oletusta jättämällä tyhjäksi.", + "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", + "Language": "Kieli", + "Languages": "Kielet", + "MultiLanguages": "Monikielinen", + "Permissions": "Käyttöoikeudet", + "RestartRequiredWindowsService": "Jotta palvelu käynnistyisi automaattisesti, voi suorittavasta käyttäjästä riippuen olla tarpeellista suorittaa sovellus kerran järjestelmänvalvojan oikeuksilla.", + "SelectLanguageModalTitle": "{modalTitle} - Valitse kieli", + "SelectLanguage": "Valitse kieli", + "SelectLanguages": "Valitse kielet", + "ShowRelativeDatesHelpText": "Näytä suhteutetut (tänään/eilen/yms.) absoluuttisten sijaan", + "SetPermissions": "Määritä käyttöoikeudet", + "Style": "Ulkoasu", + "SubtitleLanguages": "Tekstityskielet", + "TimeFormat": "Kellonajan esitys", + "UiLanguage": "Käyttöliittymän kieli", + "UiLanguageHelpText": "Käyttöliittymä näytetään tällä kielellä.", + "AutomaticUpdatesDisabledDocker": "Suoraa automaattista päivitystä ei tueta käytettäessä Dockerin päivitysmekanismia. Joko Docker-säiliö on päivitettävä {appName}in ulkopuolella tai päivitys on suoritettava skriptillä.", + "AddListExclusionSeriesHelpText": "Estä sarjan lisääminen {appName}iin listoilta", + "AppUpdated": "{appName} on päivitetty", + "AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana {appName}in käyttöön.", + "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja käytettävyyden palauttamiseksi se on ladattava uudelleen.", + "DeleteTag": "Poista tunniste", + "AppUpdatedVersion": "{appName} on päivitetty versioon {version} ja se on käynnistettävä uudelleen uusia muutoksia varten. ", + "ThemeHelpText": "Vaihda sovelluksen käyttöliittymän ulkoasu. \"Automaattinen\" vaihtaa vaalean ja tumman tilan välillä järjestelmän teeman mukaan. Innoittanut Theme.Park.", + "AnalyticsEnabledHelpText": "Lähetä nimettömiä käyttö- ja virhetietoja palvelimillemme. Tämä sisältää tietoja selaimestasi, käyttöliittymän sivujen käytöstä, virheraportoinnista, käyttöjärjestelmästä ja suoritusalustasta. Käytämme näitä tietoja ominaisuuksien ja vikakorjausten painotukseen.", + "EnableColorImpairedMode": "Heikentyneen värinäön tila", + "EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihauille, jotka suoritetaan käyttöliittymästä tai {appName}in toimesta.", + "InvalidUILanguage": "Käytöliittymän kielivalinta on virheellinen. Korjaa se ja tallenna asetukset.", + "AuthenticationRequiredWarning": "Etäkäytön estämiseksi ilman tunnistautumista {appName} vaatii nyt todennuksen käyttöönoton. Todennus voidaan poistaa käytöstä paikallisille osoitteille.", + "IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä lataustyökalu", + "ProfilesSettingsSummary": "Laatu-, kieli-, viive- ja julkaisuprofiilit.", + "ConnectionLostReconnect": "{appName} pyrkii ajoittain muodostamaan yhteyden automaattisesti tai sitä voidaan yrittää manuaalisesti painamalla alta \"Lataa uudelleen\".", + "LanguagesLoadError": "Kielien lataus ei onnistu", + "OverrideGrabNoLanguage": "Ainakin yksi kieli on valittava.", + "ChmodFolderHelpTextWarning": "Tämä toimii vain, jos käyttäjä suorittaa {appName}in tiedoston omistajana. Parempi vaihtoehto on varmistaa, että lataustyökalu asettaa oikeudet oikein.", + "InteractiveImportNoLanguage": "Kielet on valittavat jokaiselle valitulle tiedostolle", + "MediaInfoFootNote": "MediaInfo Full, AudioLanguages ja SubtitleLanguages tukevat \":EN+DE\" jälkiliitettä ja mahdollistaa tiedostonimeen sisältyvien kielten suodatuksen. \"-\" ohittaa tietyt kielet (e.g. \"-EN\") ja \"+\"-pääte (e.g. \":FI+\") tuottaa ohitettavista kielistä riippuen \"[FI]\", \"[FI+--]\" tai \"[--]\". Esimerkiksi \"{MediaInfo Full:FI+EN}\".", + "Host": "Osoite" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 3d36f819c..cdc550840 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1,17 +1,17 @@ { "Language": "Langue", - "UiLanguage": "UI Langue", + "UiLanguage": "Langue de l'interface utilisateur", "Added": "Ajouté", - "ApiKeyValidationHealthCheckMessage": "Veuillez mettre à jour votre clé API pour qu'elle contienne au moins {0} caractères. Vous pouvez le faire via les paramètres ou le fichier de configuration", + "ApiKeyValidationHealthCheckMessage": "Veuillez mettre à jour votre clé API pour qu'elle contienne au moins {length} caractères. Vous pouvez le faire via les paramètres ou le fichier de configuration", "AppDataLocationHealthCheckMessage": "La mise à jour ne sera pas possible afin empêcher la suppression de AppData lors de la mise à jour", "ApplyChanges": "Appliquer les modifications", "AutomaticAdd": "Ajout automatique", "CountSeasons": "{count} saisons", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Aucun client de téléchargement disponible", "Add": "Ajouter", - "AddingTag": "Ajouter un tag", + "AddingTag": "Ajout d'une étiquette", "Apply": "Appliquer", - "ApplyTags": "Appliquer les tags", + "ApplyTags": "Appliquer les étiquettes", "Activity": "Activité", "About": "À propos", "Actions": "Actions", @@ -33,14 +33,14 @@ "Analytics": "Statistiques", "ApiKey": "Clé API", "ApplicationURL": "URL de l'application", - "ApplicationUrlHelpText": "URL externe de cette application, y compris http(s)://, le port ainsi que la base de URL", - "AuthBasic": "Authentification de base (Basic) (popup dans le navigateur)", - "AuthForm": "Authentification par un formulaire (page de connexion)", + "ApplicationUrlHelpText": "L'URL externe de cette application, y compris http(s)://, le port ainsi que la base de URL", + "AuthBasic": "Basique (fenêtre surgissante du navigateur)", + "AuthForm": "Formulaire (page de connexion)", "Authentication": "Authentification", "Automatic": "Automatique", "AutomaticSearch": "Recherche automatique", "BackupIntervalHelpText": "Intervalle entre les sauvegardes automatiques", - "BindAddressHelpText": "Adresse IP valide, localhost ou '*' pour toutes les interfaces", + "BindAddressHelpText": "Adresse IP valide, localhost ou « * » pour toutes les interfaces", "Branch": "Branche", "BranchUpdateMechanism": "Branche utilisée par le mécanisme de mise à jour extérieur", "BypassDelayIfHighestQuality": "Contournement si la qualité est la plus élevée", @@ -51,12 +51,12 @@ "CloneIndexer": "Dupliqué l'indexeur", "CloneProfile": "Dupliqué le profil", "AddRootFolder": "Ajouter un dossier racine", - "ApplyTagsHelpTextHowToApplyIndexers": "Comment appliquer des tags aux indexeurs sélectionnés", + "ApplyTagsHelpTextHowToApplyIndexers": "Comment appliquer des étiquettes aux indexeurs sélectionnés", "AudioInfo": "Info audio", "Cancel": "Annuler", - "ApplyTagsHelpTextAdd": "Ajouter : Ajouter les tags à la liste de tags existantes", - "ApplyTagsHelpTextRemove": "Suprimer : Suprime les étiquettes renseignées", - "AddNew": "Ajouter un nouveau", + "ApplyTagsHelpTextAdd": "Ajouter : ajoute les étiquettes à la liste de étiquettes existantes", + "ApplyTagsHelpTextRemove": "Supprimer : supprime les étiquettes renseignées", + "AddNew": "Ajouter une nouvelle", "Backup": "Sauvegarde", "Blocklist": "Liste noire", "Calendar": "Calendrier", @@ -74,7 +74,7 @@ "AddCustomFormatError": "Impossible d'ajouter un nouveau format personnalisé, veuillez réessayer.", "AddIndexerError": "Impossible d'ajouter un nouvelle indexeur, veuillez réessayer.", "AddNewRestriction": "Ajouter une nouvelle restriction", - "AddListError": "Impossible d'ajouter une nouvelle liste", + "AddListError": "Impossible d'ajouter une nouvelle liste, veuillez réessayer.", "AddDownloadClientError": "Impossible d'ajouter un nouveau client de téléchargement, veuillez réessayer.", "AddRemotePathMapping": "Ajouter un mappage des chemins d'accès", "AddNewSeries": "Ajouter une nouvelle série", @@ -96,7 +96,7 @@ "ApplyTagsHelpTextHowToApplySeries": "Comment appliquer des balises à la série sélectionnée", "AuthenticationRequired": "Authentification requise", "AudioLanguages": "Langues audio", - "ApplyTagsHelpTextHowToApplyDownloadClients": "Comment appliquer des balises aux clients de téléchargement sélectionnés", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Comment appliquer des étiquettes aux clients de téléchargement sélectionnés", "AddImportList": "Ajouter une liste d'importation", "AddListExclusionError": "Impossible d'ajouter une nouvelle exclusion de liste, veuillez réessayer.", "AddNotificationError": "Impossible d'ajouter une nouvelle notification, veuillez réessayer.", @@ -108,7 +108,7 @@ "AnimeEpisodeFormat": "Format des épisodes d'animé", "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 à partir des adresses locales.", + "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", "AirDate": "Date de diffusion", "AllTitles": "Tous les titres", @@ -117,10 +117,10 @@ "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.", "AllResultsAreHiddenByTheAppliedFilter": "Tous les résultats sont masqués par le filtre appliqué", - "ApplyTagsHelpTextReplace": "Remplacer : Remplace les balises par les balises saisies (ne pas saisir de balises pour effacer toutes les balises)", + "ApplyTagsHelpTextReplace": "Remplacer : remplace les étiquettes par les étiquettes renseignées (ne pas renseigner d'étiquette pour toutes les effacer)", "Agenda": "Agenda", "AnEpisodeIsDownloading": "Un épisode est en cours de téléchargement", - "AuthenticationMethodHelpText": "Nom d'utilisateur et mot de passe requis pour accéder à {appName}", + "AuthenticationMethodHelpText": "Exiger un nom d'utilisateur et un mot de passe pour accéder à {appName}", "AddedToDownloadQueue": "Ajouté à la file d'attente de téléchargement", "AddImportListImplementation": "Ajouter une liste d'importation - {implementationName}", "Airs": "Diffusions", @@ -130,15 +130,15 @@ "AirsTbaOn": "À confirmer sur {networkLabel}", "AirsTimeOn": "{time} sur {networkLabel}", "AirsTomorrowOn": "Demain à {time} sur {networkLabel}", - "AnimeTypeFormat": "Numéro d'épisode absolu ({format})", + "AnimeEpisodeTypeFormat": "Numéro d'épisode absolu ({format})", "AppUpdatedVersion": "{appName} a été mis à jour à la version `{version}`, afin d'obtenir les derniers changements, vous devrez recharger {appName}. ", - "ApplyTagsHelpTextHowToApplyImportLists": "Comment appliquer des balises aux listes d'importation sélectionnées", + "ApplyTagsHelpTextHowToApplyImportLists": "Comment appliquer des étiquettes aux listes d'importation sélectionnées", "AuthenticationMethod": "Méthode d'authentification", "AuthenticationRequiredPasswordHelpTextWarning": "Saisir un nouveau mot de passe", "AuthenticationRequiredUsernameHelpTextWarning": "Saisir un nouveau nom d'utilisateur", "AddListExclusion": "Ajouter une liste d'exclusion", - "AddNewSeriesHelpText": "Il est facile d'ajouter une nouvelle série, il suffit de taper le nom de la série que vous souhaitez ajouter.", - "AddNewSeriesRootFolderHelpText": "Le sous-dossier '{folder}' sera créé automatiquement", + "AddNewSeriesHelpText": "C'est facile d'ajouter une nouvelle série, il suffit de saisir le nom de la série que vous souhaitez ajouter.", + "AddNewSeriesRootFolderHelpText": "Le sous-dossier « {folder} » sera créé automatiquement", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Lancer la recherche d'épisodes non satisfaits", "AddSeriesWithTitle": "Ajouter {title}", "AddedDate": "Ajouté le : {date}", @@ -147,15 +147,1344 @@ "AlreadyInYourLibrary": "Déjà dans la bibliothèque", "AlternateTitles": "Titres alternatifs", "Anime": "Animé", - "AnimeTypeDescription": "Episodes diffusés en utilisant un numéro d'épisode absolu", + "AnimeEpisodeTypeDescription": "Episodes diffusés en utilisant un numéro d'épisode absolu", "Any": "Tous", - "AppUpdated": "{appName} Mise à jour", - "AddListExclusionHelpText": "Empêcher les séries d'être ajoutées à Sonarr par des listes", + "AppUpdated": "{appName} mis à jour", + "AddListExclusionSeriesHelpText": "Empêcher les séries d'être ajoutées à Sonarr 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.", - "AuthenticationMethodHelpTextWarning": "Veuillez sélectionner une méthode d'authentification valide", - "AuthenticationRequiredHelpText": "Modifier les demandes pour lesquelles l'authentification est requise. Ne changez rien si vous ne comprenez pas les risques.", + "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" + "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.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement $1{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", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Lance une nouvelle recherche pour cet épisode et empêche que cette version soit à nouveau récupérée", + "BuiltIn": "Intégré", + "BrowserReloadRequired": "Rechargement du navigateur requis", + "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score est supérieur au format personnalisé", + "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", + "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é", + "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", + "ChmodFolder": "chmod Dossier", + "ChmodFolderHelpText": "Octal, appliqué lors de l'importation/du renommage des dossiers et fichiers multimédias (sans bits d'exécution)", + "ChmodFolderHelpTextWarning": "Cela ne fonctionne que si l'utilisateur qui exécute sonarr est le propriétaire du fichier. Il est préférable de s'assurer que le client de téléchargement définit correctement les permissions.", + "ChownGroup": "chown Groupe", + "ChownGroupHelpText": "Nom du groupe ou gid. Utilisez gid pour les systèmes de fichiers distants.", + "ChownGroupHelpTextWarning": "Cela ne fonctionne que si l'utilisateur qui exécute sonarr est le propriétaire du fichier. Il est préférable de s'assurer que le client de téléchargement utilise le même groupe que sonarr.", + "ClickToChangeQuality": "Cliquez pour changer la qualité", + "RefreshSeries": "Actualiser la série", + "RecycleBinUnableToWriteHealthCheckMessage": "Impossible d'écrire dans le dossier de la corbeille configuré : {path}. Assurez-vous que ce chemin existe et qu'il est accessible en écriture par l'utilisateur exécutant {appName}", + "RemotePathMappingFileRemovedHealthCheckMessage": "Le fichier {path} a été supprimé en cours de traitement.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Le client de téléchargement {downloadClientName} a signalé des fichiers dans {path} mais {appName} ne peut pas voir ce répertoire. Il se peut que vous deviez ajuster les permissions du dossier.", + "CalendarFeed": "Flux de calendrier {appName}", + "CalendarLegendEpisodeDownloadedTooltip": "L'épisode a été téléchargé et classé", + "CalendarLegendEpisodeDownloadingTooltip": "L'épisode est en cours de téléchargement", + "CalendarLegendSeriesFinaleTooltip": "Fin de série ou de saison", + "CalendarLegendEpisodeMissingTooltip": "L'épisode a été diffusé et est absent du disque", + "CalendarLegendEpisodeOnAirTooltip": "Épisode en cours de diffusion", + "CalendarLegendSeriesPremiereTooltip": "Première de la série ou de la saison", + "CalendarLegendEpisodeUnairedTooltip": "L'épisode n'a pas encore été diffusé", + "CalendarLegendEpisodeUnmonitoredTooltip": "L'épisode n'est pas surveillé", + "CancelProcessing": "Annuler le traitement", + "ChooseImportMode": "Sélectionnez le mode d'importation", + "ClickToChangeLanguage": "Cliquez pour changer de langue", + "ClickToChangeEpisode": "Cliquez pour changer d'épisode", + "ClickToChangeReleaseGroup": "Cliquez pour changer de groupe de diffusion", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement {downloadClientName} a signalé des fichiers dans {path} mais ce n'est pas un chemin {osName} valide. Vérifiez vos mappages de chemins d'accès distants et les paramètres du client de téléchargement.", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Score minimum requis pour le format personnalisé pour ignorer le délai pour le protocole préféré", + "BypassDelayIfHighestQualityHelpText": "Ignorer le délai lorsque la libération a la qualité activée la plus élevée dans le profil de qualité avec le protocole préféré", + "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.", + "Path": "Chemin", + "QueueIsEmpty": "La file d'attente est vide", + "Warn": "Avertissement", + "Week": "Semaine", + "Yesterday": "Hier", + "Password": "Mot de passe", + "Queue": "File d'attente", + "Wanted": "Recherché", + "Remove": "Retirer", + "RemoveSelectedItemQueueMessageText": "Êtes-vous sûr de vouloir supprimer 1 élément de la file d'attente ?", + "DeleteNotification": "Supprimer la notification", + "DeleteNotificationMessageText": "Voulez-vous supprimer la notification « {name} » ?", + "MediaManagement": "Gestion des médias", + "MediaManagementSettingsSummary": "Nommage, paramètres de gestion de fichiers et dossiers racine", + "MinimumFreeSpace": "Espace libre minimum", + "Monitored": "Surveillé", + "NoHistoryFound": "Aucun historique n'a été trouvé", + "NoHistoryBlocklist": "Pas d'historique de liste noire", + "Period": "Période", + "QualityDefinitionsLoadError": "Impossible de charger les définitions de qualité", + "RemoveSelected": "Enlever la sélection", + "UnknownEventTooltip": "Événement inconnu", + "TablePageSizeHelpText": "Nombre d'éléments à afficher sur chaque page", + "Tags": "Tags", + "Unknown": "Inconnu", + "UnmappedFolders": "Dossiers non mappés", + "UpgradesAllowed": "Mises à niveau autorisées", + "VideoDynamicRange": "Plage de dynamique vidéo", + "WouldYouLikeToRestoreBackup": "Souhaitez-vous restaurer la sauvegarde « {name} » ?", + "YesCancel": "Oui, annuler", + "Deleted": "Supprimé", + "Edit": "Modifier", + "RemoveSelectedItem": "Supprimer l'élément sélectionné", + "SubtitleLanguages": "Langues des sous-titres", + "Clone": "Cloner", + "ColonReplacementFormatHelpText": "Changer la manière dont {appName} remplace les « deux-points »", + "DefaultCase": "Casse par défaut", + "Delete": "Supprimer", + "DelayProfiles": "Profils de retard", + "DelayProfilesLoadError": "Impossible de charger les profils de retard", + "DeleteDownloadClientMessageText": "Voulez-vous supprimer le client de téléchargement « {name} » ?", + "DoneEditingGroups": "Terminer la modification des groupes", + "Duplicate": "Dupliqué", + "ExtraFileExtensionsHelpTextsExamples": "Exemples : '.sub, .nfo' ou 'sub,nfo'", + "None": "Aucun", + "NoTagsHaveBeenAddedYet": "Aucune identification n'a été ajoutée pour l'instant", + "QualityLimitsSeriesRuntimeHelpText": "Les limites sont automatiquement ajustées en fonction de la durée de la série et du nombre d'épisodes dans le fichier.", + "QualityProfiles": "Profils de qualité", + "Range": "Gamme", + "Required": "Obligatoire", + "Unmonitored": "Non surveillé", + "UsenetDelay": "Retard Usenet", + "RemoveTagsAutomatically": "Supprimer les balises automatiquement", + "CountDownloadClientsSelected": "{count} client(s) de téléchargement sélectionné(s)", + "DiskSpace": "Espace disque", + "Save": "Sauvegarder", + "RootFolders": "Dossiers racine", + "TestParsing": "Tester le parsage", + "TotalSpace": "Espace total", + "FullSeason": "Saison complète", + "ManualImport": "Importation manuelle", + "ReplaceWithSpaceDash": "Remplacer par un espace puis un tiret", + "Scene": "Scène", + "SelectDownloadClientModalTitle": "{modalTitle} – Sélectionnez le client de téléchargement", + "Shutdown": "Éteindre", + "SonarrTags": "{appName} Tags", + "TagsSettingsSummary": "Voir toutes les balises et comment elles sont utilisées. Les balises inutilisées peuvent être supprimées", + "Ui": "Interface utilisateur", + "Uppercase": "Majuscules", + "TorrentDelayTime": "Retard du torrent : {torrentDelay}", + "Underscore": "Tiret du bas", + "UnmonitorSelected": "Arrêter de surveiller la sélection", + "DefaultNameCopiedProfile": "{name} - Copier", + "DeleteSelectedDownloadClientsMessageText": "Voulez-vous vraiment supprimer {count} client(s) de téléchargement sélectionné(s) ?", + "DefaultNameCopiedSpecification": "{name} - Copier", + "IndexerDownloadClientHealthCheckMessage": "Indexeurs avec des clients de téléchargement invalides : {0].", + "Name": "Nom", + "OrganizeNothingToRename": "Succès ! Mon travail est terminé, pas de fichiers à renommer.", + "PortNumber": "Numéro de port", + "ProfilesSettingsSummary": "Qualité, langue, délais sortie et profils", + "Qualities": "Qualités", + "QualitiesLoadError": "Impossible de charger les qualités", + "RenameFiles": "Renommer les fichiers", + "Restart": "Redémarrer", + "RestartNow": "Redémarrer maintenant", + "SaveSettings": "Enregistrer les paramètres", + "ShowMonitoredHelpText": "Afficher l'état de surveillance sous le poster", + "SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre", + "Sunday": "Dimanche", + "TorrentDelay": "Retard du torrent", + "DownloadClients": "Clients de téléchargements", + "CustomFormats": "Formats perso.", + "NoIndexersFound": "Aucun indexeur n'a été trouvé", + "Profiles": "Profiles", + "Dash": "Tiret", + "DelayProfileProtocol": "Protocole : {preferredProtocol}", + "DeleteBackupMessageText": "Voulez-vous supprimer la sauvegarde « {name} » ?", + "DeleteConditionMessageText": "Voulez-vous vraiment supprimer la condition « {name} » ?", + "DeleteCondition": "Supprimer la condition", + "DeleteBackup": "Supprimer la sauvegarde", + "DeleteIndexerMessageText": "Voulez-vous vraiment supprimer l'indexeur « {name} » ?", + "DeleteIndexer": "Supprimer l'indexeur", + "DeleteSelectedIndexersMessageText": "Voulez-vous vraiment supprimer les {count} indexeur(s) sélectionné(s) ?", + "DeleteRootFolder": "Supprimer le dossier racine", + "DisabledForLocalAddresses": "Désactivée pour les adresses IP locales", + "DeleteTagMessageText": "Voulez-vous vraiment supprimer l'étiquette « {label} » ?", + "Filter": "Filtrer", + "FilterContains": "contient", + "FilterIs": "est", + "FreeSpace": "Espace libre", + "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 ?", + "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 ?", + "RestartRequiredHelpTextWarning": "Nécessite un redémarrage pour prendre effet", + "SelectLanguageModalTitle": "{modalTitle} – Sélectionner la langue", + "SelectFolderModalTitle": "{modalTitle} – Sélectionner un dossier", + "Settings": "Paramètres", + "UsenetDelayTime": "Retard Usenet : {usenetDelay}", + "CountIndexersSelected": "{count} indexeur(s) sélectionné(s)", + "EditGroups": "Modifier les groupes", + "False": "Faux", + "Example": "Exemple", + "FileNameTokens": "Tokens des noms de fichier", + "FileNames": "Noms de fichier", + "Extend": "Étendu", + "FileManagement": "Gestion de fichiers", + "FileBrowser": "Explorateur de fichiers", + "FailedToLoadQualityProfilesFromApi": "Échec du chargement des profils de qualité depuis l'API", + "Filename": "Nom de fichier", + "FailedToLoadTagsFromApi": "Échec du chargement des étiquettes depuis l'API", + "FormatTimeSpanDays": "{days}d {time}", + "FormatShortTimeSpanSeconds": "{seconds} seconde(s)", + "FilterEqual": "égale", + "Implementation": "Mise en œuvre", + "ICalSeasonPremieresOnlyHelpText": "Seul le premier épisode d'une saison sera dans le flux", + "ICalFeed": "Flux iCal", + "History": "Historique", + "HideAdvanced": "Masquer param. av.", + "Large": "Grand", + "LanguagesLoadError": "Impossible de charger les langues", + "IncludeUnmonitored": "Inclure les non surveillés", + "KeyboardShortcutsFocusSearchBox": "Placer le curseur sur la barre de recherche", + "KeyboardShortcutsSaveSettings": "Enregistrer les paramètres", + "ManageClients": "Gérer les clients", + "Logout": "Se déconnecter", + "MediaManagementSettings": "Paramètres de gestion des médias", + "Lowercase": "Minuscule", + "MaximumSizeHelpText": "Taille maximale d'une version à récupérer en Mo. Régler sur zéro pour définir sur illimité", + "MissingNoItems": "Aucun élément manquant", + "MoveAutomatically": "Se déplacer automatiquement", + "MoreInfo": "Plus d'informations", + "NoHistory": "Aucun historique", + "MonitoredStatus": "Surveillé/Statut", + "MultiEpisodeStyle": "Style multi-épisodes", + "NoChanges": "Aucuns changements", + "NoSeasons": "Pas de saisons", + "PosterSize": "Poster taille", + "PosterOptions": "Poster options", + "Posters": "Posters", + "Or": "ou", + "ParseModalHelpTextDetails": "{appName} tentera d'analyser le titre et de vous montrer des détails à ce sujet", + "OrganizeNamingPattern": "Modèle de dénomination : `{episodeFormat}`", + "OneSeason": "1 saison", + "Ok": "Ok", + "PendingChangesDiscardChanges": "Abandonner les modifications et quitter", + "PreferProtocol": "Préféré {preferredProtocol}", + "Refresh": "Rafraîchir", + "PrefixedRange": "Plage préfixée", + "PreferredProtocol": "Protocole préféré", + "ProtocolHelpText": "Choisissez quel(s) protocole(s) utiliser et lequel est préféré lorsque vous choisissez entre des versions par ailleurs égales", + "Quality": "Qualité", + "Presets": "Préconfigurations", + "RemoveSelectedItems": "Supprimer les éléments sélectionnés", + "ReplaceWithSpaceDashSpace": "Remplacer par un espace, un tiret puis un espace", + "RestartLater": "Redémarrer plus tard", + "ReplaceIllegalCharacters": "Remplacer les caractères illégaux", + "ReplaceIllegalCharactersHelpText": "Remplacer les caractères illégaux. Si non coché, {appName} les supprimera", + "Repeat": "Répété", + "Renamed": "Renommer", + "ResetQualityDefinitions": "Réinitialiser les définitions de qualité", + "ResetQualityDefinitionsMessageText": "Voulez-vous vraiment réinitialiser les définitions de qualité ?", + "ResetTitles": "Réinitialiser les titres", + "ResetDefinitionTitlesHelpText": "Réinitialiser les titres de définition ainsi que les valeurs", + "Reset": "Réinitialiser", + "RestartSonarr": "Redémarrer {appName}", + "RootFolderLoadError": "Impossible d'ajouter le dossier racine", + "SelectSeries": "Sélectionnez la série", + "SearchForMissing": "Recherche des manquants", + "SearchAll": "Tout rechercher", + "Series": "Série", + "ShowSearchHelpText": "Afficher le bouton de recherche au survol", + "SmartReplace": "Remplacement intelligent", + "SmartReplaceHint": "Dash ou Space Dash selon le nom", + "Space": "Espace", + "SizeLimit": "Limite de taille", + "TagIsNotUsedAndCanBeDeleted": "L'étiquette n'est pas utilisée et peut être supprimée", + "TagDetails": "Détails de la balise - {label}", + "Title": "Titre", + "Titles": "Titres", + "True": "Vrai", + "UnmonitorDeletedEpisodes": "Annuler la surveillance des épisodes supprimés", + "UnsavedChanges": "Modifications non enregistrées", + "TorrentDelayHelpText": "Délai en minutes avant de récupérer un torrent", + "Username": "Nom d'utilisateur", + "UnselectAll": "Tout désélectionner", + "UpgradesAllowedHelpText": "Si désactivé, les qualités ne seront pas améliorées", + "UsenetDelayHelpText": "Délai en minutes avant de récupérer une release de Usenet", + "InteractiveImport": "Importation interactive", + "InteractiveImportNoImportMode": "Un mode d'importation doit être sélectionné", + "Today": "Aujourd'hui", + "QualityDefinitions": "Définitions de la qualité", + "ManageDownloadClients": "Gérer les clients de téléchargement", + "NoDownloadClientsFound": "Aucun client de téléchargement n'a été trouvé", + "NotificationStatusAllClientHealthCheckMessage": "Toutes les notifications ne sont pas disponibles en raison d'échecs", + "NotificationStatusSingleClientHealthCheckMessage": "Notifications indisponibles en raison d'échecs : {notificationNames}", + "RecentChanges": "Changements récents", + "SetTags": "Définir des balises", + "Replace": "Remplacer", + "ResetAPIKeyMessageText": "Êtes-vous sûr de vouloir réinitialiser votre clé API ?", + "StopSelecting": "Effacer la sélection", + "WhatsNew": "Quoi de neuf ?", + "EditDownloadClientImplementation": "Modifier le client de téléchargement - {implementationName}", + "External": "Externe", + "Monday": "Lundi", + "ShowQualityProfileHelpText": "Afficher le profil de qualité sous l'affiche", + "IncludeCustomFormatWhenRenamingHelpText": "Inclure dans le format de renommage {Formats personnalisés}", + "SelectDropdown": "Sélectionner...", + "InteractiveImportNoFilesFound": "Aucun fichier vidéo n'a été trouvé dans le dossier sélectionné", + "Umask": "Umask", + "OrganizeRelativePaths": "Tous les chemins sont relatifs à : `{chemin}`", + "OverrideGrabNoLanguage": "Au moins une langue doit être sélectionnée", + "OverrideGrabNoQuality": "La qualité doit être sélectionnée", + "NoSeriesFoundImportOrAdd": "Aucune série trouvée. Pour commencer, vous souhaiterez importer votre série existante ou ajouter une nouvelle série.", + "ICalFeedHelpText": "Copiez cette URL dans votre/vos client(s) ou cliquez pour abonner si votre navigateur est compatible avec webcal", + "SeasonFolderFormat": "Format du dossier de saison", + "QualitiesHelpText": "Les qualités plus élevées dans la liste sont plus préférées. Les qualités au sein d’un même groupe sont égales. Seules les qualités vérifiées sont recherchées", + "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)", + "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", + "UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}", + "RssSync": "Synchronisation RSS", + "RestartReloadNote": "Remarque : {appName} redémarrera et rechargera automatiquement l'interface utilisateur pendant le processus de restauration.", + "FileBrowserPlaceholderText": "Commencer à écrire ou sélectionner un chemin ci-dessous", + "ICalLink": "Lien iCal", + "KeyboardShortcuts": "Raccourcis clavier", + "QualityProfilesLoadError": "Impossible de charger les profils de qualité", + "KeyboardShortcutsOpenModal": "Ouvrir cette fenêtre modale", + "ICalShowAsAllDayEvents": "Afficher comme événements d'une journée entière", + "KeyboardShortcutsCloseModal": "Fermer cette fenêtre modale", + "ICalShowAsAllDayEventsHelpText": "Les événements apparaîtront comme des événements d'une journée entière dans votre calendrier", + "Reload": "Recharger", + "ICalTagsSeriesHelpText": "Le flux ne contiendra que les séries avec au moins une étiquette correspondante", + "MediaManagementSettingsLoadError": "Impossible de charger les paramètres de gestion des médias", + "EpisodeNaming": "Nommage des épisodes", + "ConnectionLostReconnect": "{appName} essaiera de se connecter automatiquement, ou vous pouvez cliquer sur « Recharger » en bas.", + "ConnectionLostToBackend": "{appName} a perdu sa connexion au backend et devra être rechargé pour fonctionner à nouveau.", + "CouldNotFindResults": "Aucun résultat pour « {term} »", + "ConnectionLost": "Connexion perdue", + "HistoryLoadError": "Impossible de charger l'historique", + "Hostname": "Hostname", + "ReplaceWithDash": "Remplacer par un tiret", + "Status": "État", + "ColonReplacement": "Remplacement pour le « deux-points »", + "CountSeriesSelected": "{count} série(s) sélectionnée(s)", + "Default": "Par défaut", + "ShowAdvanced": "Afficher les paramètres avancés", + "SeasonPremieresOnly": "Premières saisons uniquement", + "SearchSelected": "Rechercher la sélection", + "Seasons": "Saisons", + "Season": "Saison", + "General": "Général", + "FormatAgeDays": "jours", + "FormatAgeDay": "jour", + "FormatAgeHour": "heure", + "FormatAgeHours": "heures", + "FormatAgeMinute": "minute", + "FormatAgeMinutes": "minutes", + "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "FormatRuntimeHours": "{hours} h", + "FormatRuntimeMinutes": "{minutes} m", + "FormatShortTimeSpanHours": "{hours} heure(s)", + "FormatShortTimeSpanMinutes": "{minutes} minute(s)", + "SeriesID": "ID de série", + "Score": "Score", + "IndexerStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison de pannes", + "RemoveFailedDownloads": "Supprimer les téléchargements ayant échoué", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "La série {0} a été supprimée de TheTVDB", + "FilterGreaterThanOrEqual": "supérieur ou égal à", + "FilterInLast": "à la fin", + "FilterInNext": "ensuite", + "FilterIsNot": "n'est pas", + "FilterLessThan": "moins que", + "FilterLessThanOrEqual": "inférieur ou égal", + "FilterNotEqual": "inégal", + "FilterNotInLast": "pas dans le dernier", + "FilterNotInNext": "pas dans le prochain", + "FilterSeriesPlaceholder": "Série de filtres", + "FilterStartsWith": "commence avec", + "FirstDayOfWeek": "Premier jour de la semaine", + "IconForFinalesHelpText": "Afficher l'icône pour les finales de séries/saisons en fonction des informations disponibles sur les épisodes", + "ImportListExclusionsLoadError": "Impossible de charger les exclusions de la liste d'importation", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Plusieurs dossiers racine sont manquants pour les listes d'importation : {0}", + "ImportListSearchForMissingEpisodes": "Rechercher les épisodes manquants", + "ImportListSearchForMissingEpisodesHelpText": "Une fois la série ajoutée à {appName}, recherchez automatiquement les épisodes manquants", + "ImportListStatusAllUnavailableHealthCheckMessage": "Toutes les listes sont indisponibles en raison d'échecs", + "ImportListStatusUnavailableHealthCheckMessage": "Listes indisponibles en raison d'échecs : {0}", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Activer la gestion des téléchargements terminés si possible", + "Imported": "Importé", + "IndexerDownloadClientHelpText": "Spécifiez quel client de téléchargement est utilisé pour les récupérations à partir de cet indexeur", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison de pannes pendant plus de 6 heures", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs pendant plus de six heures : {0}", + "IndexerPriorityHelpText": "Priorité de l'indexeur de 1 (la plus élevée) à 50 (la plus basse). Valeur par défaut : 25. Utilisé lors de la récupération des versions comme départage pour des versions par ailleurs égales, {appName} utilisera toujours tous les indexeurs activés pour la synchronisation RSS et la recherche", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Tous les indexeurs compatibles RSS sont temporairement indisponibles en raison d'erreurs récentes de l'indexeur", + "IndexerSearchNoAutomaticHealthCheckMessage": "Aucun indexeur disponible avec la recherche automatique activée, {appName} ne fournira aucun résultat de recherche automatique", + "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 : {0}", + "Info": "Information", + "InstallLatest": "Installer le dernier", + "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", + "InteractiveSearchModalHeaderSeason": "Recherche interactive - {season}", + "InteractiveSearchResultsSeriesFailedErrorMessage": "La recherche a échoué car il s'agit d'un {message}. Essayez d'actualiser les informations sur la série et vérifiez que les informations nécessaires sont présentes avant de lancer une nouvelle recherche.", + "InteractiveSearchSeason": "Recherche interactive de tous les épisodes de cette saison", + "Interval": "Intervalle", + "InvalidFormat": "Format invalide", + "InvalidUILanguage": "Votre interface utilisateur est définie sur une langue non valide, corrigez-la et enregistrez vos paramètres", + "Languages": "Langues", + "LastDuration": "Dernière durée", + "LastExecution": "Dernière exécution", + "LastWriteTime": "Heure de la dernière écriture", + "LatestSeason": "Dernière saison", + "LibraryImportTipsDontUseDownloadsFolder": "Ne l'utilisez pas pour importer des téléchargements à partir de votre client de téléchargement, cela concerne uniquement les bibliothèques organisées existantes, pas les fichiers non triés.", + "ListWillRefreshEveryInterval": "La liste sera actualisée tous les {refreshInterval}", + "ListsLoadError": "Impossible de charger les listes", + "Local": "Locale", + "LocalPath": "Chemin local", + "LocalStorageIsNotSupported": "Le stockage local n'est pas pris en charge ou désactivé. Un plugin ou une navigation privée l'ont peut-être désactivé.", + "LogLevel": "Niveau de journalisation", + "LogLevelTraceHelpTextWarning": "La journalisation des traces ne doit être activée que temporairement", + "Logging": "Enregistrement", + "Logs": "Journaux", + "MaintenanceRelease": "Version de maintenance : corrections de bugs et autres améliorations. Voir l'historique des validations Github pour plus de détails", + "ManageEpisodes": "Gérer les épisodes", + "ManageEpisodesSeason": "Gérer les épisodes de cette saison", + "ManageImportLists": "Gérer les listes d'importation", + "ManageIndexers": "Gérer les indexeurs", + "Manual": "Manuel", + "ManualGrab": "Saisie manuelle", + "ManualImportItemsLoadError": "Impossible de charger les éléments d'importation manuelle", + "Mapping": "Cartographie", + "MarkAsFailedConfirmation": "Êtes-vous sûr de vouloir marquer « {sourceTitle} » comme ayant échoué ?", + "MassSearchCancelWarning": "Cette opération ne peut pas être annulée une fois démarrée sans redémarrer {appName} ou désactiver tous vos indexeurs.", + "MaximumLimits": "Limites maximales", + "MaximumSingleEpisodeAge": "Âge maximum d'un seul épisode", + "MaximumSingleEpisodeAgeHelpText": "Lors d'une recherche de saison complète, seuls les packs de saisons seront autorisés lorsque le dernier épisode de la saison est plus ancien que ce paramètre. Série standard uniquement. Utilisez 0 pour désactiver.", + "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}`.", + "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", + "MetadataSourceSettings": "Paramètres de source de métadonnées", + "MetadataSourceSettingsSeriesSummary": "Informations sur l'endroit où {appName} obtient des informations sur les séries et les épisodes", + "MidseasonFinale": "Finale de la mi-saison", + "MinimumCustomFormatScore": "Score minimum de format personnalisé", + "MinimumCustomFormatScoreHelpText": "Score de format personnalisé minimum autorisé à télécharger", + "Mixed": "Mixte", + "Mode": "Mode", + "MonitorAllEpisodes": "Tous les épisodes", + "MonitorFutureEpisodesDescription": "Surveiller les épisodes qui n'ont pas encore été diffusés", + "MonitorNoEpisodesDescription": "Aucun épisode ne sera surveillé", + "MonitorPilotEpisode": "Épisode pilote", + "MonitorSelected": "Surveiller les séries sélectionnées", + "MonitorSeries": "Surveiller les séries", + "MonitorSpecialEpisodes": "Surveiller les épisodes spéciaux", + "MountSeriesHealthCheckMessage": "Le montage contenant un chemin de série est monté en lecture seule : ", + "MultiLanguages": "Multi langues", + "MultiSeason": "Multi saison", + "MustContain": "Doit contenir", + "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.", + "Negated": "Nier", + "Network": "Réseau", + "Never": "Jamais", + "New": "Nouveau", + "NextExecution": "Prochaine exécution", + "NoChange": "Pas de changement", + "NoDelay": "Sans délais", + "NoEpisodeHistory": "Pas d'historique des épisodes", + "NoEpisodesInThisSeason": "Aucun épisode dans cette saison", + "NoEventsFound": "Aucun événement trouvé", + "OnSeriesAdd": "À l'ajout de séries", + "OnSeriesDelete": "Lors de la suppression de la série", + "OnlyTorrent": "Uniquement Torrent", + "OpenBrowserOnStart": "Ouvrir le navigateur au démarrage", + "OpenSeries": "Série ouverte", + "Options": "Options", + "Organize": "Organiser", + "OrganizeLoadError": "Erreur lors du chargement des aperçus", + "OrganizeModalHeader": "Organiser et renommer", + "OrganizeModalHeaderSeason": "Organiser et renommer – {saison}", + "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", + "Original": "Original", + "OverrideAndAddToDownloadQueue": "Remplacer et ajouter à la file d'attente de téléchargement", + "OverrideGrabModalTitle": "Remplacer et récupérer - {title}", + "OverviewOptions": "Options de présentation", + "Parse": "Analyser", + "ParseModalUnableToParse": "Impossible d'analyser le titre fourni, veuillez réessayer.", + "PartialSeason": "Saison partielle", + "PreferUsenet": "Préférer Usenet", + "Preferred": "Préféré", + "ProxyBadRequestHealthCheckMessage": "Échec du test du proxy. Code d'état : {0}", + "ProxyBypassFilterHelpText": "Utilisez ',' comme séparateur et '*.' comme caractère générique pour les sous-domaines", + "ProxyPasswordHelpText": "Il vous suffit de saisir un nom d'utilisateur et un mot de passe si nécessaire. Sinon, laissez-les vides.", + "ProxyResolveIpHealthCheckMessage": "Échec de la résolution de l'adresse IP de l'hôte proxy configuré {0}", + "Rating": "Notation", + "ReadTheWikiForMoreInformation": "Lisez le wiki pour plus d'informations", + "Real": "Réel", + "RecyclingBin": "Poubelle de recyclage", + "RecyclingBinCleanup": "Nettoyage du bac de recyclage", + "RecyclingBinCleanupHelpTextWarning": "Les fichiers dans la corbeille plus anciens que le nombre de jours sélectionné seront nettoyés automatiquement", + "RefreshAndScan": "Actualiser et analyser", + "RefreshAndScanTooltip": "Actualiser les informations et analyser le disque", + "RegularExpression": "Expression régulière", + "RegularExpressionsTutorialLink": "Plus de détails sur les expressions régulières peuvent être trouvés [ici](https://www.regular-expressions.info/tutorial.html).", + "RelativePath": "Chemin relatif", + "Release": "Version", + "ReleaseGroup": "Groupe de versions", + "ReleaseGroups": "Groupes de versions", + "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", + "ReleaseProfiles": "Profils de version", + "ReleaseProfilesLoadError": "Impossible de charger les profils de version", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Le client de téléchargement {0} place les téléchargements dans {1} mais {appName} ne peut pas voir ce répertoire. Vous devrez peut-être ajuster les autorisations du dossier.", + "RemotePathMappingHostHelpText": "Le même hôte que vous avez spécifié pour le client de téléchargement distant", + "ImportListRootFolderMissingRootHealthCheckMessage": "Dossier racine manquant pour la ou les listes d'importation : {0}", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} n'a pas réussi à importer un ou plusieurs épisodes. Vérifiez vos journaux pour plus de détails.", + "IndexerJackettAllHealthCheckMessage": "Indexeurs utilisant le point de terminaison Jackett « all » non pris en charge : {0}", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Aucun indexeur disponible avec la synchronisation RSS activée, {appName} ne récupérera pas automatiquement les nouvelles versions", + "ProxyFailedToTestHealthCheckMessage": "Échec du test du proxy : {0}", + "RemotePathMappingLocalPathHelpText": "Chemin que {appName} doit utiliser pour accéder localement au chemin distant", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Le client de téléchargement local {0} place les téléchargements dans {1}, mais ce n'est pas un chemin {2} valide. Vérifiez les paramètres de votre client de téléchargement.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Le client de téléchargement à distance {0} a signalé des fichiers dans {1} mais ce répertoire ne semble pas exister. Il manque probablement un mappage de chemin distant.", + "RemotePathMappingRemotePathHelpText": "Chemin racine du répertoire auquel accède le client de téléchargement", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Le client de téléchargement à distance {0} place les téléchargements dans {1} mais ce n'est pas un chemin {2} valide. Vérifiez vos mappages de chemins distants et téléchargez les paramètres client.", + "RemotePathMappingsInfo": "Les mappages de chemins distants sont très rarement requis. Si {appName} et votre client de téléchargement sont sur le même système, il est préférable de faire correspondre vos chemins. Pour plus d'informations, consultez le [wiki]({wikiLink})", + "RemotePathMappingsLoadError": "Impossible de charger les mappages de chemin distant", + "RemoveCompleted": "Supprimer terminé", + "RemoveCompletedDownloadsHelpText": "Supprimer les téléchargements importés de l'historique du client de téléchargement", + "RemoveDownloadsAlert": "Les paramètres de suppression ont été déplacés vers les paramètres individuels du client de téléchargement dans le tableau ci-dessus.", + "RemoveFailed": "Échec de la suppression", + "RemoveFilter": "Supprimer le filtre", + "RemoveFromBlocklist": "Supprimer de la liste de blocage", + "RemoveRootFolder": "Supprimer le dossier racine", + "RootFolderMissingHealthCheckMessage": "Dossier racine manquant : {0}", + "RootFolderMultipleMissingHealthCheckMessage": "Plusieurs dossiers racine sont manquants : {0}", + "RssIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur", + "RssSyncInterval": "Intervalle de synchronisation RSS", + "RssSyncIntervalHelpText": "Intervalle en minutes. Réglez sur zéro pour désactiver (cela arrêtera toute capture de libération automatique)", + "RssSyncIntervalHelpTextWarning": "Cela s'appliquera à tous les indexeurs, veuillez suivre les règles énoncées par eux", + "SaveChanges": "Sauvegarder les modifications", + "SceneNumbering": "Numérotation des scènes", + "SearchFailedError": "La recherche a échoué, veuillez réessayer plus tard.", + "SearchForAllMissingEpisodes": "Rechercher tous les épisodes manquants", + "SearchForAllMissingEpisodesConfirmationCount": "Êtes-vous sûr de vouloir rechercher tous les {totalRecords} épisodes manquants ?", + "SearchForCutoffUnmetEpisodes": "Rechercher tous les épisodes Cutoff Unmet", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Êtes-vous sûr de vouloir rechercher tous les épisodes {totalRecords} Cutoff Unmet ?", + "SearchForMonitoredEpisodes": "Rechercher des épisodes surveillés", + "SearchForMonitoredEpisodesSeason": "Rechercher des épisodes surveillés dans cette saison", + "SearchIsNotSupportedWithThisIndexer": "La recherche n'est pas prise en charge avec cet indexeur", + "SeasonFinale": "Saison finale", + "SeasonInformation": "Informations sur la saison", + "SeasonNumber": "Numéro de saison", + "SeasonPassTruncated": "Seules les 25 dernières saisons sont affichées, allez aux détails pour voir toutes les saisons", + "SelectAll": "Tout sélectionner", + "SelectEpisodes": "Sélectionnez les épisodes", + "SelectFolder": "Sélectionner le dossier", + "SelectLanguage": "Choisir la langue", + "SendAnonymousUsageData": "Envoyer des données d'utilisation anonymes", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Les informations sur les séries et les épisodes sont fournies par TheTVDB.com. [Veuillez envisager de les soutenir](https://www.thetvdb.com/subscribe).", + "SeriesCannotBeFound": "Désolé, cette série est introuvable.", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} fichiers d'épisode", + "SeriesDetailsRuntime": "{runtime} Minutes", + "SeriesEditor": "Éditeur de la série", + "SeriesFolderFormat": "Format du dossier de série", + "SeriesFolderFormatHelpText": "Utilisé lors de l'ajout d'une nouvelle série ou du déplacement d'une série via l'éditeur de séries", + "SeriesIndexFooterContinuing": "Suite (Tous les épisodes téléchargés)", + "SeriesIndexFooterDownloading": "Téléchargement (Un ou plusieurs épisodes)", + "SeriesIndexFooterEnded": "Terminé (Tous les épisodes téléchargés)", + "SeriesIndexFooterMissingMonitored": "Épisodes manquants (série surveillée)", + "SeriesIndexFooterMissingUnmonitored": "Épisodes manquants (série non surveillée)", + "SeriesIsMonitored": "La série est surveillée", + "SeriesIsUnmonitored": "La série n'est pas surveillée", + "SeriesLoadError": "Impossible de charger la série", + "SeriesMatchType": "Type de correspondance de série", + "SeriesMonitoring": "Surveillance des séries", + "SeriesPremiere": "Première de la série", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Total : {totalEpisodeCount}, Téléchargement : {downloadingCount})", + "SeriesTitle": "Titre de la série", + "SetReleaseGroupModalTitle": "{modalTitle} – Définir le groupe de versions", + "ShortDateFormat": "Format de date courte", + "ShowBannersHelpText": "Afficher des bannières au lieu de titres", + "ShowDateAdded": "Afficher la date d'ajout", + "ShowEpisodeInformation": "Afficher les informations sur l'épisode", + "ShowEpisodeInformationHelpText": "Afficher le titre et le numéro de l'épisode", + "ShowEpisodes": "Afficher les épisodes", + "ShowMonitored": "Afficher le chemin", + "ShowNetwork": "Afficher le réseau", + "ShowPath": "Afficher le chemin", + "ShowPreviousAiring": "Afficher la diffusion précédente", + "ShowQualityProfile": "Afficher le profil de qualité", + "ShowRelativeDatesHelpText": "Afficher les dates relatives (Aujourd'hui/Hier/etc) ou absolues", + "ShowSearch": "Afficher la recherche", + "ShowSeasonCount": "Afficher le nombre de saisons", + "ShowSizeOnDisk": "Afficher la taille sur le disque", + "ShowTitle": "Montrer le titre", + "ShowSeriesTitleHelpText": "Afficher le titre de la série sous l'affiche", + "ShowUnknownSeriesItems": "Afficher les éléments de série inconnus", + "ShowUnknownSeriesItemsHelpText": "Afficher les éléments sans série dans la file d'attente. Cela peut inclure des séries, des films ou tout autre élément supprimé dans la catégorie de {appName}", + "ShownClickToHide": "Affiché, cliquez pour masquer", + "SkipFreeSpaceCheckWhenImportingHelpText": "À utiliser lorsque {appName} ne parvient pas à détecter l'espace libre de votre dossier racine lors de l'importation de fichiers", + "SkipRedownloadHelpText": "Empêche {appName} d'essayer de télécharger une version alternative pour cet élément", + "Small": "Petit", + "Socks5": "Socks5 (Support TOR)", + "SomeResultsAreHiddenByTheAppliedFilter": "Certains résultats sont masqués par le filtre appliqué", + "Source": "Source", + "SourcePath": "Chemin source", + "SourceRelativePath": "Chemin relatif de la source", + "Special": "spécial", + "SpecialEpisode": "Épisode spécial", + "Specials": "Épisodes spéciaux", + "SpecialsFolderFormat": "Format du dossier des épisodes spéciaux", + "SslCertPassword": "Mot de passe du certificat SSL/TLS", + "SslCertPasswordHelpText": "Mot de passe pour le fichier pfx", + "SslCertPath": "Chemin de certificat SSL/TLS", + "SslCertPathHelpText": "Chemin d'accès au fichier pfx", + "SslPort": "Port SSL/TLS", + "StandardEpisodeFormat": "Format d'épisode standard", + "SupportedCustomConditions": "{appName} prend en charge les conditions personnalisées pour les propriétés de version ci-dessous.", + "SupportedDownloadClients": "{appName} prend en charge de nombreux clients de téléchargement torrent et Usenet populaires.", + "SupportedDownloadClientsMoreInfo": "Pour plus d'informations sur les clients de téléchargement individuels, cliquez sur les boutons Plus d'informations.", + "SupportedIndexers": "{appName} prend en charge tout indexeur utilisant la norme Newznab, ainsi que les autres indexeurs répertoriés ci-dessous.", + "System": "Système", + "TableOptionsButton": "Bouton Options du tableau", + "TablePageSize": "Taille de la page", + "TablePageSizeMinimum": "La taille de la page doit être d'au moins {minimumValue}", + "TagCannotBeDeletedWhileInUse": "La balise ne peut pas être supprimée pendant son utilisation", + "Tasks": "Tâches", + "Test": "Tester", + "TestAll": "Tout tester", + "TestAllIndexers": "Testez tous les indexeurs", + "TheLogLevelDefault": "Le niveau de journalisation est par défaut « Info » et peut être modifié dans [Paramètres généraux] (/settings/general)", + "TheTvdb": "TheTVDB", + "Theme": "Thème", + "Time": "Heure", + "TimeFormat": "Format de l'heure", + "TimeLeft": "Temps restant", + "ToggleMonitoredSeriesUnmonitored ": "Impossible de basculer entre l'état surveillé lorsque la série n'est pas surveillée", + "ToggleMonitoredToUnmonitored": "Surveillé, cliquez pour annuler la surveillance", + "TotalFileSize": "Taille totale des fichiers", + "TotalRecords": "Enregistrements totaux : {totalRecords}", + "Trace": "Tracer", + "Type": "Type", + "UiLanguageHelpText": "Langue que {appName} utilisera pour l'interface utilisateur", + "UiSettingsLoadError": "Impossible de charger les paramètres de l'interface utilisateur", + "UiSettingsSummary": "Options de calendrier, de date et de couleurs dégradées", + "Umask750Description": "Écriture du propriétaire, lecture pour le groupe - {octal}", + "Umask755Description": "Le propriétaire écrit, tous les autres lisent - {octal}", + "Umask770Description": "Propriétaire et groupe écrivent - {octal}", + "Umask775Description": "Propriétaire et groupe écrivent, autre lecture - {octal}", + "UnableToLoadAutoTagging": "Impossible de charger le marquage automatique", + "UnableToLoadBackups": "Impossible de charger les sauvegardes", + "UnableToUpdateSonarrDirectly": "Impossible de mettre à jour directement {appName},", + "UnmonitoredOnly": "Non surveillé uniquement", + "UpdateMechanismHelpText": "Utilisez le programme de mise à jour intégré de {appName} ou un script", + "UpdateSelected": "Mise à jour sélectionnée", + "UpdateSonarrDirectlyLoadError": "Impossible de mettre à jour directement {appName},", + "UpdateStartupNotWritableHealthCheckMessage": "Impossible d'installer la mise à jour car le dossier de démarrage « {0} » n'est pas accessible en écriture par l'utilisateur '{1}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Impossible d'installer la mise à jour, car le dossier de démarrage '{0}' se trouve dans un dossier App Translocation.", + "UpdaterLogFiles": "Journaux du programme de mise à jour", + "UpgradeUntil": "Mise à niveau jusqu'à", + "UpgradeUntilCustomFormatScore": "Mise à niveau jusqu'au score de format personnalisé", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Une fois ce score de format personnalisé atteint, {appName} ne récupérera plus les sorties d'épisodes", + "UrlBase": "URL de base", + "UseHardlinksInsteadOfCopy": "Utiliser les liens durs au lieu de copier", + "UseSeasonFolder": "Utiliser le dossier de la saison", + "UseSeasonFolderHelpText": "Trier les épisodes dans les dossiers des saisons", + "Usenet": "Usenet", + "UsenetDisabled": "Usenet Désactivé", + "UtcAirDate": "Date de diffusion UTC", + "VersionNumber": "Version {version}", + "VideoCodec": "Video Codec", + "VisitTheWikiForMoreDetails": "Visitez le wiki pour plus de details : ", + "WaitingToProcess": "En attente de traitement", + "WeekColumnHeader": "En-tête de colonne de la semaine", + "WhyCantIFindMyShow": "Pourquoi je ne peux pas trouver l'épisode ?", + "Wiki": "Wiki", + "IRCLinkText": "#sonarr sur Libera", + "NoBackupsAreAvailable": "Aucune sauvegarde n'est disponible", + "RemoveFromDownloadClient": "Supprimer du client de téléchargement", + "Formats": "Formats", + "GeneralSettings": "Réglages généraux", + "Genres": "Genres", + "GrabId": "Saisir ID", + "GrabSelected": "Saisir la sélection", + "EpisodeGrabbedTooltip": "Épisode récupéré de {indexer} et envoyé à {downloadClient}", + "HourShorthand": "h", + "ImportCountSeries": "Importer {selectedCount} Séries", + "ImportErrors": "Erreurs d'importation", + "ImportFailed": "Échec de l'importation : {sourceTitle}", + "ImportSeries": "Série d'importation", + "ImportedTo": "Importé vers", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Tous les indexeurs compatibles avec la recherche sont temporairement indisponibles en raison d'erreurs récentes de l'indexeur", + "LastUsed": "Dernière utilisation", + "LiberaWebchat": "Libera Webchat", + "LibraryImportSeriesHeader": "Importez des séries que vous possédez déjà", + "LibraryImportTips": "Quelques conseils pour garantir le bon déroulement de l’importation :", + "LibraryImportTipsQualityInEpisodeFilename": "Assurez-vous que vos fichiers incluent la qualité dans leurs noms de fichiers. par exemple. `épisode.s02e15.bluray.mkv`", + "ManageLists": "Gérer les listes", + "MarkAsFailed": "Marquer comme échec", + "MegabytesPerMinute": "Mégaoctets par minute", + "Message": "Message", + "Metadata": "Metadonnées", + "MinimumFreeSpaceHelpText": "Empêcher l'importation si elle laisse moins d'espace disque disponible", + "MinimumLimits": "Limites minimales", + "MinutesFortyFive": "45 Minutes : {fortyFive}", + "Monitor": "Surveillé", + "MonitorAllEpisodesDescription": "Surveillez tous les épisodes sauf les spéciaux", + "MonitorExistingEpisodes": "Épisodes existants", + "MonitorExistingEpisodesDescription": "Surveiller les épisodes contenant des fichiers ou qui n'ont pas encore été diffusés", + "MonitorFirstSeason": "Première saison", + "MonitorFirstSeasonDescription": "Surveillez tous les épisodes de la première saison. Toutes les autres saisons seront ignorées", + "MonitorFutureEpisodes": "Épisodes futurs", + "MonitorMissingEpisodes": "Épisodes manquants", + "MonitorMissingEpisodesDescription": "Surveiller les épisodes qui n'ont pas de fichiers ou qui n'ont pas encore été diffusés", + "MonitorNoEpisodes": "Aucun", + "MonitorSpecialEpisodesDescription": "Surveillez tous les épisodes spéciaux sans modifier le statut surveillé des autres épisodes", + "MonitoringOptions": "Options de surveillance", + "NextAiring": "Prochaine diffusion", + "OnlyUsenet": "Uniquement Usenet", + "OpenBrowserOnStartHelpText": " Ouvrez un navigateur Web et accédez à la page d'accueil de {appName} au démarrage de l'application.", + "OptionalName": "Nom facultatif", + "Paused": "En pause", + "Pending": "En attente", + "Permissions": "Permissions", + "PreviouslyInstalled": "Précédemment installé", + "Priority": "Priorité", + "PublishedDate": "Date de publication", + "QualitySettings": "Paramètres de qualité", + "QualitySettingsSummary": "Tailles et dénomination de qualité", + "QueueLoadError": "Échec du chargement de la file d'attente", + "Reason": "Raison", + "RecyclingBinCleanupHelpText": "Réglez sur 0 pour désactiver le nettoyage automatique", + "RecyclingBinHelpText": "Les fichiers des épisodes seront placés ici une fois supprimés au lieu d'être définitivement supprimés", + "ReleaseProfileTagSeriesHelpText": "Les profils de version s'appliqueront aux séries avec au moins une balise correspondante. Laisser vide pour appliquer à toutes les séries", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Le client de téléchargement à distance {0} place les téléchargements dans {1} mais ce répertoire ne semble pas exister. Mappage de chemin distant probablement manquant ou incorrect.", + "RemoveCompletedDownloads": "Supprimer les téléchargements terminés", + "RemoveQueueItemConfirmation": "Êtes-vous sûr de vouloir supprimer « {sourceTitle} » de la file d'attente ?", + "RemoveSelectedBlocklistMessageText": "Êtes-vous sûr de vouloir supprimer les éléments sélectionnés de la liste de blocage ?", + "RescanAfterRefreshSeriesHelpText": "Analysez à nouveau le dossier de la série après avoir actualisé la série", + "RetryingDownloadOn": "Nouvelle tentative de téléchargement le {date} à {time}", + "RootFoldersLoadError": "Impossible de charger les dossiers racine", + "SeriesFolderImportedTooltip": "Épisode importé du dossier de la série", + "StandardEpisodeTypeDescription": "Épisodes publiés avec le modèle SxxEyy", + "Rejections": "Rejets", + "RemoveFromQueue": "Supprimer de la file d'attente", + "RemoveQueueItem": "Supprimer – {sourceTitle}", + "Search": "Rechercher", + "Seeders": "Seeders", + "SelectEpisodesModalTitle": "{modalTitle} – Sélectionnez un ou plusieurs épisodes", + "SetReleaseGroup": "Définir le groupe de versions", + "ShowRelativeDates": "Afficher les dates relatives", + "SizeOnDisk": "Taille sur le disque", + "SkipRedownload": "Ignorer le nouveau téléchargement", + "Standard": "Standard", + "StandardEpisodeTypeFormat": "Numéros de saison et d'épisode ({format})", + "StartProcessing": "Démarrer le traitement", + "TableColumnsHelpText": "Choisissez quelles colonnes sont visibles et dans quel ordre elles apparaissent", + "TablePageSizeMaximum": "La taille de la page ne doit pas dépasser {maximumValue}", + "TaskUserAgentTooltip": "Agent utilisateur fourni par l'application qui a appelé l'API", + "Tba": "À déterminer", + "TorrentsDisabled": "Torrents désactivés", + "UiSettings": "Paramètres de l'interface utilisateur", + "Umask777Description": "Tout le monde écrit - {octal}", + "UnmappedFilesOnly": "Fichiers non mappés uniquement", + "UnmonitorSpecialsEpisodesDescription": "Annulez la surveillance de tous les épisodes spéciaux sans modifier le statut surveillé des autres épisodes", + "UpdateUiNotWritableHealthCheckMessage": "Impossible d'installer la mise à jour, car le dossier de l'interface utilisateur '{0}' n'est pas accessible en écriture par l'utilisateur '{1}'.", + "UpgradeUntilEpisodeHelpText": "Une fois cette qualité atteinte, {appName} ne téléchargera plus d'épisodes", + "UpgradeUntilThisQualityIsMetOrExceeded": "Mise à niveau jusqu'à ce que cette qualité soit atteinte ou dépassée", + "UseProxy": "Utiliser le proxy", + "WaitingToImport": "En attente d'import", + "Year": "Année", + "Grabbed": "Saisie", + "HasMissingSeason": "A une saison manquante", + "Here": "ici", + "HiddenClickToShow": "Masqué, cliquez pour afficher", + "HttpHttps": "HTTP(S)", + "ImportListSettings": "Paramètres de liste d'importation", + "ImportListsLoadError": "Impossible de charger les listes d'importation", + "ImportListsSettingsSummary": "Importer depuis une autre instance {appName} ou des listes Trakt et gérer les exclusions de listes", + "ImportScriptPathHelpText": "Le chemin d'accès au script à utiliser pour l'importation", + "Indexers": "Indexeurs", + "InstanceNameHelpText": "Nom de l'instance dans l'onglet et pour le nom de l'application Syslog", + "Max": "Max", + "NoLeaveIt": "Non, laisse tomber", + "NotificationsLoadError": "Impossible de charger les notifications", + "OnEpisodeFileDeleteForUpgrade": "Lors de la suppression du fichier de l'épisode pour la mise à niveau", + "OnGrab": "À saisir", + "OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse", + "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici](http://regexstorm.net/tester).", + "ReleaseProfileIndexerHelpText": "Spécifiez à quel indexeur 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", + "SingleEpisodeInvalidFormat": "Épisode unique : format invalide", + "TestAllLists": "Tester toutes les listes", + "Updates": "Mises à jour", + "IRC": "IRC", + "Runtime": "Durée", + "Twitter": "Twitter", + "MatchedToEpisodes": "Adapté aux épisodes", + "NoEpisodesFoundForSelectedSeason": "Aucun épisode n'a été trouvé pour la saison sélectionnée", + "OnHealthRestored": "Sur la santé restaurée", + "OnImport": "À l'importation", + "OnLatestVersion": "La dernière version de {appName} est déjà installée", + "PreferAndUpgrade": "Préférer et mettre à niveau", + "RejectionCount": "Nombre de rejets", + "Script": "Script", + "SeasonFolder": "Dossier de saison", + "Security": "Sécurité", + "SupportedListsMoreInfo": "Pour plus d'informations sur les listes individuelles, cliquez sur les boutons Plus d'informations.", + "SystemTimeHealthCheckMessage": "L’heure du système est décalée de plus d’un jour. Les tâches planifiées peuvent ne pas s'exécuter correctement tant que l'heure n'est pas corrigée", + "TvdbId": "TVDB ID", + "TvdbIdExcludeHelpText": "L'ID TVDB de la série à exclure", + "TypeOfList": "{typeOfList} Liste", + "Version": "Version", + "SelectSeason": "Sélectionnez la saison", + "SelectSeasonModalTitle": "{modalTitle} – Sélectionnez la saison", + "SeriesDetailsGoTo": "Accédez à {title}", + "SeriesDetailsNoEpisodeFiles": "Aucun fichier d'épisode", + "SeriesDetailsOneEpisodeFile": "1 fichier épisode", + "Unavailable": "Indisponible", + "Ungroup": "Dissocier", + "Folder": "Dossier", + "FullColorEvents": "Événements en couleur", + "GeneralSettingsSummary": "Port, SSL/TLS, nom d'utilisateur/mot de passe, proxy, analyses et mises à jour", + "HistoryModalHeaderSeason": "Historique {season}", + "HistorySeason": "Afficher l'historique de cette saison", + "Images": "Images", + "ImdbId": "IMDb ID", + "ImportUsingScriptHelpText": "Copier des fichiers pour les importer à l'aide d'un script (ex. pour le transcodage)", + "IndexerOptionsLoadError": "Impossible de charger les options de l'indexeur", + "IndexerPriority": "Priorité de l'indexeur", + "IndexersLoadError": "Impossible de charger les indexeurs", + "IndexersSettingsSummary": "Indexeurs et options d'indexeur", + "InteractiveImportNoSeason": "La saison doit être choisie pour chaque fichier sélectionné", + "InteractiveSearch": "Recherche interactive", + "KeyboardShortcutsConfirmModal": "Accepter le mode de confirmation", + "MatchedToSeries": "Adapté à la série", + "Medium": "Moyen", + "MetadataLoadError": "Impossible de charger les métadonnées", + "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}", + "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)", + "MustNotContain": "Ne doit pas contenir", + "NamingSettingsLoadError": "Impossible de charger les paramètres de dénomination", + "NoEpisodeInformation": "Aucune information sur l'épisode n'est disponible.", + "NoResultsFound": "Aucun résultat trouvé", + "NotificationTriggers": "Déclencheurs de notifications", + "OnUpgrade": "Lors de la mise à niveau", + "Other": "Autre", + "OutputPath": "Chemin de sortie", + "OverrideGrabNoEpisode": "Au moins un épisode doit être sélectionné", + "Overview": "Aperçu", + "ParseModalErrorParsing": "Erreur d'analyse, veuillez réessayer.", + "ParseModalHelpText": "Entrez un titre de version dans l'entrée ci-dessus", + "Peers": "Peers", + "ProgressBarProgress": "Barre de progression à {progress} %", + "Progress": "Progression", + "Proper": "Approprié", + "Proxy": "Proxy", + "ProxyUsernameHelpText": "Il vous suffit de saisir un nom d'utilisateur et un mot de passe si nécessaire. Sinon, laissez-les vides.", + "RemotePath": "Chemin distant", + "RemoveFailedDownloadsHelpText": "Supprimer les téléchargements ayant échoué de l'historique du client de téléchargement", + "RenameEpisodes": "Renommer les épisodes", + "ResetDefinitions": "Réinitialiser les définitions", + "SceneInfo": "Informations sur la scène", + "SceneInformation": "Informations sur la scène", + "SceneNumberNotVerified": "Le numéro de scène n'a pas encore été vérifié", + "SeasonDetails": "Détails de la saison", + "SelectQuality": "Sélectionnez la qualité", + "Size": "Taille", + "SourceTitle": "Titre source", + "Style": "Style", + "SupportedImportListsMoreInfo": "Pour plus d'informations sur les listes d'importation individuelles, cliquez sur les boutons Plus d'informations.", + "SupportedIndexersMoreInfo": "Pour plus d'informations sur les indexeurs individuels, cliquez sur les boutons Plus d'informations.", + "TestAllClients": "Tester tous les clients", + "UnableToLoadRootFolders": "Impossible de charger les dossiers racine", + "Unlimited": "Illimité", + "UpcomingSeriesDescription": "La série a été annoncée mais pas encore de date de diffusion exacte", + "UpdateMonitoring": "Surveillance des mises à jour", + "UpdateScriptPathHelpText": "Chemin d'accès à un script personnalisé qui prend un package de mise à jour extrait et gère le reste du processus de mise à jour", + "UrlBaseHelpText": "Pour la prise en charge du proxy inverse, la valeur par défaut est vide", + "WeekColumnHeaderHelpText": "Affiché au-dessus de chaque colonne lorsque la semaine est la vue active", + "WithFiles": "Avec les fichiers", + "Yes": "Oui", + "FilterDoesNotEndWith": "ne se termine pas par", + "FilterIsAfter": "est après", + "FilterIsBefore": "est avant", + "FinaleTooltip": "Finale de la série ou de la saison", + "Fixed": "Fixé", + "Forecast": "Prévision", + "Forums": "Forums", + "From": "De", + "GeneralSettingsLoadError": "Impossible de charger les paramètres généraux", + "HomePage": "Page d'accueil", + "IconForFinales": "Icône pour les épisodes de fin", + "IconForSpecials": "Icône pour les épisodes spéciaux", + "ImportScriptPath": "Chemin du script d'importation", + "IncludeCustomFormatWhenRenaming": "Inclure un format personnalisé lors du changement de nom", + "InteractiveImportNoSeries": "Les séries doivent être choisies pour chaque fichier sélectionné", + "Level": "Niveau", + "LibraryImport": "Importation de bibliothèque", + "ListExclusionsLoadError": "Impossible de charger les exclusions de liste", + "ListQualityProfileHelpText": "Les éléments de la liste des profils 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", + "LogFiles": "Fichiers journaux", + "LogFilesLocation": "Les fichiers journaux se trouvent dans : {location}", + "SupportedListsSeries": "{appName} prend en charge plusieurs listes pour importer des séries dans la base de données.", + "MatchedToSeason": "Adapté à la saison", + "MetadataSource": "Source des métadonnées", + "MoveFiles": "Déplacer des fichiers", + "MultiEpisode": "Multi-épisode", + "MultiEpisodeInvalidFormat": "Épisode multiple : format invalide", + "NoEpisodeOverview": "Aucun aperçu des épisodes", + "OneMinute": "1 Minute", + "OriginalLanguage": "Langue originale", + "Port": "Port", + "PreferTorrent": "Préféré Torrent", + "QualityProfileInUseSeriesListCollection": "Impossible de supprimer un profil de qualité associé à une série, une liste ou une collection", + "ReleaseTitle": "Titre de la version", + "RemovingTag": "Supprimer la balise", + "Result": "Résultat", + "SelectLanguages": "Sélectionnez les langues", + "SeriesTypesHelpText": "Le type de série est utilisé pour renommer, analyser et rechercher", + "SetPermissionsLinuxHelpText": "Chmod doit-il être exécuté lorsque les fichiers sont importés/renommés ?", + "Socks4": "Socks4", + "TableColumns": "Colonnes", + "TableOptions": "Options des tableaux", + "TagsLoadError": "Impossible de charger les balises", + "ThemeHelpText": "Modifiez le thème de l'interface utilisateur de l'application, le thème « Auto » utilisera le thème de votre système d'exploitation pour définir le mode clair ou sombre. Inspiré par Theme.Park", + "UnmonitorSpecialEpisodes": "Ne plus surveiller les épisodes spéciaux", + "Queued": "En file d'attente", + "IconForCutoffUnmet": "Icône pour la date limite non respectée", + "IconForCutoffUnmetHelpText": "Afficher l'icône pour les fichiers lorsque la limite n'a pas été respectée", + "SeriesFinale": "Le final de la série", + "FilterDoesNotStartWith": "ne commence pas par", + "FilterEndsWith": "se termine par", + "FilterEpisodesPlaceholder": "Filtrer les épisodes par titre ou numéro", + "Grab": "Saisir", + "GrabRelease": "Saisir Release", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} n'a pas pu déterminer à quelle série et à quel épisode cette version était destinée. Il est possible que {appName} ne parvienne pas à importer automatiquement cette version. Voulez-vous récupérer « {title} » ?", + "Group": "Groupe", + "HideEpisodes": "Masquer les épisodes", + "ImportExtraFilesEpisodeHelpText": "Importez les fichiers supplémentaires correspondants (sous-titres, informations, etc.) après avoir importé un fichier d'épisode", + "ImportList": "Liste d'importation", + "ImportListExclusions": "Exclusions de la liste d'importation", + "ImportLists": "Importer des listes", + "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é", + "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", + "ListOptionsLoadError": "Impossible de charger les options de la liste", + "ListRootFolderHelpText": "Les éléments de la liste du dossier racine seront ajoutés à", + "MinutesThirty": "30 Minutes : {thirty}", + "Missing": "Manquant", + "MissingEpisodes": "Épisodes manquants", + "MissingLoadError": "Erreur lors du chargement des éléments manquants", + "MonitoredEpisodesHelpText": "Téléchargez les épisodes surveillés de cette série", + "Monitoring": "Surveillance", + "Month": "Mois", + "More": "Plus", + "MoreDetails": "Plus de détails", + "No": "Non", + "NoIssuesWithYourConfiguration": "Aucun problème avec votre configuration", + "NoLimitForAnyRuntime": "Aucune limite pour aucune durée d'exécution", + "NoLinks": "Aucun lien", + "NoLogFiles": "Aucun fichier journal", + "NoMatchFound": "Pas de résultat trouvé !", + "NoMinimumForAnyRuntime": "Aucun minimum pour aucune durée d'exécution", + "NoMonitoredEpisodes": "Aucun épisode surveillé dans cette série", + "NoMonitoredEpisodesSeason": "Aucun épisode surveillé dans cette saison", + "NoSeriesHaveBeenAdded": "Vous n'avez pas encore ajouté de séries, souhaitez-vous d'abord importer tout ou partie de vos séries ?", + "NoUpdatesAreAvailable": "Aucune mise à jour n'est disponible", + "NotSeasonPack": "Pas de pack saisonnier", + "NotificationTriggersHelpText": "Sélectionnez les événements qui doivent déclencher cette notification", + "NotificationsTagsSeriesHelpText": "N'envoyer des notifications que pour les séries avec au moins une balise correspondante", + "OnApplicationUpdate": "Sur la mise à jour de l'application", + "OnEpisodeFileDelete": "Lors de la suppression du fichier de l'épisode", + "OnHealthIssue": "Sur la question de la santé", + "OnManualInteractionRequired": "Sur l'interaction manuelle requise", + "OnRename": "Au renommage", + "PreferredSize": "Taille préférée", + "PreviewRename": "Aperçu Renommer", + "PreviewRenameSeason": "Aperçu Renommer pour cette saison", + "PreviousAiring": "Diffusion précédente", + "PreviousAiringDate": "Diffusion précédente : {date}", + "PriorityHelpText": "Donnez la priorité à plusieurs clients de téléchargement. Round-Robin est utilisé pour les clients ayant la même priorité.", + "ProcessingFolders": "Dossiers de traitement", + "Protocol": "Protocole", + "ProxyType": "Type de mandataire", + "ReleaseSceneIndicatorSourceMessage": "Les versions {message} existent avec une numérotation ambiguë, incapable d'identifier l'épisode de manière fiable.", + "ReleaseSceneIndicatorUnknownMessage": "La numérotation varie pour cet épisode et la version ne correspond à aucun mappage connu.", + "ReleaseSceneIndicatorUnknownSeries": "Épisode ou série inconnu.", + "RemoveFromDownloadClientHelpTextWarning": "La suppression supprimera le téléchargement et le(s) fichier(s) du client de téléchargement.", + "RemoveTagsAutomaticallyHelpText": "Supprimez automatiquement les balises si les conditions ne sont pas remplies", + "RemovedFromTaskQueue": "Supprimé de la file d'attente des tâches", + "RemovedSeriesSingleRemovedHealthCheckMessage": "La série {0} a été supprimée de TheTVDB", + "Reorder": "Réorganiser", + "Repack": "Remballer", + "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.", + "Restore": "Restaurer", + "RestoreBackup": "Restaurer la sauvegarde", + "RestrictionsLoadError": "Impossible de charger les restrictions", + "Retention": "Rétention", + "RetentionHelpText": "Usenet uniquement : définissez-le sur zéro pour définir une rétention illimitée", + "RootFolder": "Dossier racine", + "Scheduled": "Programmé", + "ScriptPath": "Chemin du script", + "SearchByTvdbId": "Vous pouvez également effectuer une recherche en utilisant l'ID TVDB d'une émission. par exemple. base de données tv:71663", + "SeriesTitleToExcludeHelpText": "Le nom de la série à exclure", + "SeriesType": "Type de série", + "SeriesTypes": "Types de séries", + "SetPermissions": "Définir les autorisations", + "SetPermissionsLinuxHelpTextWarning": "Si vous n'êtes pas sûr de l'utilité de ces paramètres, ne les modifiez pas.", + "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", + "ToggleUnmonitoredToMonitored": "Non surveillé, cliquez pour surveiller", + "Torrents": "Torrents", + "Total": "Total", + "Upcoming": "À venir", + "UpdateAutomaticallyHelpText": "Téléchargez et installez automatiquement les mises à jour. Vous pourrez toujours installer à partir du système : mises à jour", + "UpdateAvailableHealthCheckMessage": "Une nouvelle mise à jour est disponible", + "UpdateFiltered": "Mise à jour filtrée", + "IconForSpecialsHelpText": "Afficher l'icône pour les épisodes spéciaux (saison 0)", + "Ignored": "Ignoré", + "IgnoredAddresses": "Adresses ignorées", + "Import": "Importer", + "ImportCustomFormat": "Importer un format personnalisé", + "ImportExtraFiles": "Importer des fichiers supplémentaires", + "Importing": "Importation", + "IndexerSettings": "Paramètres de l'indexeur", + "IndexerTagSeriesHelpText": "Utilisez cet indexeur uniquement pour les séries avec au moins une balise correspondante. Laissez vide pour utiliser toutes les séries.", + "InfoUrl": "URL d'informations", + "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.", + "SelectReleaseGroup": "Sélectionnez un groupe de versions", + "Tomorrow": "Demain", + "OverrideGrabNoSeries": "La série doit être sélectionnée", + "PackageVersion": "Version du paquet", + "PackageVersionInfo": "{packageVersion} de {packageAuthor}", + "QuickSearch": "Recherche rapide", + "ReleaseRejected": "Libération rejetée", + "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}", + "View": "Vues", + "HardlinkCopyFiles": "Lien physique/Copie de fichiers", + "Health": "Santé", + "QualityCutoffNotMet": "Le seuil de qualité n'a pas été atteint", + "RootFolderPath": "Chemin du dossier racine", + "ShowBanners": "Afficher les bannières", + "SearchMonitored": "Recherche surveillée", + "SeasonCount": "Nombre de saisons", + "SeasonNumberToken": "Saison {seasonNumber}", + "SeasonPack": "Pack Saison", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} épisodes téléchargés", + "SeasonPremiere": "Première de la saison", + "SeriesEditRootFolderHelpText": "Le déplacement d'une série vers le même dossier racine peut être utilisé pour renommer les dossiers de séries afin qu'ils correspondent au titre ou au format de dénomination mis à jour", + "FilterGreaterThan": "plus grand que", + "Folders": "Dossiers", + "FullColorEventsHelpText": "Style modifié pour colorer l'intégralité de l'événement avec la couleur d'état, au lieu de simplement le bord gauche. Ne s'applique pas à l'ordre du jour", + "Global": "Global", + "HealthMessagesInfoBox": "Vous pouvez trouver plus d'informations sur la cause de ces messages de contrôle de santé en cliquant sur le lien wiki (icône de livre) à la fin de la ligne, ou en vérifiant vos [journaux]({link}). Si vous rencontrez des difficultés pour interpréter ces messages, vous pouvez contacter notre support, via les liens ci-dessous.", + "LongDateFormat": "Format de date longue", + "PendingDownloadClientUnavailable": "En attente – Le client de téléchargement n'est pas disponible", + "Rss": "RSS", + "Sort": "Trier", + "Uptime": "Disponibilité", + "EnableInteractiveSearch": "Activer la recherche interactive", + "CloneCondition": "État du clone", + "DeleteCustomFormat": "Supprimer le format personnalisé", + "DeleteCustomFormatMessageText": "Êtes-vous sûr de vouloir supprimer le format personnalisé '{customFormatName}' ?", + "Conditions": "Conditions", + "CountImportListsSelected": "{count} liste(s) d'importation sélectionnée(s)", + "DeleteSeriesFolderConfirmation": "Le dossier de la série `{path}` et tout son contenu seront supprimés.", + "DeleteSelectedImportLists": "Supprimer la ou les listes d'importation", + "DeletedReasonEpisodeMissingFromDisk": "{appName} n'a pas pu trouver le fichier sur le disque. Le fichier a donc été dissocié de l'épisode dans la base de données", + "Docker": "Docker", + "DockerUpdater": "Mettez à jour le conteneur Docker pour recevoir la mise à jour", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossible de communiquer avec {downloadClientName}. {errorMessage}", + "EnableAutomaticSearch": "Activer la recherche automatique", + "EpisodeAirDate": "Date de diffusion de l'épisode", + "ErrorLoadingPage": "Une erreur s'est produite lors du chargement de cette page", + "ExternalUpdater": "{appName} est configuré pour utiliser un mécanisme de mise à jour externe", + "DailyEpisodeTypeDescription": "Épisodes diffusés quotidiennement ou moins fréquemment qui utilisent année-mois-jour (2023-08-04)", + "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", + "DeleteRemotePathMapping": "Supprimer le mappage 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.", + "DownloadFailedEpisodeTooltip": "Échec du téléchargement de l'épisode", + "DownloadIgnored": "Téléchargement ignoré", + "DownloadWarning": "Avertissement de téléchargement : {warningMessage}", + "DownloadIgnoredEpisodeTooltip": "Téléchargement de l'épisode ignoré", + "Downloaded": "Téléchargé", + "EditCustomFormat": "Modifier le format personnalisé", + "Downloading": "Téléchargement", + "EditListExclusion": "Modifier l'exclusion de liste", + "EditMetadata": "Modifier les métadonnées {metadataType}", + "EnableAutomaticAdd": "Activer l'ajout automatique", + "EpisodeFileDeleted": "Fichier de l'épisode supprimé", + "EpisodeFileDeletedTooltip": "Fichier de l'épisode supprimé", + "EpisodeFileRenamed": "Fichier de l'épisode renommé", + "EpisodeFileRenamedTooltip": "Fichier de l'épisode renommé", + "EpisodeImported": "Épisode importé", + "EpisodeImportedTooltip": "Épisode téléchargé avec succès et récupéré à partir du client de téléchargement", + "Existing": "Existant", + "Enabled": "Activé", + "CompletedDownloadHandling": "Traitement du téléchargement terminé", + "Component": "Composant", + "Condition": "Condition", + "Connections": "Connexions", + "ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs multimédias et scripts personnalisés", + "CopyToClipboard": "Copier dans le presse-papier", + "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", + "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", + "DeleteImportList": "Supprimer la liste d'importation", + "DeleteImportListExclusion": "Supprimer l'exclusion de la liste d'importation", + "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 ?", + "DoNotPrefer": "Ne préfère pas", + "DoNotUpgradeAutomatically": "Ne pas mettre à niveau automatiquement", + "DownloadClient": "Client de téléchargement", + "DownloadClientSeriesTagHelpText": "Utilisez uniquement ce client de téléchargement pour les séries avec au moins une balise correspondante. Laissez vide pour utiliser toutes les séries.", + "EditDelayProfile": "Modifier le profil de retard", + "EditQualityProfile": "Modifier le profil de qualité", + "EditReleaseProfile": "Modifier le profil de version", + "EnableAutomaticAddSeriesHelpText": "Ajoutez des séries de cette liste à {appName} lorsque les synchronisations sont effectuées via l'interface utilisateur ou par {appName}", + "EnableCompletedDownloadHandlingHelpText": "Importer automatiquement les téléchargements terminés à partir du client de téléchargement", + "EnableColorImpairedModeHelpText": "Style modifié pour permettre aux utilisateurs ayant des difficultés de couleur de mieux distinguer les informations codées par couleur", + "ExtraFileExtensionsHelpText": "Liste de fichiers supplémentaires séparés par des virgules à importer (.nfo sera importé en tant que .nfo-orig)", + "CloneAutoTag": "Cloner la balise automatique", + "Failed": "Échoué", + "Daily": "Tous les jours", + "ContinuingOnly": "Continuer seulement", + "CurrentlyInstalled": "Actuellement installé", + "Donations": "Dons", + "EpisodeCount": "Nombre d'épisodes", + "DeleteSelectedDownloadClients": "Supprimer le(s) client(s) de téléchargement", + "EpisodeNumbers": "Numéro(s) d'épisode", + "DeleteRootFolderMessageText": "Êtes-vous sûr de vouloir supprimer le dossier racine « {path} » ?", + "DeleteSeriesFolderHelpText": "Supprimer le dossier de la série et son contenu", + "DeleteSeriesFolders": "Supprimer les dossiers de séries", + "DeleteSpecification": "Supprimer la spécification", + "Details": "Détails", + "EnableMetadataHelpText": "Activer la création de fichiers de métadonnées pour ce type de métadonnées", + "DownloadClientSortingHealthCheckMessage": "Le client de téléchargement {0} a le tri {1} activé pour la catégorie de {appName}. Vous devez désactiver le tri dans votre client de téléchargement pour éviter les problèmes d'importation.", + "EnableSsl": "Activer SSL/TLS", + "EnableProfile": "Activer profil", + "EpisodeProgress": "Progression de l'épisode", + "FailedToLoadSeriesFromApi": "Échec du chargement de la série depuis l'API", + "FailedToLoadTranslationsFromApi": "Échec du chargement des traductions depuis l'API", + "Table": "Tableau", + "CustomFormatScore": "Partition au format personnalisé", + "DeleteSelectedImportListsMessageText": "Êtes-vous sûr de vouloir supprimer {count} liste(s) d'importation sélectionnée(s) ?", + "DeleteSelectedIndexers": "Supprimer un ou plusieurs indexeurs", + "Connect": "Connecter", + "Connection": "Connexion", + "CustomFormatsLoadError": "Impossible de charger les formats personnalisés", + "Cutoff": "Couper", + "DailyEpisodeFormat": "Format d'épisode quotidien", + "DailyEpisodeTypeFormat": "Date ({format})", + "CreateEmptySeriesFoldersHelpText": "Créer des dossiers de séries manquants lors de l'analyse du disque", + "CreateGroup": "Créer un groupe", + "Database": "Base de données", + "Dates": "Dates", + "CustomFormatJson": "Format personnalisé JSON", + "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 ?", + "DeleteEpisodeFromDisk": "Supprimer l'épisode du disque", + "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer cette exclusion de la liste d'importation ?", + "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} » ?", + "DeleteTag": "Supprimer la balise", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de téléchargement indisponibles en raison d'échecs : {0}", + "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", + "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", + "EditAutoTag": "Modifier la balise automatique", + "EditSelectedImportLists": "Modifier les listes d'importation sélectionnées", + "Duration": "Durée", + "EditRemotePathMapping": "Modifier le mappage de chemin distant", + "EnableAutomaticSearchHelpTextWarning": "Sera utilisé lorsque la recherche interactive est utilisée", + "EpisodeSearchResultsLoadError": "Impossible de charger les résultats pour cette recherche d'épisode. Réessayez plus tard", + "EpisodeHistoryLoadError": "Impossible de charger l'historique de l'épisode", + "EpisodeIsDownloading": "L'épisode est en cours de téléchargement", + "EpisodeIsNotMonitored": "L'épisode n'est pas surveillé", + "EpisodeMissingAbsoluteNumber": "L'épisode n'a pas de numéro d'épisode absolu", + "EpisodeMissingFromDisk": "Épisode manquant sur le disque", + "EpisodeTitleRequired": "Titre de l'épisode requis", + "ErrorLoadingContent": "Une erreur s'est produite lors du chargement de ce contenu", + "Exception": "Exception", + "ExistingSeries": "Série existante", + "ExistingTag": "Balise existante", + "ExportCustomFormat": "Exporter un format personnalisé", + "CopyUsingHardlinksHelpTextWarning": "Parfois, les verrous de fichiers peuvent empêcher de renommer les fichiers en cours de création. Vous pouvez temporairement désactiver l'amorçage et utiliser la fonction de renommage de {appName} pour contourner le problème.", + "FailedToFetchUpdates": "Échec de la récupération des mises à jour", + "FilterDoesNotContain": "ne contient pas", + "EnableMediaInfoHelpText": "Extrayez les informations vidéo telles que la résolution, la durée d'exécution et les informations sur le codec à partir de fichiers. Cela nécessite que {appName} lise des parties du fichier, ce qui peut entraîner une activité élevée du disque ou du réseau pendant les analyses.", + "EnableRssHelpText": "Sera utilisé lorsque {appName} recherche périodiquement des versions via RSS Sync", + "MyComputer": "Mon ordinateur", + "Disabled": "Désactivé", + "Day": "Jour", + "ClickToChangeSeries": "Cliquez pour changer de série", + "CountSelectedFiles": "{selectedCount} fichiers sélectionnés", + "CustomFilters": "Filtres personnalisés", + "CustomFormat": "Format personnalisé", + "CustomFormatHelpText": "{appName} note chaque version en utilisant la somme des scores pour la correspondance des formats personnalisés. Si une nouvelle version devait améliorer le score, avec une qualité identique ou supérieure, alors {appName} la récupérerait.", + "CustomFormatUnknownCondition": "Condition de format personnalisé inconnue '{implémentation}'", + "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", + "EditImportListExclusion": "Modifier l'exclusion de la liste d'importation", + "EditSeries": "Modifier les séries", + "EnableAutomaticSearchHelpText": "Sera utilisé lorsque des recherches automatiques sont effectuées via l'interface utilisateur ou par {appName}", + "EnableColorImpairedMode": "Activer le mode de couleurs altérées", + "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", + "EnableRss": "Activer RSS", + "Ended": "Terminé", + "EndedOnly": "Terminé seulement", + "EndedSeriesDescription": "Aucun épisode ou saison supplémentaire n'est attendu", + "Episode": "Épisode", + "EpisodeFilesLoadError": "Impossible de charger les fichiers d'épisode", + "EpisodeHasNotAired": "L'épisode n'a pas été diffusé", + "EpisodeInfo": "Informations sur l'épisode", + "EpisodeTitle": "Titre de l'épisode", + "EpisodeTitleRequiredHelpText": "Empêcher l'importation pendant 48 heures maximum si le titre de l'épisode est au format de nom et que le titre de l'épisode est à déterminer", + "Episodes": "Épisodes", + "EpisodesLoadError": "Impossible de charger les épisodes", + "Files": "Fichiers", + "Continuing": "Continuer", + "Donate": "Faire un don", + "EditConditionImplementation": "Modifier la condition – {implementationName}", + "EditConnectionImplementation": "Modifier la connexion - {implementationName}", + "EditImportListImplementation": "Modifier la liste d'importation - {implementationName}", + "EditIndexerImplementation": "Modifier l'indexeur - {implementationName}", + "CollapseMultipleEpisodesHelpText": "Réduire plusieurs épisodes diffusés le même jour", + "ClickToChangeSeason": "Cliquez pour changer de saison", + "CollapseAll": "Réduire tout", + "CollapseMultipleEpisodes": "Réduire plusieurs épisodes", + "CollectionsLoadError": "Impossible de charger les collections", + "ConnectSettings": "Paramètres de connexion", + "ContinuingSeriesDescription": "Plus d'épisodes/une autre saison sont attendus", + "EpisodeDownloaded": "Épisode téléchargé", + "CutoffUnmetLoadError": "Erreur lors du chargement des éléments non satisfaits", + "CountSelectedFile": "{selectedCount} fichier sélectionné", + "AutoRedownloadFailed": "Retélécharger les fichiers ayant échoué", + "AutoRedownloadFailedFromInteractiveSearch": "Retélécharger les fichiers ayant échoué depuis la recherche interactive", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Rechercher et tenter automatiquement de télécharger une version différente lorsque la version ayant échoué a été récupérée à partir de la recherche interactive", + "ConditionUsingRegularExpressions": "Cette condition correspond à l'aide d'expressions régulières. Notez que les caractères `\\^$.|?*+()[{` ont des significations particulières et doivent être échappés par un `\\`", + "CutoffUnmet": "Seuil non atteint", + "CutoffUnmetNoItems": "Aucun élément non satisfait", + "Date": "Date", + "DefaultNotFoundMessage": "Vous devez être perdu, rien à voir ici.", + "DeleteAutoTag": "Supprimer la balise automatique", + "DeleteAutoTagHelpText": "Voulez-vous vraiment supprimer la balise automatique « {name} » ?", + "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} » ?", + "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}", + "DeleteSeriesFoldersHelpText": "Supprimez les dossiers de séries et tout leur contenu", + "DeleteSeriesModalHeader": "Supprimer - {titre}", + "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", + "DetailedProgressBarHelpText": "Afficher le texte sur la barre de progression", + "Discord": "Discord", + "DotNetVersion": ".NET", + "Download": "Téléchargement", + "DownloadClientOptionsLoadError": "Impossible de charger les options du client de téléchargement", + "DownloadClientSettings": "Télécharger les paramètres client", + "EditRestriction": "Modifier la restriction", + "EditSelectedSeries": "Modifier la série sélectionnée", + "EditSeriesModalHeader": "Modifier - {titre}", + "Enable": "Activer", + "Error": "Erreur", + "ErrorLoadingContents": "Erreur lors du chargement du contenu", + "ErrorLoadingItem": "Une erreur s'est produite lors du chargement de cet élément", + "ErrorRestoringBackup": "Erreur lors de la restauration de la sauvegarde", + "EventType": "Type d'événement", + "Events": "Événements", + "ExpandAll": "Développer tout", + "FailedToLoadCustomFiltersFromApi": "Échec du chargement des filtres personnalisés à partir de l'API", + "FailedToLoadSonarr": "Échec du chargement de {appName}", + "FailedToLoadSystemStatusFromApi": "Échec du chargement de l'état du système à partir de l'API", + "FailedToLoadUiSettingsFromApi": "Échec du chargement des paramètres de l'interface utilisateur à partir de l'API", + "FailedToUpdateSettings": "Échec de la mise à jour des paramètres", + "FeatureRequests": "Requêtes de nouvelles fonctionnalités", + "File": "Fichier", + "NoImportListsFound": "Aucune liste d'importation trouvée", + "QueueFilterHasNoItems": "Le filtre de file d'attente sélectionné ne contient aucun élément" } diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 17c5293b8..5cbee5d60 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -1,6 +1,6 @@ { "Added": "נוסף", - "ApiKeyValidationHealthCheckMessage": "עדכן בבקשה את מפתח ה־API שלך כדי שיהיה באורך של לפחות {0} תווים. תוכל לעשות זאת בהגדרות או דרך קובץ הקונפיגורציה.", + "ApiKeyValidationHealthCheckMessage": "עדכן בבקשה את מפתח ה־API שלך כדי שיהיה באורך של לפחות {length} תווים. תוכל לעשות זאת בהגדרות או דרך קובץ הקונפיגורציה.", "Add": "הוסף", "Activity": "פעילות", "Indexer": "אינדקסר", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index d96b4dfd5..55db72695 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1,108 +1,139 @@ { - "BlocklistReleaseHelpText": "Megakadályozza, hogy a Sonarr automatikusan újra letöltse ezt a kiadást", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Megakadályozza, hogy a {appName} automatikusan újra letöltse ezt a kiadást", "CloneCondition": "Feltétel klónozása", "CloneCustomFormat": "Egyéni formátum klónozása", "Close": "Bezárás", "Delete": "Törlés", "DeleteCondition": "Feltétel törlése", - "DeleteConditionMessageText": "Biztosan törölni akarod a '{0}' feltételt?", + "DeleteConditionMessageText": "Biztosan törölni akarod a '{name}' feltételt?", "DeleteCustomFormat": "Egyéni formátum törlése", - "DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{0}' egyéni formátumot?", + "DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{customFormatName}' egyéni formátumot?", "ExportCustomFormat": "Egyéni formátum exportálása", - "IndexerJackettAllHealthCheckMessage": "A nem támogatott Jackett 'all' végpontot használó indexelők: {0}", + "IndexerJackettAllHealthCheckMessage": "A nem támogatott Jackett 'all' végpontot használó indexelők: {indexerNames}", "Remove": "Eltávolítás", "RemoveFromDownloadClient": "Eltávolítás a letöltési kliensből", "RemoveFromDownloadClientHelpTextWarning": "A törlés eltávolítja a letöltést és a fájl(okat) a letöltési kliensből.", "RemoveSelectedItem": "Kijelölt elem eltávolítása", "RemoveSelectedItemQueueMessageText": "Biztosan el akar távolítani 1 elemet a várólistáról?", "RemoveSelectedItems": "Kijelölt elemek eltávolítása", - "RemoveSelectedItemsQueueMessageText": "Biztosan el akar távolítani {0} elemet a várólistáról?", + "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 {0} 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 {0} 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 {0} -val.", + "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.", "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltési kliens elérhetetlen meghibásodások miatt", "EditSelectedDownloadClients": "Kiválasztott letöltési kliensek szerkesztése", "EditSelectedImportLists": "Kiválasztott import listák szerkesztése", "EditSelectedIndexers": "Kiválasztott indexelők szerkesztése", - "DownloadClientStatusSingleClientHealthCheckMessage": "Letöltési kliensek elérhetetlenek meghibásodások miatt: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Letöltési kliensek elérhetetlenek meghibásodások miatt: {downloadClientNames}", "EnableAutomaticSearch": "Automatikus keresés engedélyezése", "EditSeries": "Sorozat szerkesztése", "EnableInteractiveSearch": "Interaktív keresés engedélyezése", "Ended": "Vége", "HideAdvanced": "Haladó elrejtése", - "ImportListRootFolderMissingRootHealthCheckMessage": "Hiányzó gyökérmappa a/az {0} importálási listához", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Több gyökérmappa hiányzik a/az {0} importálási listához", + "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", "Enabled": "Engedélyezés", "HiddenClickToShow": "Rejtett, kattints a felfedéshez", "ImportListStatusAllUnavailableHealthCheckMessage": "Minden lista elérhetetlen meghibásodások miatt", - "ImportListStatusUnavailableHealthCheckMessage": "Listák elérhetetlenek meghibásodások miatt: {0}", + "ImportListStatusUnavailableHealthCheckMessage": "Listák elérhetetlenek meghibásodások miatt: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Befejezett letöltés kezelésének engedélyezése, ha lehetséges", "ImportMechanismHandlingDisabledHealthCheckMessage": "Befejezett letöltés kezelésének engedélyezése", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Befejezett letöltés kezelésének engedélyezése, ha lehetséges (Több számítógépen nem támogatott)", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nincsenek elérhető indexelők RSS szinkronizációval, a Sonarr nem fog automatikusan új kiadásokat letölteni", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nincsenek elérhető indexelők RSS szinkronizációval, a {appName} nem fog automatikusan új kiadásokat letölteni", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Az összes RSS-képes indexelő ideiglenesen nem elérhető a legutóbbi indexelő hibák miatt", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodás miatt több, mint 6 órája: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodás miatt több, mint 6 órája: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodás miatt több, mint 6 órája", - "IndexerSearchNoInteractiveHealthCheckMessage": "Nincsenek elérhető indexelők az Interaktív Keresés funkcióval, a Sonarr nem fog interaktív keresési eredményeket szolgáltatni", + "IndexerSearchNoInteractiveHealthCheckMessage": "Nincsenek elérhető indexelők az Interaktív Keresés funkcióval, a {appName} nem fog interaktív keresési eredményeket szolgáltatni", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Az összes keresési képességgel rendelkező indexelő ideiglenesen nem elérhető a legutóbbi indexelő hibák miatt", - "IndexerSearchNoAutomaticHealthCheckMessage": "Nincsenek elérhető indexelők az Automatikus Keresés funkcióval, a Sonarr nem fog automatikus keresési eredményeket szolgáltatni", + "IndexerSearchNoAutomaticHealthCheckMessage": "Nincsenek elérhető indexelők az Automatikus Keresés funkcióval, a {appName} nem fog automatikus keresési eredményeket szolgáltatni", "Language": "Nyelv", - "IndexerStatusUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodások miatt: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodások miatt: {indexerNames}", "IndexerStatusAllUnavailableHealthCheckMessage": "Minden indexelő elérhetetlen meghibásodások miatt", "OneSeason": "1 évad", "OriginalLanguage": "Eredeti nyelv", "Path": "Útvonal", "NextAiring": "Következő rész", "Monitored": "Felügyelt", - "MountHealthCheckMessage": "A sorozat elérési útvonalát tartalmazó kötet csak olvasható módban van csatolva: ", + "MountSeriesHealthCheckMessage": "A sorozat elérési útvonalát tartalmazó kötet csak olvasható módban van csatolva: ", "Network": "Hálózat", "NoSeasons": "Nincsenek évadok", - "ProxyBadRequestHealthCheckMessage": "Sikertelen proxy teszt. Állapotkód: {0}", + "ProxyBadRequestHealthCheckMessage": "Sikertelen proxy teszt. Állapotkód: {statusCode}", "Priority": "Elsőbbség", - "ProxyFailedToTestHealthCheckMessage": "Sikertelen proxy teszt: {0}", - "ProxyResolveIpHealthCheckMessage": "Nem sikerült feloldani a konfigurált proxy kiszolgáló {0} IP-címét", + "ProxyFailedToTestHealthCheckMessage": "Sikertelen proxy teszt: {url}", + "ProxyResolveIpHealthCheckMessage": "Nem sikerült feloldani a konfigurált proxy kiszolgáló {proxyHostName} IP-címét", "PreviousAiring": "Előző rész", - "RecycleBinUnableToWriteHealthCheckMessage": "Nem lehet írni a konfigurált lomtár mappába {0}. Győződjön meg arról, hogy ez az elérési útvonal létezik, és az a felhasználó, aki a Sonarr-t futtatja, írási jogosultsággal rendelkezik", + "RecycleBinUnableToWriteHealthCheckMessage": "Nem lehet írni a konfigurált lomtár mappába {path}. Győződjön meg arról, hogy ez az elérési útvonal létezik, és az a felhasználó, aki a {appName}-t futtatja, írási jogosultsággal rendelkezik", "QualityProfile": "Minőségi profil", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker-t használ; a(z) {0} letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik a konténeren belül. Ellenőrizze a távoli útvonal hozzárendeléseket, és a konténer kötet beállításait.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Docker-t használ; a(z) $1{downloadClientName} letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik a konténeren belül. Ellenőrizze a távoli útvonal hozzárendeléseket, és a konténer kötet beállításait.", "RefreshSeries": "Sorozat frissítése", - "RemotePathMappingFileRemovedHealthCheckMessage": "A(z) {0} fájlt részben feldolgozás közben eltávolították.", - "RemotePathMappingDownloadPermissionsHealthCheckMessage": "A Sonarr látja, de nem tud hozzáférni a letöltött epizódhoz {0}. Valószínűleg jogosultsági hiba.", - "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "A(z) {0} helyi letöltési kliens a fájlokat a(z) {1} mappában jelentette, de ez nem érvényes {2} elérési útvonal. Ellenőrizze a letöltési kliens beállításait.", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "A(z) {0} letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de a Sonarr nem látja ezt a könyvtárat. Lehetséges, hogy be kell állítania a mappa jogosultságait.", - "RemotePathMappingImportFailedHealthCheckMessage": "A Sonarr-nak nem sikerült importálni az epizód(ok)at. Ellenőrizze a naplókat a részletekért.", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "A(z) {0} távoli letöltési kliens a fájlokat a(z) {1} mappában jelentette, de úgy tűnik, hogy ez a könyvtár nem létezik. Valószínűleg hiányzik a távoli útvonal hozzárendelés.", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "A(z) {0} távoli letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik. Valószínűleg hiányzik vagy helytelen a távoli útvonal hozzárendelés.", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "A(z) {0} helyi letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de ez nem érvényes {2} elérési útvonal. Ellenőrizze a letöltési kliens beállításait.", + "RemotePathMappingFileRemovedHealthCheckMessage": "A(z) {path} fájlt részben feldolgozás közben eltávolították.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "A {appName} látja, de nem tud hozzáférni a letöltött epizódhoz {path}. Valószínűleg jogosultsági hiba.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "A(z) {downloadClientName} helyi letöltési kliens a fájlokat a(z) {path} mappában jelentette, de ez nem érvényes {osName} elérési útvonal. Ellenőrizze a letöltési kliens beállításait.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "A(z) {downloadClientName} letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de a {appName} nem látja ezt a könyvtárat. Lehetséges, hogy be kell állítania a mappa jogosultságait.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "A {appName}-nak nem sikerült importálni az epizód(ok)at. Ellenőrizze a naplókat a részletekért.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "A(z) {downloadClientName} távoli letöltési kliens a fájlokat a(z) {path} mappában jelentette, de úgy tűnik, hogy ez a könyvtár nem létezik. Valószínűleg hiányzik a távoli útvonal hozzárendelés.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "A(z) {downloadClientName} távoli letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de úgy tűnik, hogy ez a könyvtár nem létezik. Valószínűleg hiányzik vagy helytelen a távoli útvonal hozzárendelés.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "A(z) {downloadClientName} helyi letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de ez nem érvényes {osName} elérési útvonal. Ellenőrizze a letöltési kliens beállításait.", "RemoveFailedDownloads": "Sikertelen letöltések eltávolítása", - "RemovedSeriesMultipleRemovedHealthCheckMessage": "A(z) {0} sorozatokat eltávolították a TheTVDB-ről", - "RemotePathMappingWrongOSPathHealthCheckMessage": "A(z) {0} távoli letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de ez nem érvényes {2} elérési útvonal. Kérjük, ellenőrizze a távoli útvonal leképezéseket és a letöltési kliens beállításait.", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "A(z) {series} sorozatokat eltávolították a TheTVDB-ről", + "RemotePathMappingWrongOSPathHealthCheckMessage": "A(z) {downloadClientName} távoli letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de ez nem érvényes {osName} elérési útvonal. Kérjük, ellenőrizze a távoli útvonal leképezéseket és a letöltési kliens beállításait.", "RemoveCompletedDownloads": "Befejezett letöltések eltávolítása", - "RemovedSeriesSingleRemovedHealthCheckMessage": "A(z) {0} sorozatot eltávolították a TheTVDB-ről", - "RootFolderMissingHealthCheckMessage": "Hiányzó gyökérmappa: {0}", + "RemovedSeriesSingleRemovedHealthCheckMessage": "A(z) {series} sorozatot eltávolították a TheTVDB-ről", + "RootFolderMissingHealthCheckMessage": "Hiányzó gyökérmappa: {rootFolderPath}", "RootFolder": "Gyökérmappa", "SearchForMonitoredEpisodes": "Megfigyelt epizódok keresése", "ShowAdvanced": "Haladó nézet", - "RootFolderMultipleMissingHealthCheckMessage": "Több gyökérmappa hiányzik: {0}", + "RootFolderMultipleMissingHealthCheckMessage": "Több gyökérmappa hiányzik: {rootFolderPaths}", "SizeOnDisk": "Méret a lemezen", "ShownClickToHide": "Kattints, hogy elrejtsd", "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 '{0}' nem írható a(z) '{1}' felhasználó által.", - "UpdateStartupTranslocationHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{0}' az App Translocation mappában található.", + "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 '{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ó '{1}' nem rendelkezik írási jogosultsággal a(z) '{0}' felhasználói felület mappában.", - "DownloadClientSortingHealthCheckMessage": "A(z) {0} letöltési kliensben engedélyezve van a {1} rendezés a Sonarr 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.", - "RemotePathMappingBadDockerPathHealthCheckMessage": "Docker-t használ; a(z) {0} letöltési kliens a letöltéseket a(z) {1} mappába helyezi, de ez nem érvényes {2} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Docker-t használ; a(z) {0} letöltési kliens a fájlokat a(z) {1} mappában jelentette, de ez nem érvényes {2} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "A(z) {0} letöltési kliens a fájlokat a(z) {1} mappában jelentette, de a Sonarr nem látja ezt a könyvtárat. Lehetséges, hogy be kell állítania a mappa jogosultságait.", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "A(z) {0} távoli letöltési kliens a fájlokat a(z) {1} mappában jelentette, de ez nem érvényes {2} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "A Sonarr látja, de nem fér hozzá a letöltési könyvtárhoz {0}. Valószínűleg jogosultsági hiba." + "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.", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Docker-t használ; a(z) {downloadClientName} letöltési kliens a letöltéseket a(z) {path} mappába helyezi, de ez nem érvényes {osName} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Docker-t használ; a(z) {downloadClientName} letöltési kliens a fájlokat a(z) {path} mappában jelentette, de ez nem érvényes {osName} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "A(z) {downloadClientName} letöltési kliens a fájlokat a(z) {path} mappában jelentette, de a {appName} nem látja ezt a könyvtárat. Lehetséges, hogy be kell állítania a mappa jogosultságait.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "A(z) {downloadClientName} távoli letöltési kliens a fájlokat a(z) {path} mappában jelentette, de ez nem érvényes {osName} elérési útvonal. Ellenőrizze a távoli útvonal hozzárendeléseket, és a letöltési kliens beállításait.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "A {appName} látja, de nem fér hozzá a letöltési könyvtárhoz {downloadPath}. Valószínűleg jogosultsági hiba.", + "All": "Összes", + "Updates": "Frissítések", + "UnknownEventTooltip": "Ismeretlen Esemény", + "AddList": "Lista hozzáadása", + "AddQualityProfile": "Minőségi Profil hozzáadása", + "Automatic": "Automatikus", + "AutomaticSearch": "Automatikus keresés", + "Backups": "Biztonsági mentések", + "Cancel": "Mégse", + "CalendarLoadError": "Naptár betöltése sikertelen", + "Age": "Kor", + "Add": "Hozzáadás", + "Calendar": "Naptár", + "Activity": "Aktivitás", + "Absolute": "Abszolút", + "ApplicationURL": "Alkalmazás URL", + "AutoAdd": "Automatikus hozzáadás", + "CalendarLegendEpisodeDownloadingTooltip": "Epizód letöltés alatt", + "BuiltIn": "Beépített", + "CalendarLegendSeriesFinaleTooltip": "Sorozat vagy évad finálé", + "CancelProcessing": "Folyamat leállítása", + "CalendarOptions": "Naptár beállítások", + "About": "Névjegy", + "Actions": "Teendők", + "Anime": "Anime", + "AuthenticationRequiredUsernameHelpTextWarning": "Adjon meg új felhasználónevet", + "Unavailable": "Nem elérhető", + "Unknown": "Ismeretlen", + "UnmonitoredOnly": "Csak a nem felügyelt", + "UpdateAll": "Összes frissítése", + "AuthenticationRequiredPasswordHelpTextWarning": "Adjon meg új jelszót" } diff --git a/src/NzbDrone.Core/Localization/Core/id.json b/src/NzbDrone.Core/Localization/Core/id.json index 5729ecd63..720303511 100644 --- a/src/NzbDrone.Core/Localization/Core/id.json +++ b/src/NzbDrone.Core/Localization/Core/id.json @@ -1,6 +1,6 @@ { "Added": "Ditambahkan", - "BlocklistReleaseHelpText": "Mencegah Sonarr memperoleh rilis ini secara otomatis", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Mencegah {appName} memperoleh rilis ini secara otomatis", "Delete": "Hapus", "Close": "Tutup", "EnableAutomaticSearch": "Aktifkan Penelusuran Otomatis", @@ -14,8 +14,8 @@ "PreviousAiring": "Sebelumnya Tayang", "OriginalLanguage": "Bahasa Asli", "Priority": "Prioritas", - "ProxyFailedToTestHealthCheckMessage": "Gagal menguji proxy: {0}", - "ProxyBadRequestHealthCheckMessage": "Gagal menguji proxy. Kode Status: {0}", + "ProxyFailedToTestHealthCheckMessage": "Gagal menguji proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Gagal menguji proxy. Kode Status: {statusCode}", "QualityProfile": "Profil Kualitas", "Add": "Tambah", "Cancel": "Batal", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 5d2bc44a8..574b32f5c 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -1,11 +1,11 @@ { - "DeleteConditionMessageText": "Sei sicuro di voler eliminare la condizione '{0}'?", + "DeleteConditionMessageText": "Sei sicuro di voler eliminare la condizione '{name}'?", "ApplyTagsHelpTextReplace": "Sostituire: Sostituisce le etichette con quelle inserite (non inserire nessuna etichette per eliminarle tutte)", "ApplyTagsHelpTextHowToApplyIndexers": "Come applicare etichette agli indicizzatori selezionati", "MoveAutomatically": "Sposta Automaticamente", "ApplyTagsHelpTextRemove": "Rimuovi: Rimuove le etichette inserite", "ApplyTagsHelpTextAdd": "Aggiungi: Aggiunge le etichette alla lista esistente di etichette", - "DeleteCustomFormatMessageText": "Sei sicuro di voler eliminare il formato personalizzato '{0}'?", + "DeleteCustomFormatMessageText": "Sei sicuro di voler eliminare il formato personalizzato '{customFormatName}'?", "DeleteSelectedDownloadClients": "Cancella i Client di Download", "Added": "Aggiunto", "AutomaticAdd": "Aggiungi Automaticamente", @@ -15,7 +15,7 @@ "AutoAdd": "Aggiungi Automaticamente", "AirDate": "Data di Trasmissione", "AllTitles": "Tutti i Titoli", - "ApiKeyValidationHealthCheckMessage": "Aggiorna la tua chiave API in modo che abbia una lunghezza di almeno {0} caratteri. Puoi farlo dalle impostazioni o dal file di configurazione", + "ApiKeyValidationHealthCheckMessage": "Aggiorna la tua chiave API in modo che abbia una lunghezza di almeno {length} caratteri. Puoi farlo dalle impostazioni o dal file di configurazione", "Apply": "Applica", "ApplyChanges": "Applica Cambiamenti", "ApplyTags": "Applica Etichette", @@ -27,5 +27,6 @@ "Actions": "Azioni", "AptUpdater": "Usa apt per installare l'aggiornamento", "Backup": "Backup", - "ApplyTagsHelpTextHowToApplySeries": "Come applicare le etichette alle serie selezionate" + "ApplyTagsHelpTextHowToApplySeries": "Come applicare le etichette alle serie selezionate", + "AddImportList": "Aggiungi lista da importare" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index 0967ef424..a719741c0 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -1 +1,16 @@ -{} +{ + "StopSelecting": "선택 취소", + "Sort": "정렬", + "AddedToDownloadQueue": "다운로드 대기열에 추가됨", + "Calendar": "달력", + "CalendarOptions": "달력 옵션", + "System": "시스템", + "AddToDownloadQueue": "다운로드 대기열에 추가됨", + "NoHistory": "내역 없음", + "SelectAll": "모두 선택", + "View": "표시 변경", + "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다.", + "AddNew": "새로 추가하기", + "History": "내역", + "Sunday": "일요일" +} diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index e8082be62..83382909f 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -1,5 +1,5 @@ { - "ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {0} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen", + "ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {length} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen", "ApplyChanges": "Bekreft endringer", "AllTitles": "Alle titler", "AddAutoTag": "Legg til automatisk tagg", diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index edddcaba9..7e9b7742d 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -1,6 +1,6 @@ { "RemoveFailedDownloads": "Verwijder mislukte downloads", - "ApiKeyValidationHealthCheckMessage": "Maak je API sleutel alsjeblieft minimaal {0} karakters lang. Dit kan gedaan worden via de instellingen of het configuratiebestand", + "ApiKeyValidationHealthCheckMessage": "Maak je API sleutel alsjeblieft minimaal {length} karakters lang. Dit kan gedaan worden via de instellingen of het configuratiebestand", "RemoveCompletedDownloads": "Verwijder voltooide downloads", "AppDataLocationHealthCheckMessage": "Updaten zal niet mogelijk zijn om het verwijderen van AppData te voorkomen", "Added": "Toegevoegd", @@ -21,7 +21,7 @@ "ApplyTagsHelpTextHowToApplyIndexers": "Hoe tags toepassen op de geselecteerde indexeerders", "CountDownloadClientsSelected": "{count} download client(s) geselecteerd", "ApplyTagsHelpTextHowToApplySeries": "Hoe tags toepassen op de geselecteerde series", - "BlocklistReleaseHelpText": "Verbied Sonarr om deze release opnieuw automatisch te downloaden", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Verbied {appName} om deze release opnieuw automatisch te downloaden", "Delete": "Verwijder", "ApplyTagsHelpTextRemove": "Verwijderen: Verwijder de ingevoerde tags", "ApplyTagsHelpTextReplace": "Vervangen: Vervang de tags met de ingevoerde tags (vul geen tags in om alle tags te wissen)", @@ -48,5 +48,31 @@ "AutoTagging": "Automatisch Taggen", "AddAutoTag": "Voeg Automatische Tag toe", "AddCondition": "Voeg Conditie toe", - "CloneAutoTag": "Kopieer Automatische Tag" + "CloneAutoTag": "Kopieer Automatische Tag", + "AbsoluteEpisodeNumbers": "Absolute Afleveringsnummer(s)", + "AbsoluteEpisodeNumber": "Absoluut Afleveringsnummer", + "AddAutoTagError": "Niet in staat een nieuw automatisch label toe te voegen, probeer het opnieuw.", + "AddConditionError": "Niet in staat een nieuwe voorwaarde toe te voegen, probeer het opnieuw.", + "AddConnection": "Voeg connectie toe", + "Absolute": "Absoluut", + "AddANewPath": "Voeg een nieuw pad toe", + "AddConditionImplementation": "Voeg voorwaarde toe - {implementationName}", + "AddConnectionImplementation": "Voeg connectie toe - {implementationName}", + "AddCustomFormat": "Voeg aangepast formaat toe", + "AddDelayProfile": "Voeg vertragingsprofiel toe", + "AddCustomFormatError": "Kan geen nieuw aangepast formaat toevoegen. Probeer het opnieuw.", + "AddDownloadClientError": "Kan geen nieuwe downloadclient toevoegen. Probeer het opnieuw.", + "AddDownloadClient": "Download Client Toevoegen", + "AddIndexerError": "Kon geen nieuwe indexeerder toevoegen, Probeer het opnieuw.", + "AddList": "Lijst Toevoegen", + "AddListError": "Kon geen nieuwe lijst toevoegen, probeer het opnieuw.", + "AddNewRestriction": "Voeg nieuwe restrictie toe", + "AddCustomFilter": "Aangepaste Filter Toevoegen", + "AddDownloadClientImplementation": "Voeg Downloadclient toe - {implementationName}", + "AddIndexer": "Voeg Indexeerder Toe", + "AddIndexerImplementation": "Indexeerder toevoegen - {implementationName}", + "UpdateMechanismHelpText": "Gebruik de ingebouwde updater van {appName} of een script", + "CalendarOptions": "Kalender Opties", + "DeleteQualityProfileMessageText": "Bent u zeker dat u het kwaliteitsprofiel {name} wilt verwijderen?", + "AppUpdated": "{appName} is geüpdatet" } diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 974579375..d45c4d835 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -13,6 +13,10 @@ "ApplyTagsHelpTextHowToApplyIndexers": "Jak zastosować tagi do wybranych indeksatorów", "Authentication": "Autoryzacja", "BlocklistReleases": "Dodaj wersje do czarnej listy", - "ApiKeyValidationHealthCheckMessage": "Zaktualizuj swój klucz API aby był długi na co najmniej {0} znaków. Możesz to zrobić poprzez ustawienia lub plik konfiguracyjny", - "AudioInfo": "Informacje o audio" + "ApiKeyValidationHealthCheckMessage": "Zaktualizuj swój klucz API aby był długi na co najmniej {length} znaków. Możesz to zrobić poprzez ustawienia lub plik konfiguracyjny", + "AudioInfo": "Informacje o audio", + "AddAutoTagError": "Nie można dodać nowego tagu automatycznego, spróbuj ponownie.", + "AddConditionError": "Nie można dodać nowego warunku, spróbuj ponownie.", + "AddConnection": "Dodaj połączenie", + "AddCustomFilter": "Dodaj spersonalizowany filtr" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 32766a87b..833f03964 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -4,8 +4,8 @@ "CountSeasons": "{count} temporadas", "Language": "Idioma", "Added": "Adicionado", - "ApiKeyValidationHealthCheckMessage": "Por favor, atualize a sua API Key para ter no mínimo {0} caracteres. Pode fazer através das definições ou do ficheiro de configuração", - "AppDataLocationHealthCheckMessage": "Não foi possível atualizar para prevenir apagar a AppData durante a atualização", + "ApiKeyValidationHealthCheckMessage": "Por favor, atualize a sua API Key para ter no mínimo {length} caracteres. Pode fazer através das definições ou do ficheiro de configuração", + "AppDataLocationHealthCheckMessage": "Não foi possível actualizar para prevenir apagar a AppData durante a actualização", "AddAutoTag": "Adicionar Etiqueta Automática", "AbsoluteEpisodeNumbers": "Números de Episódios Absolutos", "Add": "Adicionar", @@ -14,5 +14,171 @@ "About": "Sobre", "Actions": "Ações", "Absolute": "Absoluto", - "AddANewPath": "Adicionar novo caminho" + "AddANewPath": "Adicionar um novo caminho", + "AddCondition": "Adicionar Condição", + "AirDate": "Data de Transmissão", + "AllTitles": "Todos os Títulos", + "Apply": "Aplicar", + "AddingTag": "Adicionando etiqueta", + "AgeWhenGrabbed": "Idade (quando capturada)", + "ApplyTags": "Aplicar etiquetas", + "Authentication": "Autenticação", + "AuthenticationMethodHelpText": "Solicitar nome de utilizador e palavra-passe para acessar ao {appName}", + "AuthenticationRequired": "Autenticação Necessária", + "AutoAdd": "Adicionar automaticamente", + "AddRootFolder": "Adicionar pasta raiz", + "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar etiquetas às listas de importação selecionadas", + "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", + "AddListExclusion": "Adicionar exclusão de lista", + "AddListExclusionSeriesHelpText": "Impedir série de ser adicionada ao {appName} através de listas", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar busca por episódios de corte não atendidos", + "AddSeriesWithTitle": "Adicionar {title}", + "AddedDate": "Adicionado: {date}", + "Airs": "Exibições", + "AirsDateAtTimeOn": "{date} às {time} em {networkLabel}", + "AirsTbaOn": "TBA em {networkLabel}", + "AirsTimeOn": "{time} em {networkLabel}", + "AirsTomorrowOn": "Amanhã às {time} em {networkLabel}", + "AllFiles": "Todos os Arquivos", + "AllSeriesAreHiddenByTheAppliedFilter": "Todos os resultados foram ocultados pelo filtro aplicado", + "AlreadyInYourLibrary": "Já está na sua biblioteca", + "AlternateTitles": "Títulos Alternativos", + "Anime": "Anime", + "AnimeEpisodeTypeDescription": "Episódios lançados usando um número de episódio absoluto", + "AnimeEpisodeTypeFormat": "Número absoluto do episódio ({format})", + "Any": "Quaisquer", + "AppUpdated": "{appName} Atualizado", + "AppUpdatedVersion": "{appName} foi atualizado para a versão `{version}`, para obter as alterações mais recentes, você precisará recarregar {appName} ", + "ApplyTagsHelpTextHowToApplySeries": "Como aplicar etiquetas à série selecionada", + "ApplyTagsHelpTextRemove": "Remover: eliminar as etiquetas adicionadas", + "AptUpdater": "Utilize o apt para instalar a atualização", + "AuthenticationMethod": "Método de Autenticação", + "AuthenticationRequiredPasswordHelpTextWarning": "Insira uma nova senha", + "AuthenticationRequiredUsernameHelpTextWarning": "Insira um novo Nome de Usuário", + "AutoTagging": "Etiqueta Automática", + "AutoTaggingLoadError": "Não foi possível carregar a etiqueta automática", + "AutoTaggingNegateHelpText": "Se marcada, a regra de etiqueta automática não será aplicada se esta condição {implementationName} corresponder.", + "AddNew": "Adicionar Novo", + "Age": "Idade", + "AddAutoTagError": "Não foi possível adicionar uma nova etiqueta automática, tente novamente.", + "AddConditionError": "Não foi possível adicionar uma nova condição, tente novamente.", + "AddConnection": "Adicionar Conexão", + "AddCustomFormat": "Adicionar formato personalizado", + "AddDelayProfile": "Adicionar perfil de atraso", + "AddDownloadClientError": "Não foi possível adicionar um novo cliente de downloads, tente novamente.", + "AddExclusion": "Adicionar exclusão", + "AddImportList": "Adicionar Lista de Importação", + "AddIndexer": "Adicionar indexador", + "AddImportListExclusion": "Adicionar exclusão na lista de importação", + "AddNotificationError": "Não foi possível adicionar uma nova notificação, tente novamente.", + "AddQualityProfile": "Adicionar Perfil de Qualidade", + "AddReleaseProfile": "Adicionar Perfil de Lançamento", + "AddRemotePathMappingError": "Não foi possível adicionar um novo mapeamento de caminho remoto, tente novamente.", + "AfterManualRefresh": "Após a atualização manual", + "AllResultsAreHiddenByTheAppliedFilter": "Todos os resultados foram ocultados pelo filtro aplicado", + "Always": "Sempre", + "AnalyseVideoFiles": "Analisar arquivos de vídeo", + "Analytics": "Análise", + "AnimeEpisodeFormat": "Formato do Episódio do Anime", + "ApplicationUrlHelpText": "O URL desta aplicação externa, incluindo http(s)://, porta e URL base", + "AuthBasic": "Básico (pop-up do browser)", + "AppDataDirectory": "Diretório AppData", + "AddCustomFormatError": "Não foi possível adicionar um novo formato personalizado, tente novamente.", + "AddDownloadClient": "Adicionar cliente de transferências", + "AddImportListExclusionError": "Não foi possível adicionar uma nova exclusão na lista de importação, tente novamente.", + "AddNewSeriesRootFolderHelpText": "A subpasta '{folder}' será criada automaticamente", + "AddRemotePathMapping": "Adicionar mapeamento de caminho remoto", + "AnalyseVideoFilesHelpText": "Extraia informações de vídeo como resolução, tempo de execução e informações de codec dos ficheiros. Isso requer que {appName} leia partes do ficheiro, o que pode causar alta atividade do disco ou da rede durante as análises.", + "AnalyticsEnabledHelpText": "Envie informações anónimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas do {appName} WebUI você usa, relatórios de erros, bem como sistema operacional e versão de tempo de execução. Usaremos essas informações para priorizar funcionalidades e correções de bugs.", + "ApiKey": "Chave da API", + "ApplicationURL": "URL do Aplicativo", + "ApplyTagsHelpTextAdd": "Adicionar: agregar as etiquetas à lista existente de etiquetas", + "AddQualityProfileError": "Não foi possível adicionar um novo perfil de qualidade, tente novamente.", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Como aplicar etiquetas aos clientes de download selecionados", + "ApplyTagsHelpTextReplace": "Substituir: mudar as etiquetas pelas adicionadas (deixe em branco para limpar todas as etiquetas)", + "AuthenticationMethodHelpTextWarning": "Selecione um método de autenticação válido", + "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação de endereços locais.", + "AutoRedownloadFailedHelpText": "Procurar automaticamente e tente baixar uma versão diferente", + "AudioInfo": "Informações do áudio", + "AuthForm": "Formulários (Página de Login)", + "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", + "Agenda": "Agenda", + "All": "Todos", + "AnEpisodeIsDownloading": "Um episódio está sendo baixado", + "AudioLanguages": "Idiomas do Áudio", + "AddConditionImplementation": "Adicionar Condição - {implementationName}", + "AddConnectionImplementation": "Adicionar Conexão - {implementationName}", + "AddCustomFilter": "Adicionar Filtro Personalizado", + "AddDownloadClientImplementation": "Adicionar Cliente de Download - {implementationName}", + "AddImportListImplementation": "Adicionar Lista de Importação - {implementationName}", + "AddIndexerError": "Não foi possível adicionar um novo indexador, tente novamente.", + "AddIndexerImplementation": "Adicionar Indexador - {implementationName}", + "AddNewRestriction": "Adicionar nova restrição", + "AddNewSeriesSearchForMissingEpisodes": "Iniciar pesquisa por episódios ausentes", + "AddToDownloadQueue": "Adicionar à fila de download", + "AddList": "Adicionar Lista", + "AddListError": "Não foi possível adicionar uma nova lista, tente novamente.", + "AddListExclusionError": "Não foi possível adicionar uma nova exclusão de lista, tente novamente.", + "AddedToDownloadQueue": "Adicionado à fila de download", + "AllSeriesInRootFolderHaveBeenImported": "Todas as séries em {path} foram importadas", + "AddNewSeries": "Adicionar Nova Série", + "AddNewSeriesError": "Erro ao carregar resultados da busca. Por favor, tente novamente.", + "AddNewSeriesHelpText": "É fácil adicionar uma nova série, apenas comece a escrever o nome da série que quer adicionar.", + "DeleteTagMessageText": "Tem a certeza que quer eliminar a etiqueta \"{label}\"?", + "DeleteSelectedIndexersMessageText": "Tem a certeza de que pretende eliminar {count} indexador(es) selecionado(s)?", + "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)?", + "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.", + "CountIndexersSelected": "{count} indexador(es) selecionado(s)", + "DeleteImportListMessageText": "Tem a certeza de que pretende eliminar a lista '{name}'?", + "DeleteRootFolder": "Eliminar a Pasta Raiz", + "DeleteRootFolderMessageText": "Tem a certeza de que pretende eliminar a pasta de raiz '{path}'?", + "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)?", + "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação de formato personalizado", + "CouldNotFindResults": "Nenhum resultado encontrado para \"{term}\"", + "CountImportListsSelected": "{count} importar lista(s) selecionada(s)", + "DeleteCondition": "Eliminar Condição", + "DeleteQualityProfileMessageText": "Tem a certeza de que pretende eliminar o perfil de qualidade '{name}'?", + "DeletedReasonManual": "O ficheiro foi eliminado através da IU", + "DeletedReasonUpgrade": "O ficheiro foi eliminado para importar uma atualização", + "DownloadIgnored": "Transferência Ignorada", + "DownloadWarning": "Alerta de transferência: {warningMessage}", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação mínima de formato personalizado", + "BlocklistLoadError": "Não foi possível carregar a lista de bloqueio", + "DeleteImportList": "Eliminar Lista de Importação", + "DownloadClientsLoadError": "Não foi possível carregar os clientes de transferências", + "DefaultNameCopiedProfile": "{name} - Copiar", + "CustomFormatJson": "Formato personalizado JSON", + "DeleteAutoTag": "Eliminar Etiqueta Automática", + "DeleteAutoTagHelpText": "Tem a certeza de que pretende eliminar a etiqueta automática \"{name}\"?", + "EditAutoTag": "Editar Etiqueta Automática", + "CloneCondition": "Clonar Condição", + "EditImportListImplementation": "Editar Lista de Importação - {implementationName}", + "EditIndexerImplementation": "Editar Indexador - {implementationName}", + "AutoRedownloadFailedFromInteractiveSearch": "Falha na transferência a partir da Pesquisa interactiva", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Procurar automaticamente e tentar transferir uma versão diferente quando a versão falhada foi obtida a partir da pesquisa interactiva", + "AutomaticUpdatesDisabledDocker": "As actualizações automáticas não são diretamente suportadas quando se utiliza o mecanismo de atualização do Docker. Terá de atualizar a imagem do contentor fora de {appName} ou utilizar um script", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Ativar o desvio quando a versão tem uma pontuação superior à pontuação mínima configurada para o formato personalizado", + "DeleteConditionMessageText": "Tem a certeza de que pretende eliminar a condição '{name}'?", + "EditConditionImplementation": "Editar Condição - {implementationName}", + "EditDownloadClientImplementation": "Editar Cliente de Transferência - {implementationName}", + "AutoRedownloadFailed": "Falha na transferência", + "CountDownloadClientsSelected": "{count} cliente(s) de transferência selecionado(s)", + "Default": "Predefinição", + "DefaultNameCopiedSpecification": "{name} - Copiar", + "DelayingDownloadUntil": "Atraso da transferência até {date} a {time}", + "DeleteIndexerMessageText": "Tem a certeza de que pretende eliminar o indexador '{name}'?", + "DisabledForLocalAddresses": "Desativado para Endereços Locais", + "CalendarOptions": "Opções do calendário", + "UpdateMechanismHelpText": "Use o atualizador do {appName} ou um script" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 1182fb291..670169662 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -8,19 +8,19 @@ "EditSelectedImportLists": "Editar listas de importação selecionadas", "Enabled": "Habilitado", "Ended": "Terminou", - "HideAdvanced": "Ocultar Avançado", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão faltando nas listas de importação: {0}", + "HideAdvanced": "Ocultar opções avançadas", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão faltando nas listas de importação: {rootFoldersInfo}", "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: {0}", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com sincronização de RSS ativada, o Sonarr não obterá novos lançamentos automaticamente", - "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a pesquisa automática ativada, o Sonarr não fornecerá nenhum resultado de pesquisa automática", - "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa interativa habilitada, o Sonarr não fornecerá nenhum resultado de pesquisa interativa", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Nenhum indexador disponível com sincronização de RSS ativada, o {appName} não obterá novos lançamentos automaticamente", + "IndexerSearchNoAutomaticHealthCheckMessage": "Nenhum indexador disponível com a pesquisa automática ativada, o {appName} não fornecerá nenhum resultado de pesquisa automática", + "IndexerSearchNoInteractiveHealthCheckMessage": "Nenhum indexador disponível com a Pesquisa interativa habilitada, o {appName} não fornecerá nenhum resultado de pesquisa interativa", "IndexerStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas", - "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {0}", + "IndexerStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas: {indexerNames}", "Language": "Idioma", "Monitored": "Monitorado", - "MountHealthCheckMessage": "A montagem que contém um caminho de série é montada somente para leitura: ", + "MountSeriesHealthCheckMessage": "A montagem que contém um caminho de série é montada somente para leitura: ", "Network": "Rede", "NoSeasons": "Sem temporadas", "OneSeason": "1 Temporada", @@ -30,64 +30,63 @@ "RemoveFailedDownloads": "Remover downloads com falha", "QualityProfile": "Perfil de Qualidade", "RefreshSeries": "Atualizar Séries", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Você está usando o docker; o cliente de download {0} coloca os downloads em {1}, mas esse diretório parece não existir dentro do contêiner. Revise seus mapeamentos de caminho remoto e configurações de volume do contêiner.", - "RemotePathMappingDownloadPermissionsHealthCheckMessage": "O Sonarr pode ver, mas não acessar o episódio baixado {0}. Provável erro de permissão.", - "RemotePathMappingFileRemovedHealthCheckMessage": "O arquivo {0} foi removido no meio do processamento.", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Baixe os arquivos relatados do cliente {0} em {1}, mas o Sonarr não pode ver este diretório. Pode ser necessário ajustar as permissões da pasta.", - "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "O cliente de download local {0} relatou arquivos em {1}, mas este não é um caminho {2} válido. Revise as configurações do cliente de download.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "Sonarr pode ver, mas não acessar o diretório de download {0}. Provável erro de permissão.", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "O cliente de download {0} coloca os downloads em {1}, mas o Sonarr não pode ver este diretório. Pode ser necessário ajustar as permissões da pasta.", - "RemotePathMappingImportFailedHealthCheckMessage": "Sonarr falhou ao importar (um) episódio(s). Verifique seus logs para obter detalhes.", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "O cliente de download local {0} coloca os downloads em {1}, mas este não é um caminho {2} válido. Revise as configurações do cliente de download.", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "O cliente de download remoto {0} relatou arquivos em {1}, mas este diretório parece não existir. Provavelmente faltando mapeamento de caminho remoto.", - "RemovedSeriesMultipleRemovedHealthCheckMessage": "A série {0} foi removida do TheTVDB", - "RemovedSeriesSingleRemovedHealthCheckMessage": "As séries {0} foram removidas do TheTVDB", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Você está usando o docker; o cliente de download {downloadClientName} coloca os downloads em {path}, mas este diretório parece não existir dentro do contêiner. Revise seus mapeamentos de caminho remoto e configurações de volume de contêiner.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "O {appName} pode ver, mas não acessar o episódio baixado {path}. Provável erro de permissão.", + "RemotePathMappingFileRemovedHealthCheckMessage": "O arquivo {path} foi removido no meio do processamento.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Baixe os arquivos relatados do cliente {downloadClientName} em {path}, mas o {appName} não pode ver este diretório. Pode ser necessário ajustar as permissões da pasta.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "O cliente de download local {downloadClientName} relatou arquivos em {path}, mas este não é um caminho {osName} válido. Revise as configurações do cliente de download.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} pode ver, mas não acessar o diretório de download {downloadPath}. Provável erro de permissão.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads em {path}, mas o {appName} não pode ver este diretório. Pode ser necessário ajustar as permissões da pasta.", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} falhou ao importar (um) episódio(s). Verifique seus logs para obter detalhes.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "O cliente de download local {downloadClientName} coloca os downloads em {path}, mas este não é um caminho {osName} válido. Revise as configurações do cliente de download.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "O cliente de download remoto {downloadClientName} relatou arquivos em {path}, mas este diretório parece não existir. Provavelmente faltando mapeamento de caminho remoto.", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "A série {series} foi removida do TheTVDB", + "RemovedSeriesSingleRemovedHealthCheckMessage": "As séries {series} foram removidas do TheTVDB", "RootFolder": "Pasta Raiz", - "RootFolderMissingHealthCheckMessage": "Pasta raiz ausente: {0}", - "RootFolderMultipleMissingHealthCheckMessage": "Faltam várias pastas raiz: {0}", + "RootFolderMissingHealthCheckMessage": "Pasta raiz ausente: {rootFolderPath}", + "RootFolderMultipleMissingHealthCheckMessage": "Faltam várias pastas raiz: {rootFolderPaths}", "SearchForMonitoredEpisodes": "Pesquisar episódios monitorados", - "ShowAdvanced": "Mostrar Avançado", + "ShowAdvanced": "Mostrar opções avançadas", "ShownClickToHide": "Mostrado, clique para ocultar", "SizeOnDisk": "Tamanho no disco", "SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido", "Unmonitored": "Não monitorado", "UpdateAvailableHealthCheckMessage": "Nova atualização está disponível", - "UpdateUINotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de IU '{0}' não pode ser gravada pelo usuário '{1}'.", "Added": "Adicionado", - "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {0} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", + "ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração", "RemoveCompletedDownloads": "Remover downloads concluídos", "AppDataLocationHealthCheckMessage": "A atualização não será possível para evitar a exclusão de AppData na atualização", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {0}.", - "DownloadClientRootFolderHealthCheckMessage": "O cliente de download {0} coloca os downloads na pasta raiz {1}. Você não deve baixar para uma pasta raiz.", - "DownloadClientSortingHealthCheckMessage": "O cliente de download {0} tem classificação {1} habilitada para a categoria Sonarr. Você deve desativar a classificação em seu cliente de download para evitar problemas de importação.", - "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de download indisponíveis devido a falhas: {0}", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {downloadClientName}. {errorMessage}", + "DownloadClientRootFolderHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads na pasta raiz {rootFolderPath}. Você não deve baixar para uma pasta raiz.", + "DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria {appName}. Você deve desativar a classificação em seu cliente de download para evitar problemas de importação.", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de download indisponíveis devido a falhas: {downloadClientNames}", "EditSelectedIndexers": "Editar indexadores selecionados", "EditSeries": "Editar Série", "EnableAutomaticSearch": "Ativar pesquisa automática", "EnableInteractiveSearch": "Ativar pesquisa interativa", "HiddenClickToShow": "Oculto, clique para mostrar", - "ImportListRootFolderMissingRootHealthCheckMessage": "Pasta raiz ausente para lista(s) de importação: {0}", - "ImportListStatusUnavailableHealthCheckMessage": "Listas indisponíveis devido a falhas: {0}", + "ImportListRootFolderMissingRootHealthCheckMessage": "Pasta raiz ausente para lista(s) de importação: {rootFolderInfo}", + "ImportListStatusUnavailableHealthCheckMessage": "Listas indisponíveis devido a falhas: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Ative o Gerenciamento de download concluído, se possível (Multi-computador não suportado)", - "IndexerJackettAllHealthCheckMessage": "Indexadores usando o endpont Jackett 'all' sem suporte: {0}", + "IndexerJackettAllHealthCheckMessage": "Indexadores usando o endpont Jackett 'all' sem suporte: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Todos os indexadores estão indisponíveis devido a falhas por mais de 6 horas", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos os indexadores compatíveis com rss estão temporariamente indisponíveis devido a erros recentes do indexador", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos os indexadores com capacidade de pesquisa estão temporariamente indisponíveis devido a erros recentes do indexador", "NextAiring": "Próxima Exibição", "OriginalLanguage": "Idioma Original", - "ProxyBadRequestHealthCheckMessage": "Falha ao testar o proxy. Código de estado: {0}", - "ProxyFailedToTestHealthCheckMessage": "Falha ao testar o proxy: {0}", - "ProxyResolveIpHealthCheckMessage": "Falha ao resolver o endereço IP do host de proxy configurado {0}", - "RecycleBinUnableToWriteHealthCheckMessage": "Não é possível gravar na pasta da lixeira configurada: {0}. Certifique-se de que este caminho exista e seja gravável pelo usuário executando o Sonarr", - "RemotePathMappingBadDockerPathHealthCheckMessage": "Você está usando o docker; cliente de download {0} coloca downloads em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Você está usando o docker; baixe os arquivos relatados do cliente {0} em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "O cliente de download remoto {0} relatou arquivos em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "O cliente de download remoto {0} coloca os downloads em {1}, mas este diretório parece não existir. Mapeamento de caminho remoto provavelmente ausente ou incorreto.", - "RemotePathMappingWrongOSPathHealthCheckMessage": "O cliente de download remoto {0} coloca os downloads em {1}, mas este não é um caminho {2} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", - "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' não pode ser gravada pelo usuário '{1}'.", - "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{0}' está em uma pasta de translocação de aplicativo.", - "BlocklistReleaseHelpText": "Inicia uma busca por este episódio novamente e impede que esta versão seja capturada novamente", + "ProxyBadRequestHealthCheckMessage": "Falha ao testar o proxy. Código de estado: {statusCode}", + "ProxyFailedToTestHealthCheckMessage": "Falha ao testar o proxy: {url}", + "ProxyResolveIpHealthCheckMessage": "Falha ao resolver o endereço IP do host de proxy configurado {proxyHostName}", + "RecycleBinUnableToWriteHealthCheckMessage": "Não é possível gravar na pasta da lixeira configurada: {path}. Certifique-se de que este caminho exista e seja gravável pelo usuário executando o {appName}", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Você está usando o docker; cliente de download {downloadClientName} coloca downloads em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Você está usando o docker; baixe os arquivos relatados do cliente {downloadClientName} em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "O cliente de download remoto {downloadClientName} relatou arquivos em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "O cliente de download remoto {downloadClientName} coloca os downloads em {path}, mas este diretório parece não existir. Mapeamento de caminho remoto provavelmente ausente ou incorreto.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "O cliente de download remoto {downloadClientName} coloca os downloads em {path}, mas este não é um caminho {osName} válido. Revise seus mapeamentos de caminho remoto e baixe as configurações do cliente.", + "UpdateStartupNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' não pode ser gravada pelo usuário '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de inicialização '{startupFolder}' está em uma pasta de translocação do App.", + "BlocklistReleaseSearchEpisodeAgainHelpText": "Inicia uma busca por este episódio novamente e impede que esta versão seja capturada novamente", "BlocklistReleases": "Lançamentos na lista de bloqueio", "CloneCondition": "Clonar Condição", "CloneCustomFormat": "Clonar formato personalizado", @@ -96,8 +95,8 @@ "DeleteCondition": "Excluir condição", "DeleteConditionMessageText": "Tem certeza de que deseja excluir a condição '{name}'?", "DeleteCustomFormat": "Excluir formato personalizado", - "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado '{0}'?", - "ExportCustomFormat": "Exportar Formato Personalizado", + "DeleteCustomFormatMessageText": "Tem certeza de que deseja excluir o formato personalizado '{customFormatName}'?", + "ExportCustomFormat": "Exportar formato personalizado", "Negated": "Negado", "Remove": "Remover", "RemoveFromDownloadClient": "Remover Do Cliente de Download", @@ -121,7 +120,7 @@ "DeleteSelectedIndexers": "Excluir indexador(es)", "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir {count} cliente(s) de download selecionado(s)?", "DeleteSelectedImportListsMessageText": "Tem certeza de que deseja excluir {count} lista(s) de importação selecionada(s)?", - "ExistingTag": "Etiqueta existente", + "ExistingTag": "Tag existente", "Implementation": "Implementação", "Disabled": "Desabilitado", "Edit": "Editar", @@ -139,11 +138,11 @@ "RemoveFailed": "Falha na remoção", "Replace": "Substituir", "Result": "Resultado", - "SetTags": "Definir etiquetas", + "SetTags": "Definir tags", "Yes": "Sim", "Tags": "Tags", "AutoAdd": "Adicionar automaticamente", - "RemovingTag": "Removendo etiqueta", + "RemovingTag": "Removendo a tag", "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir {count} indexadores selecionados?", "RemoveCompleted": "Remoção Concluída", "LibraryImport": "Importar para biblioteca", @@ -161,9 +160,9 @@ "Tasks": "Tarefas", "Updates": "Atualizações", "Wanted": "Procurado", - "ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas", - "ApplyTagsHelpTextReplace": "Substituir: Substitua as etiquetas pelas etiquetas inseridas (não digite nenhuma etiqueta para limpar todas as etiquetas)", - "ApplyTagsHelpTextRemove": "Remover: Remove as etiquetas inseridas", + "ApplyTagsHelpTextAdd": "Adicionar: Adicione as tags à lista existente de tags", + "ApplyTagsHelpTextReplace": "Substituir: Substitua as tags pelas tags inseridas (não digite nenhuma tag para limpar todas as tags)", + "ApplyTagsHelpTextRemove": "Remover: Remove as tags inseridas", "CustomFormatScore": "Pontuação do formato personalizado", "Activity": "Atividade", "AddNew": "Adicionar Novo", @@ -180,15 +179,15 @@ "ImportLists": "Listas de importação", "Indexers": "Indexadores", "AbsoluteEpisodeNumbers": "Número(s) absoluto(s) do episódio", - "AirDate": "Data de Exibição", + "AirDate": "Data de exibição", "Daily": "Diário", "Details": "Detalhes", "AllTitles": "Todos os Títulos", "Version": "Versão", "ApplyTagsHelpTextHowToApplyDownloadClients": "Como aplicar tags aos clientes de download selecionados", "ApplyTagsHelpTextHowToApplyImportLists": "Como aplicar tags às listas de importação selecionadas", - "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados", - "ApplyTagsHelpTextHowToApplySeries": "Como aplicar etiquetas à série selecionada", + "ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar tags aos indexadores selecionados", + "ApplyTagsHelpTextHowToApplySeries": "Como aplicar tags à série selecionada", "EpisodeInfo": "Info do Episódio", "EpisodeNumbers": "Número(s) do(s) Episódio(s)", "FullSeason": "Temporada Completa", @@ -196,7 +195,7 @@ "MatchedToEpisodes": "Correspondente aos Episódios", "MatchedToSeason": "Correspondente a Temporada", "MatchedToSeries": "Correspondente à Série", - "MultiSeason": "Multi-Temporada", + "MultiSeason": "Várias temporadas", "PartialSeason": "Temporada Parcial", "Proper": "Proper", "Real": "Real", @@ -231,7 +230,7 @@ "Duration": "Duração", "ErrorRestoringBackup": "Erro ao restaurar o backup", "Exception": "Exceção", - "ExternalUpdater": "O Sonarr está configurado para usar um mecanismo de atualização externo", + "ExternalUpdater": "O {appName} está configurado para usar um mecanismo de atualização externo", "FailedToFetchUpdates": "Falha ao buscar atualizações", "FailedToUpdateSettings": "Falha ao atualizar as configurações", "FeatureRequests": "Solicitações de recursos", @@ -243,7 +242,7 @@ "GeneralSettings": "Configurações Gerais", "Health": "Saúde", "HomePage": "Página Inicial", - "OnLatestVersion": "A versão mais recente do Sonarr já está instalada", + "OnLatestVersion": "A versão mais recente do {appName} já está instalada", "InstallLatest": "Instalar o mais recente", "Interval": "Intervalo", "IRC": "IRC", @@ -279,7 +278,7 @@ "TaskUserAgentTooltip": "User-Agent fornecido pelo aplicativo que chamou a API", "ResetQualityDefinitions": "Redefinir Definições de Qualidade", "ResetQualityDefinitionsMessageText": "Tem certeza de que deseja redefinir as definições de qualidade?", - "ResetTitles": "Redefinir Títulos", + "ResetTitles": "Redefinir títulos", "Restore": "Restaurar", "RestoreBackup": "Restaurar backup", "Scheduled": "Agendado", @@ -290,22 +289,22 @@ "StartupDirectory": "Diretório de inicialização", "Status": "Estado", "TestAll": "Testar Tudo", - "TheLogLevelDefault": "O padrão do nível de log é 'Info' e pode ser alterado em [Configurações gerais](/configurações /geral)", + "TheLogLevelDefault": "O nível de registro é padronizado como 'Info' e pode ser alterado em [Configurações Gerais](/settings/general)", "Time": "Horário", "TotalSpace": "Espaço Total", "Twitter": "Twitter", "UnableToLoadBackups": "Não foi possível carregar os backups", - "UnableToUpdateSonarrDirectly": "Incapaz de atualizar o Sonarr diretamente,", + "UnableToUpdateSonarrDirectly": "Incapaz de atualizar o {appName} diretamente,", "UpdaterLogFiles": "Arquivos de log do atualizador", "Uptime": "Tempo de atividade", "Wiki": "Wiki", "WouldYouLikeToRestoreBackup": "Gostaria de restaurar o backup '{name}'?", "YesCancel": "Sim, Cancelar", "Reset": "Redefinir", - "ResetDefinitionTitlesHelpText": "Redefinir títulos de definição, bem como valores", - "RestartReloadNote": "Observação: o Sonarr reiniciará automaticamente e recarregará a IU durante o processo de restauração.", + "ResetDefinitionTitlesHelpText": "Redefinir títulos de definição e valores", + "RestartReloadNote": "Observação: o {appName} reiniciará automaticamente e recarregará a IU durante o processo de restauração.", "SkipRedownload": "Ignorar o Redownload", - "SkipRedownloadHelpText": "Impede que o Sonarr tente baixar uma versão alternativa para este item", + "SkipRedownloadHelpText": "Impede que o {appName} tente baixar uma versão alternativa para este item", "IRCLinkText": "#sonarr na Libera", "LiberaWebchat": "Libera Webchat", "All": "Todos", @@ -330,11 +329,11 @@ "Grabbed": "Obtido", "Ignored": "Ignorado", "Imported": "Importado", - "IncludeUnmonitored": "Incluir não monitorado", + "IncludeUnmonitored": "Incluir não monitorados", "Indexer": "Indexador", "LatestSeason": "Última temporada", "MissingEpisodes": "Episódios ausentes", - "MonitoredOnly": "Somente monitorado", + "MonitoredOnly": "Somente monitorados", "OutputPath": "Caminho de saída", "Progress": "Progresso", "Rating": "Avaliação", @@ -357,11 +356,11 @@ "VideoDynamicRange": "Faixa Dinâmica de Vídeo", "Warn": "Alerta", "Year": "Ano", - "Age": "Idade", + "Age": "Tempo de vida", "Episodes": "Episódios", "Failed": "Falhou", "HasMissingSeason": "Está faltando temporada", - "Info": "Info", + "Info": "Informações", "NotSeasonPack": "Não Pacote de Temporada", "Peers": "Pares", "Protocol": "Protocolo", @@ -378,30 +377,30 @@ "Negate": "Negar", "Save": "Salvar", "AddRootFolder": "Adicionar Pasta Raiz", - "AutoTagging": "Marcação Automática", + "AutoTagging": "Tagging Automática", "DeleteRootFolder": "Excluir Pasta Raiz", "DeleteRootFolderMessageText": "Tem certeza de que deseja excluir a pasta raiz '{path}'?", - "RemoveTagsAutomaticallyHelpText": "Remova tags automaticamente se as condições não forem atendidas", + "RemoveTagsAutomaticallyHelpText": "Remover tags automaticamente se as condições não forem encontradas", "RootFolders": "Pastas Raiz", "AllResultsAreHiddenByTheAppliedFilter": "Todos os resultados são ocultados pelo filtro aplicado", "Folder": "Pasta", - "InteractiveImport": "Importação Interativa", + "InteractiveImport": "Importação interativa", "LastUsed": "Usado por último", - "MoveAutomatically": "Mover Automaticamente", + "MoveAutomatically": "Mover automaticamente", "NoResultsFound": "Nenhum resultado encontrado", "RemoveRootFolder": "Remover pasta raiz", - "RemoveTagsAutomatically": "Remover tags automaticamente", + "RemoveTagsAutomatically": "Remover Tags Automaticamente", "Season": "Temporada", "SelectFolder": "Selecionar Pasta", "Unavailable": "Indisponível", "UnmappedFolders": "Pastas não mapeadas", - "AutoTaggingNegateHelpText": "se marcada, a regra de marcação automática não será aplicada se esta condição {implementationName} corresponder.", - "AutoTaggingRequiredHelpText": "Esta condição {implementationName} deve corresponder para que a regra de etiqueta automática seja aplicada. Caso contrário, uma única correspondência {implementationName} é suficiente.", + "AutoTaggingNegateHelpText": "Se marcada, a regra de tagging automática não será aplicada se esta condição {implementationName} corresponder.", + "AutoTaggingRequiredHelpText": "Esta condição {implementationName} deve corresponder para que a regra de tagging automática seja aplicada. Caso contrário, uma única correspondência de {implementationName} será suficiente.", "SomeResultsAreHiddenByTheAppliedFilter": "Alguns resultados estão ocultos pelo filtro aplicado", "UnableToLoadAutoTagging": "Não foi possível carregar a marcação automática", "UnableToLoadRootFolders": "Não foi possível carregar as pastas raiz", - "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {0}.", - "AddConditionError": "Não foi possível adicionar uma nova condição. Tente novamente.", + "IndexerDownloadClientHealthCheckMessage": "Indexadores com clientes de download inválidos: {indexerNames}.", + "AddConditionError": "Não foi possível adicionar uma nova condição, por favor, tente novamente.", "AddConnection": "Adicionar Conexão", "AddCustomFormat": "Adicionar Formato Personalizado", "AddCustomFormatError": "Não foi possível adicionar um novo formato personalizado. Tente novamente.", @@ -419,7 +418,7 @@ "AddListExclusionError": "Não foi possível adicionar uma nova exclusão de lista, tente novamente.", "AddNewRestriction": "Adicionar nova restrição", "AddNotificationError": "Não foi possível adicionar uma nova notificação, tente novamente.", - "AddQualityProfile": "Adicionar Perfil de Qualidade", + "AddQualityProfile": "Adicionar perfil de qualidade", "AddQualityProfileError": "Não foi possível adicionar uma nova notificação, tente novamente.", "AddReleaseProfile": "Adicionar Perfil de Lançamento", "AddRemotePathMapping": "Adicionar Mapeamento de Caminho Remoto", @@ -428,21 +427,21 @@ "Always": "Sempre", "AnalyseVideoFiles": "Analisar arquivos de vídeo", "Analytics": "Analítica", - "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do Sonarr. Isso inclui informações sobre seu navegador, quais páginas do Sonarr WebUI você usa, relatórios de erros, bem como sistema operacional e versão de tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", + "AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do {appName}. Isso inclui informações sobre seu navegador, quais páginas do {appName} WebUI você usa, relatórios de erros, bem como sistema operacional e versão de tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.", "AnimeEpisodeFormat": "Formato do Episódio de Anime", "ApiKey": "Chave API", "ApplicationURL": "URL do Aplicativo", "ApplicationUrlHelpText": "A URL externa deste aplicativo, incluindo http(s)://, porta e base da URL", - "AuthBasic": "Básico (Balão do Navegador)", - "AuthForm": "Formulário (Página de login)", + "AuthBasic": "Básico (pop-up do navegador)", + "AuthForm": "Formulário (página de login)", "Authentication": "Autenticação", - "AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar {appName}", - "AuthenticationRequired": "Autenticação Requerida", + "AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar o {appName}", + "AuthenticationRequired": "Autenticação exigida", "AutoRedownloadFailedHelpText": "Procurar automaticamente e tente baixar uma versão diferente", - "AutoTaggingLoadError": "Não foi possível carregar a marcação automática", + "AutoTaggingLoadError": "Não foi possível carregar tagging automática", "Automatic": "Automático", "AutomaticSearch": "Pesquisa Automática", - "BackupFolderHelpText": "Os caminhos relativos estarão no diretório AppData do Sonarr", + "BackupFolderHelpText": "Os caminhos relativos estarão no diretório AppData do {appName}", "BackupIntervalHelpText": "Intervalo entre backups automáticos", "BackupRetentionHelpText": "Backups automáticos anteriores ao período de retenção serão limpos automaticamente", "BackupsLoadError": "Não foi possível carregar os backups", @@ -450,13 +449,13 @@ "BindAddressHelpText": "Endereço IP válido, localhost ou '*' para todas as interfaces", "BlocklistLoadError": "Não foi possível carregar a lista de bloqueio", "Branch": "Ramificação", - "BranchUpdate": "Ramificação a ser usada para atualizar o Sonarr", + "BranchUpdate": "Ramificação a ser usada para atualizar o {appName}", "BranchUpdateMechanism": "Ramificação usada pelo mecanismo externo de atualização", "BrowserReloadRequired": "Necessário Recarregar o Navegador", "BuiltIn": "Embutido", "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação do formato personalizado", "BypassDelayIfAboveCustomFormatScoreHelpText": "Ativar a opção de ignorar quando a versão tiver uma pontuação maior que a pontuação mínima configurada do formato personalizado", - "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação Mínima de Formato Personalizado", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Pontuação mínima de formato personalizado", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Pontuação mínima de formato personalizado necessária para ignorar o atraso do protocolo preferido", "BypassDelayIfHighestQuality": "Ignorar se a qualidade é mais alta", "BypassDelayIfHighestQualityHelpText": "Ignorar o atraso quando o lançamento tiver a qualidade mais alta habilitada no perfil de qualidade com o protocolo preferencial", @@ -468,30 +467,30 @@ "ChangeFileDateHelpText": "Alterar a data do arquivo na importação/rescan", "ChmodFolder": "chmod Pasta", "ChmodFolderHelpText": "Octal, aplicado durante a importação/renomeação de pastas e arquivos de mídia (sem bits de execução)", - "ChmodFolderHelpTextWarning": "Isso só funciona se o usuário que estiver executando o sonarr for o proprietário do arquivo. É melhor garantir que o cliente de download defina as permissões corretamente.", + "ChmodFolderHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download defina as permissões corretamente.", "ChownGroup": "chown Grupo", "ChownGroupHelpText": "Nome do grupo ou gid. Use gid para sistemas de arquivos remotos.", - "ChownGroupHelpTextWarning": "Isso só funciona se o usuário que estiver executando o sonarr for o proprietário do arquivo. É melhor garantir que o cliente de download use o mesmo grupo do sonarr.", + "ChownGroupHelpTextWarning": "Isso só funciona se o usuário que executa {appName} for o proprietário do arquivo. É melhor garantir que o cliente de download use o mesmo grupo que {appName}.", "ClientPriority": "Prioridade do Cliente", "Clone": "Clonar", "CloneIndexer": "Clonar Indexador", "CloneProfile": "Clonar Perfil", "CollectionsLoadError": "Não foi possível carregar as coleções", "ColonReplacement": "Substituto para dois-pontos", - "ColonReplacementFormatHelpText": "Mude como o Sonarr lida com a substituição do dois-pontos", + "ColonReplacementFormatHelpText": "Mude como o {appName} lida com a substituição do dois-pontos", "CompletedDownloadHandling": "Gerenciamento de Downloads Completos", "Condition": "Condição", - "ConditionUsingRegularExpressions": "Essa condição corresponde ao uso de expressões regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam ser substituídos por um `\\`", + "ConditionUsingRegularExpressions": "Esta condição corresponde ao uso de expressões regulares. Observe que os caracteres `\\^$.|?*+()[{` têm significados especiais e precisam ser escapados com um `\\`", "ConnectSettings": "Configurações de Conexão", "ConnectSettingsSummary": "Notificações, conexões com servidores/reprodutores de mídia e scripts personalizados", "Connections": "Conexões", "CopyToClipboard": "Copiar para Área de Transferência", - "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo propagados. Você pode desativar temporariamente a propagação e usar a função de renomeação do Sonarr como solução alternativa.", + "CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, os bloqueios de arquivo podem impedir a renomeação de arquivos que estão sendo propagados. Você pode desativar temporariamente a propagação e usar a função de renomeação do {appName} como solução alternativa.", "CreateEmptySeriesFolders": "Criar Pastas de Séries Vazias", "CreateEmptySeriesFoldersHelpText": "Crie pastas de série ausentes durante a verificação de disco", "CreateGroup": "Criar Grupo", "Custom": "Personalizar", - "CustomFormat": "Formato Personalizado", + "CustomFormat": "Formato personalizado", "CustomFormatUnknownCondition": "Condição de formato personalizado desconhecido '{implementation}'", "CustomFormatUnknownConditionOption": "Opção desconhecida '{key}' para a condição '{implementation}'", "CustomFormatsLoadError": "Não foi possível carregar Formatos Personalizados", @@ -502,18 +501,18 @@ "Dash": "Traço", "Dates": "Datas", "Debug": "Depuração", - "DefaultDelayProfile": "Este é o perfil padrão. Aplica-se a todas as séries que não possuem um perfil explícito.", + "DefaultDelayProfileSeries": "Este é o perfil padrão. Aplica-se a todas as séries que não possuem um perfil explícito.", "DelayMinutes": "{delay} Minutos", "DelayProfile": "Perfil de Atraso", "DefaultCase": "Minúscula ou maiúscula", - "DelayProfileTagsHelpText": "Aplica-se a séries com pelo menos uma tag correspondente", + "DelayProfileSeriesTagsHelpText": "Aplica-se a séries com pelo menos uma tag correspondente", "DelayProfiles": "Perfis de Atraso", "DelayProfilesLoadError": "Não foi possível carregar perfis de atraso", "DeleteDelayProfile": "Excluir Perfil de Atraso", "DeleteDownloadClient": "Excluir Cliente de Download", "DeleteDownloadClientMessageText": "Tem certeza de que deseja excluir o cliente de download '{name}'?", "DeleteEmptyFolders": "Excluir Pastas Vazias", - "DeleteEmptyFoldersHelpText": "Excluir pastas vazias de séries e temporadas durante a verificação de disco e quando os arquivos de episódios são excluídos", + "DeleteEmptySeriesFoldersHelpText": "Excluir pastas vazias de séries e temporadas durante a verificação de disco e quando os arquivos de episódios são excluídos", "DeleteImportList": "Excluir Lista de Importação", "DeleteImportListExclusion": "Excluir Exclusão da Lista de Importação", "DeleteImportListExclusionMessageText": "Tem certeza de que deseja excluir esta exclusão da lista de importação?", @@ -530,7 +529,7 @@ "DeleteSpecificationHelpText": "Tem certeza de que deseja excluir a especificação '{name}'?", "DeleteTag": "Excluir Tag", "DeleteTagMessageText": "Tem certeza de que deseja excluir a tag '{label}'?", - "DisabledForLocalAddresses": "Desabilitar para Endereços Locais", + "DisabledForLocalAddresses": "Desabilitado para endereços locais", "DoNotPrefer": "Não Preferir", "DoNotUpgradeAutomatically": "Não Atualizar Automaticamente", "DoneEditingGroups": "Concluir Edição de Grupos", @@ -541,7 +540,7 @@ "DownloadPropersAndRepacks": "Propers e Repacks", "DownloadPropersAndRepacksHelpText": "Se deve ou não atualizar automaticamente para Propers/Repacks", "DownloadPropersAndRepacksHelpTextCustomFormat": "Use 'Não Preferir' para classificar por pontuação de formato personalizado em Propers/Repacks", - "DownloadPropersAndRepacksHelpTextWarning": "Use formatos personalizados para atualizações automáticas para Propers/Repacks", + "DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para atualizações automáticas para propers/repacks", "Duplicate": "Duplicado", "EditCustomFormat": "Editar Formato Personalizado", "EditDelayProfile": "Editar Perfil de Atraso", @@ -555,20 +554,20 @@ "EditRestriction": "Editar Restrição", "Enable": "Habilitar", "EnableAutomaticAdd": "Habilitar Adição Automática", - "EnableAutomaticAddHelpText": "Adicione séries desta lista ao Sonarr quando as sincronizações forem realizadas por meio da interface do usuário ou pelo Sonarr", - "EnableAutomaticSearchHelpText": "Será usado quando pesquisas automáticas forem realizadas por meio da interface do usuário ou pelo Sonarr", + "EnableAutomaticAddSeriesHelpText": "Adicione séries desta lista ao {appName} quando as sincronizações forem realizadas por meio da interface do usuário ou pelo {appName}", + "EnableAutomaticSearchHelpText": "Será usado quando pesquisas automáticas forem realizadas por meio da interface do usuário ou pelo {appName}", "EnableAutomaticSearchHelpTextWarning": "Será usado quando a pesquisa interativa for usada", "EnableCompletedDownloadHandlingHelpText": "Importar automaticamente downloads concluídos do cliente de download", "EnableHelpText": "Habilitar criação de arquivo de metadados para este tipo de metadados", "EnableInteractiveSearchHelpText": "Será usado quando a pesquisa interativa for usada", "EnableColorImpairedMode": "Habilitar Modo para Deficientes Visuais", "EnableInteractiveSearchHelpTextWarning": "A pesquisa não é compatível com este indexador", - "EnableMediaInfoHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o Sonarr leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", + "EnableMediaInfoHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", "EnableMetadataHelpText": "Habilitar criação de arquivo de metadados para este tipo de metadados", "EnableProfile": "Habilitar Perfil", "EnableProfileHelpText": "Marque para habilitar o perfil de lançamento", "EnableRss": "Habilitar RSS", - "EnableRssHelpText": "Será usado quando o Sonarr procurar periodicamente lançamentos via RSS Sync", + "EnableRssHelpText": "Será usado quando o {appName} procurar periodicamente lançamentos via RSS Sync", "EnableSsl": "Habilitar SSL", "EnableSslHelpText": "Requer reinicialização em execução como administrador para entrar em vigor", "EpisodeNaming": "Nomenclatura do Episódio", @@ -595,92 +594,92 @@ "Host": "Host", "Hostname": "Hostname", "ImportExtraFiles": "Importar Arquivos Extras", - "ImportExtraFilesHelpText": "Importar arquivos extras correspondentes (legendas, nfo, etc) após importar um arquivo de episódio", + "ImportExtraFilesEpisodeHelpText": "Importar arquivos extras correspondentes (legendas, nfo, etc) após importar um arquivo de episódio", "ImportList": "Importar Lista", "ImportListExclusions": "Importar Lista de Exclusões", "ImportListExclusionsLoadError": "Não foi possível carregar as exclusões da lista de importação", - "ImportListSettings": "Configurações da Importação das Listas", - "ImportListsLoadError": "Não foi possível carregar listas de importação", - "ImportListsSettingsSummary": "Importar de outra instância Sonarr ou listas Trakt e gerenciar exclusões de lista", - "ImportScriptPath": "Importar Caminho do Script", + "ImportListSettings": "Configurações de Importar listas", + "ImportListsLoadError": "Não foi possível carregar Importar listas", + "ImportListsSettingsSummary": "Importar de outra instância do {appName} ou de listas do Trakt, e gerenciar as exclusões de lista", + "ImportScriptPath": "Caminho para importar script", "ImportScriptPathHelpText": "O caminho para o script a ser usado para importar", - "ImportUsingScript": "Importar Usando o Script", - "ImportUsingScriptHelpText": "Copiar arquivos para importar usando um script (ex. para transcodificação)", + "ImportUsingScript": "Importar usando script", + "ImportUsingScriptHelpText": "Copiar arquivos para importar usando um script (p. ex. para transcodificação)", "Importing": "Importando", - "IncludeCustomFormatWhenRenaming": "Incluir Formato Personalizado ao Renomear", + "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear", "IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}", - "IncludeHealthWarnings": "Incluir Avisos de Saúde", + "IncludeHealthWarnings": "Incluir avisos de integridade", "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 ao obter lançamentos como desempate para lançamentos iguais, o Sonarr ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS", - "IndexerSettings": "Configuraçõ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", + "IndexerSettings": "Configurações do indexador", "IndexersLoadError": "Não foi possível carregar os indexadores", "IndexersSettingsSummary": "Indexadores e opções de indexador", - "InstanceName": "Nome da Instância", + "InstanceName": "Nome da instância", "InstanceNameHelpText": "Nome da instância na aba e para o nome do aplicativo Syslog", - "InteractiveSearch": "Pesquisa Interativa", - "InvalidFormat": "Formato Inválido", + "InteractiveSearch": "Pesquisa interativa", + "InvalidFormat": "Formato inválido", "LanguagesLoadError": "Não foi possível carregar os idiomas", - "ListExclusionsLoadError": "Não foi possível carregar as exclusões da lista", - "ListOptionsLoadError": "Não foi possível carregar as opções da lista", + "ListExclusionsLoadError": "Não foi possível carregar as exclusões de lista", + "ListOptionsLoadError": "Não foi possível carregar as opções de lista", "ListQualityProfileHelpText": "Os itens da lista de perfil de qualidade serão adicionados com", "ListRootFolderHelpText": "Os itens da lista da pasta raiz serão adicionados a", - "ListTagsHelpText": "Tags que serão adicionadas na importação desta lista", + "ListTagsHelpText": "Tags que serão adicionadas ao importar esta lista", "ListWillRefreshEveryInterval": "A lista será atualizada a cada {refreshInterval}", "ListsLoadError": "Não foi possível carregar as listas", "LocalAirDate": "Data de exibição local", - "LocalPath": "Caminho Local", - "LogLevel": "Nível de Registro", - "LogLevelTraceHelpTextWarning": "O registro de rastreamento deve ser ativado apenas temporariamente", - "Logging": "Registrando", - "LongDateFormat": "Formato de Data Longa", + "LocalPath": "Caminho local", + "LogLevel": "Nível de registro", + "LogLevelTraceHelpTextWarning": "O registro em log deve ser habilitado apenas temporariamente", + "Logging": "Registro em log", + "LongDateFormat": "Formato longo de data", "Lowercase": "Minúsculas", - "ManualImportItemsLoadError": "Não é possível carregar itens de importação manual", + "ManualImportItemsLoadError": "Não foi possível carregar itens de importação manual", "Max": "Máx.", - "MaximumLimits": "Limites Máximos", - "MaximumSingleEpisodeAge": "Idade Máxima de Episódio Único", + "MaximumLimits": "Limites máximos", + "MaximumSingleEpisodeAge": "Idade máxima de episódio único", "MaximumSingleEpisodeAgeHelpText": "Durante uma pesquisa de temporada completa, apenas os pacotes de temporada serão permitidos quando o último episódio da temporada for mais antigo do que esta configuração. Somente série padrão. Use 0 para desabilitar.", - "MaximumSize": "Tamanho Máximo", - "MaximumSizeHelpText": "Tamanho máximo para uma liberação a ser capturada em MB. Defina como zero para definir como ilimitado", + "MaximumSize": "Tamanho máximo", + "MaximumSizeHelpText": "Tamanho máximo, em MB, para obter um lançamento. Zero significa ilimitado", "Mechanism": "Mecanismo", "MediaInfo": "Informações da mídia", - "MediaManagementSettings": "Configurações de Gerenciamento de Mídia", + "MediaManagementSettings": "Configurações de gerenciamento de mídia", "MediaManagementSettingsLoadError": "Não foi possível carregar as configurações de gerenciamento de mídia", "MediaManagementSettingsSummary": "Nomenclatura, configurações de gerenciamento de arquivos e pastas raiz", "MegabytesPerMinute": "Megabytes por minuto", - "MetadataLoadError": "Não é possível carregar metadados", - "MetadataSettings": "Configurações de Metadados", - "MetadataSettingsSummary": "Crie arquivos de metadados quando os episódios são importados ou as séries são atualizadas", - "MetadataSourceSettings": "Configurações de Fontes de Metadados", - "MetadataSourceSettingsSummary": "Informações sobre onde Sonarr obtém informações sobre séries e episódios", + "MetadataLoadError": "Não foi possível carregar os metadados", + "MetadataSettings": "Configurações de metadados", + "MetadataSettingsSeriesSummary": "Criar arquivos de metadados ao importar episódios ou atualizar a série", + "MetadataSourceSettings": "Configurações da fonte de metadados", + "MetadataSourceSettingsSeriesSummary": "Informações sobre onde o {appName} obtém informações sobre séries e episódios", "Min": "Mín.", - "MinimumAge": "Idade Miníma", - "MinimumAgeHelpText": "Somente Usenet: Idade mínima em minutos dos NZBs antes de serem capturados. Use isso para dar aos novos lançamentos tempo para se propagar para seu provedor usenet.", - "MinimumCustomFormatScore": "Pontuação Mínima de Formato Personalizado", + "MinimumAge": "Idade miníma", + "MinimumAgeHelpText": "Somente Usenet: idade mínima, em minutos, dos NZBs antes de serem capturados. Use isso para dar aos novos lançamentos tempo para se propagar para seu provedor de Usenet.", + "MinimumCustomFormatScore": "Pontuação mínima de formato personalizado", "MinimumCustomFormatScoreHelpText": "Pontuação mínima de formato personalizado permitida para download", - "MinimumFreeSpace": "Espaço Livre Mínimo", - "MinimumLimits": "Limites Mínimos", - "MinutesFortyFive": "45 Minutos: {fortyFive}", - "MinutesSixty": "60 Minutos: {sixty}", - "MinutesThirty": "30 Minutos: {thirty}", + "MinimumFreeSpace": "Mínimo de espaço livre", + "MinimumLimits": "Limites mínimos", + "MinutesFortyFive": "45 minutos: {fortyFive}", + "MinutesSixty": "60 minutos: {sixty}", + "MinutesThirty": "30 minutos: {thirty}", "Monday": "Segunda-feira", - "MonitoringOptions": "Opções de Monitoramento", + "MonitoringOptions": "Opções de monitoramento", "MoreDetails": "Mais detalhes", - "MultiEpisode": "Multi Episódio", - "MultiEpisodeInvalidFormat": "Multi Episódio: Formato Inválido", - "MultiEpisodeStyle": "Estilo de Multi Episódios", - "MustContain": "Deve Conter", - "MustNotContain": "Não Deve Conter", - "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais termos (sem distinção entre maiúsculas e minúsculas)", - "NamingSettings": "Configurações de Nomes", - "NamingSettingsLoadError": "Não foi possível carregar as configurações de nomeação", + "MultiEpisode": "Multiepisódio", + "MultiEpisodeInvalidFormat": "Multiepisódio: formato inválido", + "MultiEpisodeStyle": "Estilo de multiepisódio", + "MustContain": "Deve conter", + "MustNotContain": "Não deve conter", + "MustNotContainHelpText": "O lançamento será rejeitado se contiver um ou mais destes termos (sem distinção entre maiúsculas e minúsculas)", + "NamingSettings": "Configurações de nomenclatura", + "NamingSettingsLoadError": "Não foi possível carregar as configurações de nomenclatura", "Never": "Nunca", - "NoChanges": "Sem Alterações", - "NoDelay": "Sem Atraso", + "NoChanges": "Sem alterações", + "NoDelay": "Sem atraso", "NoLinks": "Sem links", "NoTagsHaveBeenAddedYet": "Nenhuma tag foi adicionada ainda", - "None": "Vazio", + "None": "Nenhum", "NotificationTriggers": "Gatilhos de Notificação", "NotificationTriggersHelpText": "Selecione quais eventos devem acionar esta notificação", "NotificationsLoadError": "Não foi possível carregar as notificações", @@ -700,7 +699,7 @@ "OnlyTorrent": "Só Torrent", "OnlyUsenet": "Só Usenet", "OpenBrowserOnStart": "Abrir navegador ao iniciar", - "OpenBrowserOnStartHelpText": " Abra um navegador da Web e navegue até a página inicial do Sonarr no início do aplicativo.", + "OpenBrowserOnStartHelpText": " Abra um navegador da Web e navegue até a página inicial do {appName} no início do aplicativo.", "OptionalName": "Nome opcional", "Original": "Original", "Other": "Outro", @@ -712,7 +711,7 @@ "PortNumber": "Número da Porta", "PreferAndUpgrade": "Preferir e Atualizar", "PreferProtocol": "Preferir {preferredProtocol}", - "PreferTorrent": "Preferir Torrent", + "PreferTorrent": "Preferir torrent", "PreferUsenet": "Preferir Usenet", "Preferred": "Preferido", "PreferredProtocol": "Protocolo Preferido", @@ -730,7 +729,7 @@ "QualitiesHelpText": "Qualidades mais altas na lista são mais preferidas. As qualidades dentro do mesmo grupo são iguais. Somente qualidades verificadas são desejadas", "QualitiesLoadError": "Não foi possível carregar qualidades", "QualityDefinitions": "Definições de Qualidade", - "QualityLimitsHelpText": "Os limites são ajustados automaticamente para o tempo de execução da série e o número de episódios no arquivo.", + "QualityLimitsSeriesRuntimeHelpText": "Os limites são ajustados automaticamente para o tempo de execução da série e o número de episódios no arquivo.", "QualityProfiles": "Perfis de Qualidade", "QualityProfilesLoadError": "Não é possível carregar perfis de qualidade", "QualitySettings": "Configurações de Qualidade", @@ -740,38 +739,37 @@ "RecyclingBinCleanup": "Esvaziar Lixeira", "RecyclingBinCleanupHelpText": "Defina como 0 para desativar a limpeza automática", "RecyclingBinCleanupHelpTextWarning": "Os arquivos na lixeira mais antigos do que o número de dias selecionado serão limpos automaticamente", - "RecyclingBinHelpText": "Os arquivos do episódio irão para cá quando excluídos, em vez de serem excluídos permanentemente", - "RedownloadFailed": "Redownload falhou", + "RecyclingBinHelpText": "Os arquivos irão para cá quando excluídos, em vez de serem excluídos permanentemente", "AbsoluteEpisodeNumber": "Número Absoluto do Episódio", - "AddAutoTagError": "Não foi possível adicionar uma nova tag automática. Tente novamente.", - "AnalyseVideoFilesHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o Sonarr leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", + "AddAutoTagError": "Não foi possível adicionar uma nova tag automática, por favor, tente novamente.", + "AnalyseVideoFilesHelpText": "Extraia informações de vídeo, como resolução, tempo de execução e informações de codec de arquivos. Isso requer que o {appName} leia partes do arquivo que podem causar alta atividade no disco ou na rede durante as varreduras.", "AuthenticationRequiredHelpText": "Altere para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.", "AuthenticationRequiredWarning": "Para evitar o acesso remoto sem autenticação, {appName} agora exige que a autenticação esteja habilitada. Opcionalmente, você pode desabilitar a autenticação de endereços locais.", - "CopyUsingHardlinksHelpText": "Os links rígidos permitem que o Sonarr importe torrents de propagação para a pasta da série sem ocupar espaço extra em disco ou copiar todo o conteúdo do arquivo. Links rígidos só funcionarão se a origem e o destino estiverem no mesmo volume", - "CustomFormatHelpText": "O Sonarr pontua cada lançamento usando a soma das pontuações para corresponder aos formatos personalizados. Se um novo lançamento melhorar a pontuação, com a mesma, ou melhor, qualidade, o Sonarr o baixará.", + "CopyUsingHardlinksSeriesHelpText": "Os links rígidos permitem que o {appName} importe torrents de propagação para a pasta da série sem ocupar espaço extra em disco ou copiar todo o conteúdo do arquivo. Links rígidos só funcionarão se a origem e o destino estiverem no mesmo volume", + "CustomFormatHelpText": "O {appName} pontua cada lançamento usando a soma das pontuações para corresponder aos formatos personalizados. Se um novo lançamento melhorar a pontuação, com a mesma, ou melhor, qualidade, o {appName} o baixará.", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DeleteDelayProfileMessageText": "Tem certeza de que deseja excluir este perfil de atraso?", "DeleteImportListMessageText": "Tem certeza de que deseja excluir a lista '{name}'?", "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir este perfil de lançamento '{name}'?", - "DownloadClientTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", + "DownloadClientSeriesTagHelpText": "Use este cliente de download apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", "EpisodeTitleRequiredHelpText": "Impeça a importação por até 48 horas se o título do episódio estiver no formato de nomenclatura e o título do episódio for TBA", "External": "Externo", "ExtraFileExtensionsHelpText": "Lista separada por vírgulas de arquivos extras para importar (.nfo será importado como .nfo-orig)", "HistoryLoadError": "Não foi possível carregar o histórico", - "IndexerTagHelpText": "Use este indexador apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", - "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages oferece suporte a um sufixo `:EN+DE`, permitindo que você filtre os idiomas incluídos no nome do arquivo. Use `-DE` para excluir idiomas específicos. Acrescentar `+` (por exemplo `:EN+`) resultará em `[EN]`/`[EN+--]`/`[--]` dependendo dos idiomas excluídos. Por exemplo `{MediaInfo Full:EN+DE}`.", + "IndexerTagSeriesHelpText": "Usar este indexador apenas para séries com pelo menos uma tag correspondente. Deixe em branco para usar com todas as séries.", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages oferece suporte a um sufixo \":EN+DE\", permitindo que você filtre os idiomas inclusos no nome do arquivo. Use \"-DE\" para excluir idiomas específicos. Usar \"+\" (p. ex.: \":EN+\") resultará em \"[EN]\"/\"[EN+--]\"/\"[--]\" dependendo dos idiomas excluídos. P. ex.: \"{MediaInfo Full:EN+DE}\".", "MinimumFreeSpaceHelpText": "Impedir a importação se deixar menos do que esta quantidade de espaço em disco disponível", "MustContainHelpText": "O lançamento deve conter pelo menos um desses termos (sem distinção entre maiúsculas e minúsculas)", "NegateHelpText": "Se marcado, o formato personalizado não será aplicado se esta condição {implementationName} corresponder.", - "NoLimitForAnyRuntime": "Sem limite para qualquer tempo de execução", - "NoMinimumForAnyRuntime": "Sem mínimo para qualquer tempo de execução", - "NotificationsTagsHelpText": "Envie notificações apenas para séries com pelo menos uma tag correspondente", + "NoLimitForAnyRuntime": "Sem limite para qualquer duração", + "NoMinimumForAnyRuntime": "Sem mínimo para qualquer duração", + "NotificationsTagsSeriesHelpText": "Envie notificações apenas para séries com pelo menos uma tag correspondente", "OnManualInteractionRequired": "Na Interação Manual Necessária", "PendingChangesMessage": "Você tem alterações não salvas. Tem certeza de que deseja sair desta página?", "ProtocolHelpText": "Escolha qual(is) protocolo(s) usar e qual é o preferido ao escolher entre laçamentos iguais", "ProxyUsernameHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.", "QualityDefinitionsLoadError": "Não foi possível carregar as definições de qualidade", - "QualityProfileInUse": "Não é possível excluir um perfil de qualidade anexado a uma série, lista ou coleção", + "QualityProfileInUseSeriesListCollection": "Não é possível excluir um perfil de qualidade anexado a uma série, lista ou coleção", "EnableColorImpairedModeHelpText": "Estilo alterado para permitir que usuários com deficiência de cor distingam melhor as informações codificadas por cores", "RegularExpression": "Expressão Regular", "RegularExpressionsCanBeTested": "Expressões regulares podem ser testadas [aqui](http://regexstorm.net/tester).", @@ -779,9 +777,9 @@ "ReleaseProfile": "Perfil de Lançamento", "ReleaseProfileIndexerHelpText": "Especifique a qual indexador o perfil se aplica", "ReleaseProfileIndexerHelpTextWarning": "O uso de um indexador específico com perfis de lançamento pode levar à obtenção de lançamentos duplicados", - "ReleaseProfileTagHelpText": "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çamento", - "ReleaseProfilesLoadError": "Não foi possível carregar os perfis de lançamento", + "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", "RemotePath": "Caminho Remoto", "RemotePathMappingHostHelpText": "O mesmo host que você especificou para o Download Client remoto", "RemotePathMappingRemotePathHelpText": "Caminho raiz para o diretório que o Cliente de Download acessa", @@ -793,21 +791,21 @@ "Reorder": "Reordenar", "Repeat": "Repetir", "ReplaceIllegalCharacters": "Substituir Caracteres Ilegais", - "ReplaceIllegalCharactersHelpText": "Substituir caracteres ilegais. Se desmarcado, o Sonarr irá removê-los", + "ReplaceIllegalCharactersHelpText": "Substituir caracteres ilegais. Se desmarcado, o {appName} irá removê-los", "ReplaceWithDash": "Substituir por Traço", "ReplaceWithSpaceDash": "Substituir por Espaço e Traço", - "RescanAfterRefreshHelpText": "Verifique novamente a pasta da série após atualizar a série", - "RescanAfterRefreshHelpTextWarning": "O Sonarr não detectará automaticamente as alterações nos arquivos quando não estiver definido como 'Sempre'", + "RescanAfterRefreshSeriesHelpText": "Verifique novamente a pasta da série após atualizar a série", + "RescanAfterRefreshHelpTextWarning": "O {appName} não detectará automaticamente as alterações nos arquivos quando não estiver definido como 'Sempre'", "RescanSeriesFolderAfterRefresh": "Verificar novamente a pasta da série após a atualização", "ResetAPIKey": "Redefinir chave de API", "ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave de API?", - "ResetDefinitions": "Redefinir Definições", + "ResetDefinitions": "Redefinir definições", "RestartLater": "Vou reiniciar mais tarde", "RestartNow": "Reiniciar Agora", "RestartRequiredHelpTextWarning": "Requer reinicialização para entrar em vigor", - "RestartRequiredToApplyChanges": "O Sonarr requer uma reinicialização para aplicar as alterações. Deseja reiniciar agora?", - "RestartRequiredWindowsService": "Dependendo de qual usuário está executando o serviço Sonarr, pode ser necessário reiniciar o Sonarr como administrador uma vez antes que o serviço seja iniciado automaticamente.", - "RestartSonarr": "Reiniciar Sonarr", + "RestartRequiredToApplyChanges": "O {appName} requer uma reinicialização para aplicar as alterações. Deseja reiniciar agora?", + "RestartRequiredWindowsService": "Dependendo de qual usuário está executando o serviço {appName}, pode ser necessário reiniciar o {appName} como administrador uma vez antes que o serviço seja iniciado automaticamente.", + "RestartSonarr": "Reiniciar {appName}", "RestrictionsLoadError": "Não foi possível carregar as Restrições", "Retention": "Retenção", "RetentionHelpText": "Somente Usenet: defina como zero para definir a retenção ilimitada", @@ -819,7 +817,7 @@ "RssSyncIntervalHelpText": "Intervalo em minutos. Defina como zero para desativar (isso interromperá todas as capturas de lançamentos automáticas)", "RssSyncIntervalHelpTextWarning": "Isso se aplica a todos os indexadores, siga as regras estabelecidas por eles", "SaveChanges": "Salvar Mudanças", - "SaveSettings": "Salvar Configurações", + "SaveSettings": "Salvar configurações", "Score": "Pontuação", "Script": "Script", "ScriptPath": "Caminho do Script", @@ -844,12 +842,12 @@ "SingleEpisode": "Episódio Único", "SizeLimit": "Limite de Tamanho", "SkipFreeSpaceCheck": "Ignorar verificação de espaço livre", - "SkipFreeSpaceCheckWhenImportingHelpText": "Use quando o Sonarr não conseguir detectar espaço livre na pasta raiz da série", - "SmartReplace": "Substituição Inteligente", + "SkipFreeSpaceCheckWhenImportingHelpText": "Use quando {appName} não consegue detectar espaço livre em sua pasta raiz durante a importação do arquivo", + "SmartReplace": "Substituição inteligente", "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome", "Socks4": "Socks4", "Socks5": "Socks5 (Suporte à TOR)", - "SonarrTags": "Tags do Sonarr", + "SonarrTags": "Tags do {appName}", "Space": "Espaço", "SpecialsFolderFormat": "Formato da Pasta para Especiais", "SslCertPassword": "Senha do Certificado SSL", @@ -860,14 +858,14 @@ "StandardEpisodeFormat": "Formato do Episódio Padrão", "Style": "Estilo", "Sunday": "Domingo", - "SupportedAutoTaggingProperties": "O Sonarr suporta as seguintes propriedades para regras de marcação automática", - "SupportedCustomConditions": "O Sonarr oferece suporte a condições personalizadas nas propriedades de lançamento abaixo.", - "SupportedDownloadClients": "O Sonarr suporta muitos clientes populares de download de torrent e usenet.", + "SupportedAutoTaggingProperties": "{appName} oferece suporte às propriedades a seguir para regras de codificação automática", + "SupportedCustomConditions": "O {appName} oferece suporte a condições personalizadas nas propriedades de lançamento abaixo.", + "SupportedDownloadClients": "O {appName} suporta muitos clientes populares de download de torrent e usenet.", "SupportedDownloadClientsMoreInfo": "Para obter mais informações sobre os clientes de download individuais, clique nos botões de mais informações.", "SupportedImportListsMoreInfo": "Para obter mais informações sobre as listas de importação individuais, clique nos botões de mais informações.", - "SupportedIndexers": "O Sonarr suporta qualquer indexador que use o padrão Newznab, bem como outros indexadores listados abaixo.", + "SupportedIndexers": "O {appName} suporta qualquer indexador que use o padrão Newznab, bem como outros indexadores listados abaixo.", "SupportedIndexersMoreInfo": "Para obter mais informações sobre os indexadores individuais, clique nos botões de mais informações.", - "SupportedLists": "O Sonarr oferece suporte a várias listas para importar séries para o banco de dados.", + "SupportedListsSeries": "O {appName} oferece suporte a várias listas para importar séries para o banco de dados.", "TagCannotBeDeletedWhileInUse": "A tag não pode ser excluída durante o uso", "TagDetails": "Detalhes da Tag - {label}", "TagIsNotUsedAndCanBeDeleted": "A tag não é usada e pode ser excluída", @@ -890,7 +888,7 @@ "TypeOfList": "{typeOfList} Lista", "Ui": "IU", "UiLanguage": "Idioma da UI", - "UiLanguageHelpText": "Idioma que o Sonarr usará para interface do usuário", + "UiLanguageHelpText": "Idioma que o {appName} usará para interface do usuário", "UiSettings": "Configurações da UI", "UiSettingsSummary": "Opções de calendário, data e cores para deficientes visuais", "Underscore": "Sublinhar", @@ -900,16 +898,16 @@ "UnsavedChanges": "Alterações Não Salvas", "UpdateAutomaticallyHelpText": "Baixe e instale atualizações automaticamente. Você ainda poderá instalar a partir do Sistema: Atualizações", "UpdateMechanismHelpText": "Use o atualizador integrado do {appName} ou um script", - "UpdateSonarrDirectlyLoadError": "Incapaz de atualizar o Sonarr diretamente,", - "UpdateUiNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de IU '{0}' não pode ser salva pelo usuário '{1}'.", + "UpdateSonarrDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,", + "UpdateUiNotWritableHealthCheckMessage": "Não é possível instalar a atualização porque a pasta de IU '{uiFolder}' não pode ser salva pelo usuário '{userName}'.", "UpgradeUntil": "Atualizar Até", "UpgradeUntilCustomFormatScore": "Atualizar até pontuação de formato personalizado", - "UpgradeUntilHelpText": "Quando essa qualidade for atingida, o Sonarr não fará mais download de episódios", + "UpgradeUntilEpisodeHelpText": "Quando essa qualidade for atingida, o {appName} não fará mais download de episódios", "UpgradeUntilThisQualityIsMetOrExceeded": "Atualize até que essa qualidade seja atendida ou excedida", "UpgradesAllowed": "Atualizações Permitidas", "UpgradesAllowedHelpText": "se as qualidades desativadas não forem atualizadas", "Uppercase": "Maiúsculo", - "UrlBase": "URL Base", + "UrlBase": "URL base", "UrlBaseHelpText": "Para suporte a proxy reverso, o padrão é vazio", "UseProxy": "Usar Proxy", "Usenet": "Usenet", @@ -921,9 +919,9 @@ "UtcAirDate": "Data de Exibição UTC", "WeekColumnHeader": "Cabeçalho da Coluna da Semana", "WeekColumnHeaderHelpText": "Mostrado acima de cada coluna quando a semana é a exibição ativa", - "RemotePathMappingLocalPathHelpText": "Caminho que o Sonarr deve usar para acessar o caminho remoto localmente", + "RemotePathMappingLocalPathHelpText": "Caminho que o {appName} deve usar para acessar o caminho remoto localmente", "RemoveCompletedDownloadsHelpText": "Remover downloads importados do histórico do cliente de download", - "RenameEpisodesHelpText": "O Sonarr usará o nome do arquivo existente se a renomeação estiver desativada", + "RenameEpisodesHelpText": "O {appName} usará o nome do arquivo existente se a renomeação estiver desativada", "ReplaceWithSpaceDashSpace": "Substituir com Espaço, Traço e Espaço", "RequiredHelpText": "Esta condição {implementationName} deve corresponder para que o formato personalizado seja aplicado. Caso contrário, uma única correspondência {implementationName} é suficiente.", "SetPermissionsLinuxHelpText": "O chmod deve ser executado quando os arquivos são importados/renomeados?", @@ -931,13 +929,13 @@ "SupportedListsMoreInfo": "Para obter mais informações sobre as listas individuais, clique nos botões de mais informações.", "ThemeHelpText": "Alterar o tema da interface do usuário do aplicativo, o tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por Theme.Park", "UiSettingsLoadError": "Não foi possível carregar as configurações da UI", - "UnmonitorDeletedEpisodesHelpText": "Os episódios excluídos do disco são deixados de ser monitorados automaticamente no Sonarr", + "UnmonitorDeletedEpisodesHelpText": "Os episódios excluídos do disco são deixados de ser monitorados automaticamente no {appName}", "UpdateScriptPathHelpText": "Caminho para um script personalizado que usa um pacote de atualização extraído e lida com o restante do processo de atualização", - "UpgradeUntilCustomFormatScoreHelpText": "Assim que essa pontuação de formato personalizado for alcançada, o Sonarr não baixará mais lançamentos de episódios", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Assim que essa pontuação de formato personalizado for alcançada, o {appName} não baixará mais lançamentos de episódios", "UseHardlinksInsteadOfCopy": "Usar links rígidos ao invés de Copiar", "VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ", "WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)", - "UnmonitorSpecials": "Não Monitorar Especiais", + "UnmonitorSpecialEpisodes": "Não Monitorar Especiais", "MonitorAllEpisodes": "Todos os Episódios", "AddNewSeries": "Adicionar Novas Séries", "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja adicionar.", @@ -955,10 +953,10 @@ "ImportErrors": "Erros de Importação", "ImportExistingSeries": "Importar Série Existente", "ImportSeries": "Importar Séries", - "LibraryImportHeader": "Importa séries que você já possui", + "LibraryImportSeriesHeader": "Importar as séries que você já possui", "LibraryImportTips": "Algumas dicas para garantir que a importação ocorra sem problemas:", - "LibraryImportTipsDontUseDownloadsFolder": "Não use para importar downloads de seu cliente de download, isso é apenas para bibliotecas organizadas existentes, não para arquivos não organizados.", - "LibraryImportTipsQualityInFilename": "Certifique-se de que seus arquivos incluam a qualidade em seus nomes de arquivo. Por exemplo: `episode.s02e15.bluray.mkv`", + "LibraryImportTipsDontUseDownloadsFolder": "Não use para importar downloads de seu cliente. Isso se aplica apenas a bibliotecas organizadas existentes, e não a arquivos desorganizados.", + "LibraryImportTipsQualityInEpisodeFilename": "Certifique-se de que seus arquivos incluam a qualidade nos nomes de arquivo. Por exemplo: \"episódio.s02e15.bluray.mkv\"", "Monitor": "Monitorar", "MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais", "MonitorExistingEpisodes": "Episódios Existentes", @@ -966,44 +964,42 @@ "MonitorFirstSeasonDescription": "Monitorar todos os episódios da primeira temporada. As demais temporadas serão ignoradas", "MonitorFutureEpisodes": "Futuros Episódios", "MonitorFutureEpisodesDescription": "Monitorar episódios que não foram exibidos", - "MonitorLatestSeason": "Última Temporada", - "MonitorMissingEpisodes": "Episódios Ausentes", + "MonitorMissingEpisodes": "Episódios ausentes", "MonitorMissingEpisodesDescription": "Monitora os episódios que não possuem arquivos ou ainda não foram ao ar", - "MonitorNone": "Nada", - "MonitorNoneDescription": "Nenhum episódio será monitorado", - "MonitorSpecials": "Monitorar Especiais", - "MonitorSpecialsDescription": "Monitore todos os episódios especiais sem alterar o status monitorado de outros episódios", + "MonitorNoEpisodes": "Nenhum", + "MonitorNoEpisodesDescription": "Nenhum episódio será monitorado", + "MonitorSpecialEpisodes": "Monitorar especiais", + "MonitorSpecialEpisodesDescription": "Monitorar todos os episódios especiais sem alterar o status de monitoramento de outros episódios", "NoMatchFound": "Nenhum resultado encontrado!", "ProcessingFolders": "Processando Pastas", "SearchByTvdbId": "Você também pode pesquisar usando o ID TVDB de um programa. Por exemplo: tvdb:71663", "SearchFailedError": "Falha na pesquisa, tente novamente mais tarde.", "SeriesTypesHelpText": "O tipo de série é usado para renomear, analisar e pesquisar", "Standard": "Padrão", - "StandardTypeDescription": "Episódios lançados com o padrão SxxEyy", + "StandardEpisodeTypeDescription": "Episódios lançados com o padrão SxxEyy", "StartImport": "Iniciar Importação", "StartProcessing": "Iniciar Processamento", "Upcoming": "Por vir", - "AddNewSeriesError": "Falha ao carregar os resultados da pesquisa, tente novamente.", + "AddNewSeriesError": "Falha ao carregar os resultados da pesquisa. Tente novamente.", "AddNewSeriesRootFolderHelpText": "A subpasta '{folder}' será criada automaticamente", - "AnimeTypeDescription": "Episódios lançados usando um número de episódio absoluto", - "DailyTypeDescription": "Episódios lançados diariamente ou com menos frequência que usam ano-mês-dia (2023-08-04)", - "LibraryImportTipsUseRootFolder": "Aponte o Sonarr para a pasta que contém todos os seus programas de TV, não um específico. Por exemplo. '`{goodFolderExample}`' e não '`{badFolderExample}`'. Além disso, cada série deve estar em sua própria pasta dentro da pasta raiz/biblioteca.", + "AnimeEpisodeTypeDescription": "Episódios lançados usando um número de episódio absoluto", + "DailyEpisodeTypeDescription": "Episódios lançados diariamente ou com menos frequência que usam ano-mês-dia (2023-08-04)", + "LibraryImportTipsSeriesUseRootFolder": "Aponte o {appName} para a pasta que contém todas as suas séries, não uma específica. Por exemplo. \"`{goodFolderExample}`\" e não \"`{badFolderExample}`\". Além disso, cada série deve estar em sua própria pasta dentro da pasta raiz/biblioteca.", "MonitorExistingEpisodesDescription": "Monitorar os episódios que possuem arquivos ou ainda não foram exibidos", - "MonitorLatestSeasonDescription": "Monitora todos os episódios da última temporada que foram ao ar nos últimos 90 dias e todas as temporadas futuras", "NoSeriesHaveBeenAdded": "Você ainda não adicionou nenhuma série. Deseja importar algumas ou todas as suas séries primeiro?", - "UnmonitorSpecialsDescription": "Cancela o monitoramento de todos os episódios especiais sem alterar o status monitorado de outros episódios", + "UnmonitorSpecialsEpisodesDescription": "Cancela o monitoramento de todos os episódios especiais sem alterar o status monitorado de outros episódios", "WhyCantIFindMyShow": "Por que não consigo encontrar meu programa?", "EpisodeImported": "Episódio importado", "Month": "Mês", "Today": "Hoje", - "AgeWhenGrabbed": "Idade (quando baixado)", + "AgeWhenGrabbed": "Tempo de vida (quando obtido)", "DelayingDownloadUntil": "Atrasando o download até {date} às {time}", - "DeletedReasonMissingFromDisk": "O Sonarr 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 UI", + "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", "DownloadFailed": "Download Falhou", "DestinationRelativePath": "Caminho Relativo de Destino", - "DownloadIgnoredTooltip": "Download do Episódio Ignorado", - "DownloadFailedTooltip": "O download do episódio falhou", + "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", + "DownloadFailedEpisodeTooltip": "O download do episódio falhou", "DownloadIgnored": "Download ignorado", "DownloadWarning": "Aviso de download: {warningMessage}", "Downloading": "Baixando", @@ -1014,10 +1010,10 @@ "EpisodeFileRenamed": "Arquivo do Episódio Renomeado", "GrabId": "Obter ID", "GrabSelected": "Obter Selecionado", - "GrabbedHistoryTooltip": "Episódio retirado de {indexer} e enviado para {downloadClient}", - "ImportedTo": "Importado Para", - "InfoUrl": "URL de Info", - "MarkAsFailed": "Marcar como Falha", + "EpisodeGrabbedTooltip": "Episódio retirado de {indexer} e enviado para {downloadClient}", + "ImportedTo": "Importado para", + "InfoUrl": "URL da info", + "MarkAsFailed": "Marcar como falha", "NoHistoryFound": "Nenhum histórico encontrado", "Ok": "Ok", "Paused": "Pausado", @@ -1047,17 +1043,17 @@ "ShowEpisodeInformationHelpText": "Mostrar título e número do episódio", "SourcePath": "Caminho da Fonte", "SpecialEpisode": "Episódio Especial", - "Agenda": "Agenda", + "Agenda": "Programação", "AnEpisodeIsDownloading": "Um episódio está baixando", - "CalendarLegendMissingTooltip": "O episódio foi ao ar e está faltando no disco", + "CalendarLegendEpisodeMissingTooltip": "O episódio foi ao ar e está faltando no disco", "CalendarFeed": "{appName} Feed do Calendário", - "CalendarLegendDownloadedTooltip": "O episódio foi baixado e classificado", - "CalendarLegendDownloadingTooltip": "O episódio está sendo baixado no momento", - "CalendarLegendFinaleTooltip": "Final de série ou temporada", - "CalendarLegendOnAirTooltip": "Episódio está sendo exibido no momento", - "CalendarLegendPremiereTooltip": "Estreia de série ou temporada", - "CalendarLegendUnairedTooltip": "Episódio ainda não foi ao ar", - "CalendarLegendUnmonitoredTooltip": "Episódio não monitorado", + "CalendarLegendEpisodeDownloadedTooltip": "O episódio foi baixado e classificado", + "CalendarLegendEpisodeDownloadingTooltip": "O episódio está sendo baixado no momento", + "CalendarLegendSeriesFinaleTooltip": "Final de série ou temporada", + "CalendarLegendEpisodeOnAirTooltip": "Episódio está sendo exibido no momento", + "CalendarLegendSeriesPremiereTooltip": "Estreia de série ou temporada", + "CalendarLegendEpisodeUnairedTooltip": "Episódio ainda não foi ao ar", + "CalendarLegendEpisodeUnmonitoredTooltip": "Episódio não monitorado", "CalendarOptions": "Opções de Calendário", "CheckDownloadClientForDetails": "verifique o cliente de download para mais detalhes", "CollapseMultipleEpisodes": "Agrupar Múltiplos Episódios", @@ -1074,7 +1070,7 @@ "FullColorEvents": "Eventos em Cores", "Global": "Global", "ICalFeedHelpText": "Copie esta URL para seu(s) cliente(s) ou clique para se inscrever se seu navegador for compatível com webcal", - "ICalIncludeUnmonitoredHelpText": "Incluir episódios não monitorados no feed iCal", + "ICalIncludeUnmonitoredEpisodesHelpText": "Incluir episódios não monitorados no feed iCal", "ICalSeasonPremieresOnlyHelpText": "Apenas o primeiro episódio de uma temporada estará no feed", "ICalShowAsAllDayEventsHelpText": "Os eventos aparecerão como eventos de dia inteiro em seu calendário", "IconForCutoffUnmet": "Ícone para Corte Não Atendido", @@ -1084,12 +1080,12 @@ "IconForSpecialsHelpText": "Mostrar ícone para episódios especiais (temporada 0)", "ImportFailed": "Falha na importação: {sourceTitle}", "EpisodeMissingAbsoluteNumber": "O episódio não tem um número de episódio absoluto", - "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor de status, em vez de apenas a borda esquerda. Não se aplica à Agenda", - "ICalTagsHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", + "FullColorEventsHelpText": "Estilo alterado para colorir todo o evento com a cor do status, em vez de apenas a borda esquerda. Não se aplica à Agenda", + "ICalTagsSeriesHelpText": "O feed conterá apenas séries com pelo menos uma tag correspondente", "IconForFinalesHelpText": "Mostrar ícone para finais de séries/temporadas com base nas informações de episódios disponíveis", - "NoHistoryBlocklist": "Sem histórico na lista de bloqueio", + "NoHistoryBlocklist": "Não há lista de bloqueio no histórico", "QualityCutoffNotMet": "O corte de qualidade não foi atingido", - "QueueLoadError": "Falha ao carregar a Fila", + "QueueLoadError": "Falha ao carregar a fila", "RemoveQueueItem": "Remover - {sourceTitle}", "RemoveQueueItemConfirmation": "Tem certeza de que deseja remover '{sourceTitle}' da fila?", "Absolute": "Absoluto", @@ -1103,9 +1099,9 @@ "AddIndexerImplementation": "Adicionar Indexador - {implementationName}", "AddToDownloadQueue": "Adicionar à fila de download", "AddedToDownloadQueue": "Adicionado à fila de download", - "Airs": "Exibições", + "Airs": "Vai ao ar em", "AirsDateAtTimeOn": "{date} às {time} em {networkLabel}", - "AnimeTypeFormat": "Número absoluto do episódio ({format})", + "AnimeEpisodeTypeFormat": "Número absoluto do episódio ({format})", "AppUpdatedVersion": "{appName} foi atualizado para a versão `{version}`, para obter as alterações mais recentes, você precisará recarregar {appName} ", "ChooseImportMode": "Escolha o Modo de Importação", "ClickToChangeEpisode": "Clique para alterar o episódio", @@ -1122,18 +1118,18 @@ "FilterEpisodesPlaceholder": "Filtrar episódios por título ou número", "FilterIsAfter": "está depois", "Grab": "Obter", - "GrabReleaseMessageText": "Sonarr não conseguiu determinar para qual série e episódio era este lançamento. O Sonarr pode não conseguir importar automaticamente esta versão. Você quer pegar '{title}'?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "O {appName} não conseguiu determinar para qual série e episódio é este lançamento. O {appName} pode não conseguir importar automaticamente este lançamento. Deseja obter \"{title}\"?", "ICalFeed": "Feed do iCal", "ICalLink": "Link do iCal", "InteractiveImportLoadError": "Não foi possível carregar itens de importação manual", "InteractiveImportNoFilesFound": "Nenhum arquivo de vídeo foi encontrado na pasta selecionada", "InteractiveImportNoSeason": "A temporada deve ser escolhida para cada arquivo selecionado", - "InteractiveSearchResultsFailedErrorMessage": "A pesquisa falhou porque é {message}. Tente atualizar as informações da série e verifique se as informações necessárias estão presentes antes de pesquisar novamente.", - "KeyboardShortcutsFocusSearchBox": "Foco na Caixa de Pesquisa", - "KeyboardShortcutsSaveSettings": "Salvar Configurações", - "LocalStorageIsNotSupported": "O armazenamento local não é compatível ou está desativado. Um plugin ou navegação privada pode tê-lo desativado.", - "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis ao executar como um serviço do Windows, consulte as [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote -server) para obter mais informações.", - "AirsTbaOn": "TBA em {networkLabel}", + "InteractiveSearchResultsSeriesFailedErrorMessage": "A pesquisa falhou porque {message}. Tente atualizar as informações da série e verifique se as informações necessárias estão presentes antes de pesquisar novamente.", + "KeyboardShortcutsFocusSearchBox": "Selecionar a caixa de pesquisa", + "KeyboardShortcutsSaveSettings": "Salvar configurações", + "LocalStorageIsNotSupported": "O armazenamento local não é compatível ou está desabilitado. Um plugin ou a navegação privada pode tê-lo desativado.", + "MappedNetworkDrivesWindowsService": "As unidades de rede mapeadas não estão disponíveis ao executar como um serviço do Windows, consulte as [Perguntas frequentes](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote -server) para saber mais.", + "AirsTbaOn": "A ser anunciado em {networkLabel}", "AirsTimeOn": "{time} em {networkLabel}", "AirsTomorrowOn": "Amanhã às {time} em {networkLabel}", "AllFiles": "Todos os Arquivos", @@ -1146,11 +1142,11 @@ "ClickToChangeSeason": "Clique para mudar a temporada", "ClickToChangeSeries": "Clique para mudar de série", "ConnectionLost": "Conexão Perdida", - "ConnectionLostToBackend": "{appName} perdeu sua conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLostToBackend": "{appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", "Continuing": "Continuando", "CountSelectedFile": "{selectedCount} arquivo selecionado", "CustomFilters": "Filtros Personalizados", - "DailyTypeFormat": "Data ({format})", + "DailyEpisodeTypeFormat": "Data ({format})", "Default": "Padrão", "DeleteEpisodeFileMessage": "Tem certeza de que deseja excluir '{path}'?", "DeleteEpisodeFromDisk": "Excluir episódio do disco", @@ -1171,7 +1167,7 @@ "ExistingSeries": "Série Existente", "FailedToLoadCustomFiltersFromApi": "Falha ao carregar filtros personalizados da API", "FailedToLoadSeriesFromApi": "Falha ao carregar a série da API", - "FailedToLoadSonarr": "Falha ao carregar Sonarr", + "FailedToLoadSonarr": "Falha ao carregar {appName}", "FailedToLoadSystemStatusFromApi": "Falha ao carregar o status do sistema da API", "FailedToLoadTagsFromApi": "Falha ao carregar tags da API", "FailedToLoadTranslationsFromApi": "Falha ao carregar as traduções da API", @@ -1200,23 +1196,23 @@ "FilterNotInNext": "não no próximo", "FilterSeriesPlaceholder": "Filtrar séries", "FilterStartsWith": "começa com", - "GrabRelease": "Obter Lançamento", + "GrabRelease": "Obter lançamento", "HardlinkCopyFiles": "Hardlink/Copiar Arquivos", - "InteractiveImportNoEpisode": "Um ou mais episódios devem ser escolhidos para cada arquivo selecionado", - "InteractiveImportNoImportMode": "Um modo de importação deve ser selecionado", - "InteractiveImportNoLanguage": "O(s) idioma(s) deve(m) ser escolhido(s) para cada arquivo selecionado", - "InteractiveImportNoQuality": "A qualidade deve ser escolhida para cada arquivo selecionado", + "InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado", + "InteractiveImportNoImportMode": "Defina um modo de importação", + "InteractiveImportNoLanguage": "Defina um idioma para cada arquivo selecionado", + "InteractiveImportNoQuality": "Defina a qualidade para cada arquivo selecionado", "InteractiveImportNoSeries": "A série deve ser escolhida para cada arquivo selecionado", - "KeyboardShortcuts": "Atalhos do Teclado", - "KeyboardShortcutsCloseModal": "Fechar Modal Atual", - "KeyboardShortcutsConfirmModal": "Aceitar Modal de Confirmação", - "KeyboardShortcutsOpenModal": "Abrir Este Modal", + "KeyboardShortcuts": "Atalhos de teclado", + "KeyboardShortcutsCloseModal": "Fechar pop-up atual", + "KeyboardShortcutsConfirmModal": "Aceitar o pop-up de confirmação", + "KeyboardShortcutsOpenModal": "Abrir este pop-up", "Local": "Local", "Logout": "Sair", - "ManualGrab": "Baixar Manualmente", - "ManualImport": "Importação Manual", - "Mapping": "Mapeando", - "MarkAsFailedConfirmation": "Tem certeza de que deseja marcar '{sourceTitle}' como reprovado?", + "ManualGrab": "Obter manualmente", + "ManualImport": "Importação manual", + "Mapping": "Mapeamento", + "MarkAsFailedConfirmation": "Tem certeza de que deseja marcar \"{sourceTitle}\" como em falha?", "MidseasonFinale": "Final da Meia Temporada", "More": "Mais", "MyComputer": "Meu Computador", @@ -1235,7 +1231,7 @@ "ReleaseSceneIndicatorSourceMessage": "Existem lançamentos de {message} com numeração ambígua, incapaz de identificar o episódio de forma confiável.", "ReleaseSceneIndicatorUnknownMessage": "A numeração varia para este episódio e o lançamento não corresponde a nenhum mapeamento conhecido.", "ReleaseSceneIndicatorUnknownSeries": "Episódio ou série desconhecida.", - "RemotePathMappingsInfo": "Raramente são necessários mapeamentos de caminho remoto, se {app} e seu cliente de download estiverem no mesmo sistema, é melhor combinar seus caminhos. Para mais informações, consulte o [wiki]({wikiLink})", + "RemotePathMappingsInfo": "Raramente são necessários mapeamentos de caminho remoto, se {appName} e seu cliente de download estiverem no mesmo sistema, é melhor combinar seus caminhos. Para mais informações, consulte o [wiki]({wikiLink})", "RemoveFilter": "Remover filtro", "RootFolderSelectFreeSpace": "{freeSpace} Livre", "Search": "Pesquisar", @@ -1244,9 +1240,9 @@ "SelectSeason": "Selecionar Temporada", "SelectSeasonModalTitle": "{modalTitle} - Selecione a Temporada", "SetReleaseGroup": "Definir Grupo do Lançamento", - "StandardTypeFormat": "Números da temporada e do episódio ({format})", + "StandardEpisodeTypeFormat": "Números da temporada e do episódio ({format})", "TableColumnsHelpText": "Escolha quais colunas são visíveis e em que ordem elas aparecem", - "TablePageSizeMinimum": "O tamanho da página deve ser de pelo menos {minimumValue}", + "TablePageSizeMinimum": "O tamanho da página precisa ser de pelo menos {minimumValue}", "Umask750Description": "{octal} - gravação do proprietário, leitura do grupo", "Umask775Description": "{octal} - gravação do proprietário e do grupo, leitura de outros", "Week": "Semana", @@ -1254,16 +1250,16 @@ "SceneNumberNotVerified": "O número da Scene ainda não foi verificado", "MetadataProvidedBy": "Os metadados são fornecidos por {provider}", "Mixed": "Misturado", - "MoveFiles": "Mover Arquivos", - "MultiLanguages": "Multi-Idiomas", + "MoveFiles": "Mover arquivos", + "MultiLanguages": "Vários idiomas", "NoEpisodesFoundForSelectedSeason": "Nenhum episódio foi encontrado para a temporada selecionada", - "NotificationStatusSingleClientHealthCheckMessage": "Notificações indisponíveis devido a falhas: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notificações indisponíveis devido a falhas: {notificationNames}", "Or": "ou", "Organize": "Organizar", "OrganizeLoadError": "Erro ao carregar visualizações", "OrganizeModalHeader": "Organizar & Renomear", "OrganizeNamingPattern": "Padrão de nomenclatura: `{episodeFormat}`", - "OrganizeNothingToRename": "Sucesso! Meu trabalho está feito, nenhum arquivo para renomear.", + "OrganizeNothingToRename": "Sucesso! Meu trabalho está concluído, não há arquivos para renomear.", "OrganizeRelativePaths": "Todos os caminhos são relativos a: `{path}`", "OverrideAndAddToDownloadQueue": "Substituir e adicionar à fila de download", "OverrideGrabModalTitle": "Substituir e Baixar - {title}", @@ -1298,7 +1294,7 @@ "TableOptions": "Opções de Tabela", "TablePageSize": "Tamanho da Página", "TablePageSizeHelpText": "Número de itens a serem exibidos em cada página", - "TablePageSizeMaximum": "O tamanho da página não deve exceder {maximumValue}", + "TablePageSizeMaximum": "O tamanho da página não pode exceder {maximumValue}", "Tba": "A ser anunciado", "Titles": "Título", "ToggleMonitoredSeriesUnmonitored ": "Não é possível alternar o estado monitorado quando a série não é monitorada", @@ -1306,7 +1302,7 @@ "ToggleUnmonitoredToMonitored": "Não monitorado, clique para monitorar", "TotalRecords": "Total de registros: {totalRecords}", "True": "Verdadeiro", - "Umask": "Máscara de Usuário", + "Umask": "Desmascarar", "Umask755Description": "{octal} - Escrita do proprietário, todos os outros lêem", "Umask770Description": "{octal} - proprietário e gravação do grupo", "Umask777Description": "{octal} - Todo mundo escreve", @@ -1316,17 +1312,17 @@ "WhatsNew": "O que há de novo?", "Rejections": "Rejeições", "Connection": "Conexão", - "CustomFormatJson": "JSON do Formato Personalizado", + "CustomFormatJson": "JSON do formato personalizado", "Database": "Banco de dados", - "HealthMessagesInfoBox": "Você pode encontrar mais informações sobre a causa dessas mensagens de verificação de integridade clicando no link da wiki (ícone do livro) no final da linha ou verificando seus [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, você pode entrar em contato com nosso suporte, nos links abaixo.", + "HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.", "ImdbId": "ID do IMDb", "Port": "Porta", "ShowUnknownSeriesItems": "Mostrar Itens de Séries Desconhecidas", - "ShowUnknownSeriesItemsHelpText": "Mostrar itens sem uma série na fila, isso pode incluir séries, filmes removidos ou qualquer outra coisa na categoria do Sonarr", + "ShowUnknownSeriesItemsHelpText": "Mostrar itens sem uma série na fila, isso pode incluir séries, filmes removidos ou qualquer outra coisa na categoria do {appName}", "Test": "Teste", "Level": "Nível", - "AddListExclusion": "Adicionar à Lista de Exclusão", - "AddListExclusionHelpText": "Impedir que séries sejam adicionadas ao Sonarr por listas", + "AddListExclusion": "Adicionar exclusão à lista", + "AddListExclusionSeriesHelpText": "Impedir que o {appName} adicione séries por listas", "EditSeriesModalHeader": "Editar - {title}", "EditSelectedSeries": "Editar Séries Selecionadas", "HideEpisodes": "Ocultar episódios", @@ -1355,7 +1351,7 @@ "SeriesIndexFooterMissingUnmonitored": "Episódios Ausentes (Série não monitorada)", "DefaultNameCopiedProfile": "{name} - Cópia", "DefaultNameCopiedSpecification": "{name} - Cópia", - "DeleteEpisodesFilesHelpText": "Exclua os arquivos do episódio e a pasta da série", + "DeleteEpisodesFilesHelpText": "Excluir os arquivos do episódio e a pasta da série", "DeleteSelectedSeries": "Excluir Séries Selecionadas", "DeleteSeriesFolder": "Excluir Pasta da Série", "DeleteSeriesFolderConfirmation": "A pasta da série `{path}` e todo o seu conteúdo serão excluídos.", @@ -1365,32 +1361,32 @@ "DeleteSeriesModalHeader": "Excluir - {title}", "DeletedSeriesDescription": "A série foi excluída do TheTVDB", "DetailedProgressBarHelpText": "Mostrar texto na barra de progresso", - "ExpandAll": "Expandir Todos", + "ExpandAll": "Expandir tudo", "Files": "Arquivos", "HistorySeason": "Exibir histórico para esta temporada", - "HistoryModalHeaderSeason": "Histórico {season}", + "HistoryModalHeaderSeason": "Histórico - {season}", "InteractiveSearchModalHeader": "Pesquisa Interativa", - "InteractiveSearchModalHeaderSeason": "Pesquisa Interativa - {season}", + "InteractiveSearchModalHeaderSeason": "Pesquisa interativa - {season}", "InteractiveSearchSeason": "Pesquisa interativa para todos os episódios desta temporada", - "InvalidUILanguage": "Sua IU está definida com um idioma inválido, corrija-a e salve suas configurações", + "InvalidUILanguage": "A interface está configurada com um idioma inválido, corrija-o e salve as configurações", "Large": "Grande", "Links": "Links", - "ManageEpisodes": "Gerenciar Episódios", - "ManageEpisodesSeason": "Gerenciar Arquivos de Episódios nesta temporada", + "ManageEpisodes": "Gerenciar episódios", + "ManageEpisodesSeason": "Gerenciar arquivos de episódios nesta temporada", "Medium": "Médio", "MonitorSeries": "Monitorar Série", - "MonitoredHelpText": "Baixa episódios monitorados desta série", + "MonitoredEpisodesHelpText": "Baixar episódios monitorados desta série", "MonitoredStatus": "Monitorado/Status", "Monitoring": "Monitorando", "NoEpisodeInformation": "Nenhuma informação do episódio está disponível.", "NoEpisodesInThisSeason": "Não há episódios nesta temporada", - "NoHistory": "Sem histórico", + "NoHistory": "Não há histórico", "NoMonitoredEpisodesSeason": "Nenhum episódio monitorado nesta temporada", "OrganizeSelectedSeriesModalConfirmation": "Tem certeza de que deseja organizar todos os arquivos da {count} série selecionada?", "OrganizeSelectedSeriesModalHeader": "Organizar Séries Selecionadas", - "Overview": "Visão Geral", - "OverviewOptions": "Opções de Visão Geral", - "PosterOptions": "Opções de Pôster", + "Overview": "Visão geral", + "OverviewOptions": "Opções da visão geral", + "PosterOptions": "Opções do pôster", "PosterSize": "Tamanho do Pôster", "Posters": "Pôsteres", "RefreshAndScan": "Atualizar & Escanear", @@ -1416,7 +1412,7 @@ "ShowSeasonCount": "Mostrar Número da Temporada", "ShowSizeOnDisk": "Mostrar Tamanho no Disco", "ShowTitle": "Mostrar Título", - "ShowTitleHelpText": "Mostrar o título da série abaixo do pôster", + "ShowSeriesTitleHelpText": "Mostrar o título da série abaixo do pôster", "Small": "Pequeno", "StopSelecting": "Pare de Selecionar", "Table": "Tabela", @@ -1424,13 +1420,13 @@ "UnselectAll": "Desmarcar Todos", "UpcomingSeriesDescription": "A série foi anunciada, mas ainda não há data exata para ir ao ar", "UpdateAll": "Atualizar Tudo", - "UpdateFiltered": "Atualizar Filtrado", + "UpdateFiltered": "Atualização Filtrada", "UpdateMonitoring": "Atualizar Monitoramento", "UpdateSelected": "Atualizar Selecionado", "UseSeasonFolder": "Usar Pasta de Temporada", "UseSeasonFolderHelpText": "Organizar episódios em pastas de temporadas", "WithFiles": "Com Arquivos", - "DeleteEpisodesFiles": "Excluir arquivos de episódios {episodeFileCount}", + "DeleteEpisodesFiles": "Excluir {episodeFileCount} arquivos de episódios", "NoMonitoredEpisodes": "Nenhum episódio monitorado nesta série", "ShowBanners": "Mostrar Banners", "DeleteSeriesFolderCountWithFilesConfirmation": "Tem certeza de que deseja excluir {count} séries selecionadas e todos os conteúdos?", @@ -1442,31 +1438,31 @@ "DeleteSeriesFoldersHelpText": "Excluir as pastas da série e todo o seu conteúdo", "Total": "Total", "DetailedProgressBar": "Barra de progresso detalhada", - "EndedSeriesDescription": "Não são esperados episódios ou temporadas adicionais", + "EndedSeriesDescription": "Não se espera mais episódios ou temporadas", "EpisodeFilesLoadError": "Não foi possível carregar os arquivos do episódio", "AddedDate": "Adicionado: {date}", "AllSeriesAreHiddenByTheAppliedFilter": "Todos os resultados são ocultados pelo filtro aplicado", - "AlternateTitles": "Títulos Alternativos", - "AuthenticationMethod": "Método de Autenticação", + "AlternateTitles": "Títulos alternativos", + "AuthenticationMethod": "Método de autenticação", "AuthenticationMethodHelpTextWarning": "Selecione um método de autenticação válido", - "AuthenticationRequiredPasswordHelpTextWarning": "Insira uma nova senha", + "AuthenticationRequiredPasswordHelpTextWarning": "Digite uma nova senha", "AuthenticationRequiredUsernameHelpTextWarning": "Digite um novo nome de usuário", - "CollapseAll": "Agrupar Todos", - "ContinuingSeriesDescription": "Mais episódios/outra temporada é esperada", + "CollapseAll": "Recolher tudo", + "ContinuingSeriesDescription": "Espera-se mais episódio ou outra temporada", "CountSeriesSelected": "{count} séries selecionadas", "MissingLoadError": "Erro ao carregar itens ausentes", "MissingNoItems": "Nenhum item ausente", "SearchAll": "Pesquisar Todos", "UnmonitorSelected": "Não Monitorar Selecionado", - "CutoffUnmetNoItems": "Nenhum item de corte não atendido", - "MonitorSelected": "Monitorar Selecionado", - "SearchForAllMissingConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", + "CutoffUnmetNoItems": "Nenhum item com limite não atendido", + "MonitorSelected": "Monitorar selecionados", + "SearchForAllMissingEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios ausentes de {totalRecords}?", "SearchSelected": "Pesquisar Selecionado", - "CutoffUnmetLoadError": "Erro ao carregar itens de corte não atendidos", - "MassSearchCancelWarning": "Isso não pode ser cancelado depois de iniciado sem reiniciar o Sonarr ou desabilitar todos os seus indexadores.", - "SearchForAllMissing": "Pesquisar por todos os episódios ausentes", - "SearchForCutoffUnmet": "Pesquise todos os episódios que o corte não foi atingido", - "SearchForCutoffUnmetConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios de {totalRecords} corte não atingido?", + "CutoffUnmetLoadError": "Erro ao carregar itens de limite não atendido", + "MassSearchCancelWarning": "Após começar, não é possível cancelar sem reiniciar o {appName} ou desabilitar todos os seus indexadores.", + "SearchForAllMissingEpisodes": "Pesquisar por todos os episódios ausentes", + "SearchForCutoffUnmetEpisodes": "Pesquise todos os episódios que o corte não foi atingido", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Tem certeza de que deseja pesquisar todos os episódios de {totalRecords} corte não atingido?", "FormatAgeDay": "dia", "FormatAgeHours": "horas", "FormatDateTime": "{formattedDate} {formattedTime}", @@ -1478,10 +1474,231 @@ "FormatAgeMinutes": "minutos", "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "FormatRuntimeHours": "{hours}h", - "FormatRuntimeMinutes": "{minutes}m", + "FormatRuntimeMinutes": "{minutes} m", "FormatShortTimeSpanHours": "{hours} hora(s)", "FormatShortTimeSpanMinutes": "{minutes} minuto(s)", "FormatShortTimeSpanSeconds": "{seconds} segundo(s)", "FormatTimeSpanDays": "{days}d {time}", - "Yesterday": "Ontem" + "Yesterday": "Ontem", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "O cliente de download {downloadClientName} está configurado para remover downloads concluídos. Isso pode resultar na remoção dos downloads do seu cliente antes que {appName} possa importá-los.", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Procure e tente baixar automaticamente uma versão diferente quando a versão com falha for obtida pela pesquisa interativa", + "AutoRedownloadFailed": "Falha no Novo Download", + "AutoRedownloadFailedFromInteractiveSearch": "Falha no Novo Download pela Pesquisa Interativa", + "ImportListSearchForMissingEpisodes": "Pesquisar Episódios Ausentes", + "ImportListSearchForMissingEpisodesHelpText": "Depois que a série for adicionada ao {appName}, procure automaticamente episódios ausentes", + "QueueFilterHasNoItems": "O filtro de fila selecionado não possui itens", + "BlackholeFolderHelpText": "Pasta na qual {appName} armazenará o arquivo {extension}", + "Destination": "Destinação", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL json do deluge, consulte {url}", + "DownloadClientDelugeValidationLabelPluginFailure": "Falha na configuração do rótulo", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} não conseguiu adicionar o rótulo ao {clientName}.", + "DownloadClientDownloadStationProviderMessage": "{appName} não consegue se conectar ao Download Station se a autenticação de dois fatores estiver habilitada em sua conta DSM", + "DownloadClientDownloadStationValidationFolderMissingDetail": "A pasta '{downloadDir}' não existe, ela deve ser criada manualmente dentro da Pasta Compartilhada '{sharedFolder}'.", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "O Diskstation não possui uma pasta compartilhada com o nome '{sharedFolder}', tem certeza de que a especificou corretamente?", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID do aplicativo fornecido ao criar acesso à API Freebox (ou seja, 'app_id')", + "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é '{port}'", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Não foi possível acessar a API Freebox. Verifique a configuração de 'URL da API' para URL base e versão.", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração NzbGet KeepHistory deve ser menor que 25.000", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "A configuração KeepHistory do NzbGet está definida como 0. O que impede que {appName} veja os downloads concluídos.", + "DownloadClientSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {clientName}, como {url}", + "DownloadClientSettingsUseSslHelpText": "Use conexão segura ao conectar-se a {clientName}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL rpc {clientName}, por exemplo, {url}, o padrão é '{defaultUrl}'", + "DownloadClientValidationAuthenticationFailureDetail": "Por favor, verifique seu nome de usuário e senha. Verifique também se o host que executa {appName} não está impedido de acessar {clientName} pelas limitações da WhiteList na configuração de {clientName}.", + "DownloadClientValidationSslConnectFailureDetail": "{appName} não consegue se conectar a {clientName} usando SSL. Este problema pode estar relacionado ao computador. Tente configurar {appName} e {clientName} para não usar SSL.", + "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status de descompactação: {unpackStatus} - Status de movimentação: {moveStatus} - Status do script: {scriptStatus} - Status de exclusão: {deleteStatus} - Status de marcação: {markStatus}", + "PostImportCategory": "Categoria Pós-Importação", + "SecretToken": "Token Secreto", + "TorrentBlackhole": "Torrent Blackhole", + "TorrentBlackholeSaveMagnetFiles": "Salvar arquivos magnéticos", + "TorrentBlackholeSaveMagnetFilesHelpText": "Salve o link magnético se nenhum arquivo .torrent estiver disponível (útil apenas se o cliente de download suportar magnets salvos em um arquivo)", + "UnknownDownloadState": "Estado de download desconhecido: {state}", + "UsenetBlackhole": "Usenet Blackhole", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para torrents adicionados ao qBittorrent. Observe que os Torrents Forçados não obedecem às restrições de sementes", + "DownloadClientQbittorrentTorrentStatePathError": "Não foi possível importar. O caminho corresponde ao diretório de download da base do cliente, é possível que 'Manter pasta de nível superior' esteja desativado para este torrent ou 'Layout de conteúdo do torrent' NÃO esteja definido como 'Original' ou 'Criar subpasta'?", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "As categorias não são suportadas até a versão 3.3.0 do qBittorrent. Atualize ou tente novamente com uma categoria vazia.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para remover torrents quando eles atingem seu limite de proporção de compartilhamento", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} não poderá realizar o tratamento de download concluído conforme configurado. Você pode corrigir isso no qBittorrent ('Ferramentas - Opções...' no menu) alterando 'Opções - BitTorrent - Limitação da proporção de compartilhamento' de 'Removê-los' para 'Pausá-los'", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Caminho para o endpoint XMLRPC, consulte {url}. Geralmente é RPC2 ou [caminho para ruTorrent]{url2} ao usar o ruTorrent.", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes do download' afeta a capacidade do {appName} de rastrear novos downloads. Além disso, o Sabnzbd recomenda 'Abortar trabalhos que não podem ser concluídos', pois é mais eficaz.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Você deve desativar a ordenação de filmes para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefere que cada download tenha uma pasta separada. Com * anexado à pasta/caminho, o Sabnzbd não criará essas pastas de trabalho. Vá para Sabnzbd para consertar.", + "DownloadClientSettingsCategorySubFolderHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado. Cria um subdiretório [categoria] no diretório de saída.", + "XmlRpcPath": "Caminho RPC XML", + "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as tags iniciais. Isso evita conflitos com downloads não relacionados.", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como tags. As dicas são exemplos.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta tags após a importação de um download.", + "BlackholeWatchFolder": "Pasta Monitorada", + "BlackholeWatchFolderHelpText": "Pasta da qual {appName} deve importar downloads concluídos", + "Category": "Categoria", + "Directory": "Diretório", + "DownloadClientDelugeTorrentStateError": "Deluge está relatando um erro", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Você deve ter o plugin Rotulo habilitado em {clientName} para usar categorias.", + "DownloadClientDownloadStationSettingsDirectory": "Pasta compartilhada opcional para colocar downloads, deixe em branco para usar o local padrão do Download Station", + "DownloadClientDownloadStationValidationApiVersion": "A versão da API do Download Station não é suportada; deve ser pelo menos {requiredVersion}. Suporta de {minVersion} a {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissing": "A pasta não existe", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Nenhum destino padrão", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Você deve fazer login em seu Diskstation como {username} e configurá-lo manualmente nas configurações do DownloadStation em BT/HTTP/FTP/NZB - Localização.", + "DownloadClientDownloadStationValidationSharedFolderMissing": "A pasta compartilhada não existe", + "DownloadClientFloodSettingsAdditionalTags": "Etiquetas Adicionais", + "DownloadClientFloodSettingsPostImportTags": "Etiquetas Pós-Importação", + "DownloadClientFloodSettingsRemovalInfo": "{appName} cuidará da remoção automática de torrents com base nos critérios de propagação atuais em Configurações - Indexadores", + "DownloadClientFloodSettingsStartOnAdd": "Comece em Adicionar", + "DownloadClientFloodSettingsUrlBaseHelpText": "Adiciona um prefixo à API Flood, como {url}", + "DownloadClientFreeboxApiError": "A API Freebox retornou um erro: {errorDescription}", + "DownloadClientFreeboxAuthenticationError": "A autenticação na API Freebox falhou. Motivo: {errorDescription}", + "DownloadClientFreeboxNotLoggedIn": "Não logado", + "DownloadClientFreeboxSettingsApiUrl": "URL da API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Defina o URL base da API Freebox com a versão da API, por exemplo, '{url}', o padrão é '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "ID do App", + "DownloadClientFreeboxSettingsAppToken": "Token do App", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token do aplicativo recuperado ao criar acesso à API Freebox (ou seja, 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Nome do host ou endereço IP do host do Freebox, o padrão é '{url}' (só funcionará se estiver na mesma rede)", + "DownloadClientFreeboxUnableToReachFreebox": "Não foi possível acessar a API Freebox. Verifique as configurações de 'Host', 'Porta' ou 'Usar SSL'. (Erro: {exceptionMessage})", + "DownloadClientNzbVortexMultipleFilesMessage": "O download contém vários arquivos e não está em uma pasta de trabalho: {outputPath}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opção requer pelo menos NzbGet versão 16.0", + "DownloadClientDelugeValidationLabelPluginInactive": "Plugin de tag não está ativado", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "A configuração KeepHistory do NzbGet está muito alta.", + "DownloadClientNzbgetValidationKeepHistoryZero": "A configuração KeepHistory do NzbGet deve ser maior que 0", + "DownloadClientPneumaticSettingsNzbFolder": "Pasta Nzb", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta pasta precisará estar acessível no XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Pasta Strm", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Os arquivos .strm nesta pasta serão importados pelo drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeiro e último primeiro", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Baixe a primeira e a última peças primeiro (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsSequentialOrder": "Ordem sequencial", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Baixe em ordem sequencial (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Use uma conexão segura. Consulte Opções - UI da Web - 'Usar HTTPS em vez de HTTP' no qBittorrent.", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent não pode resolver o link magnético com DHT desativado", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent está relatando um erro", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está baixando metadados", + "DownloadClientQbittorrentTorrentStateStalled": "O download está parado sem conexões", + "DownloadClientQbittorrentTorrentStateUnknown": "Estado de download desconhecido: {state}", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Falha na configuração da categoria", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} não conseguiu adicionar o rótulo ao qBittorrent.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Categoria é recomendada", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} não tentará importar downloads concluídos sem uma categoria.", + "DownloadClientQbittorrentValidationCategoryUnsupported": "A categoria não é suportada", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Fila não habilitada", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "O Filas de Torrent não está habilitado nas configurações do qBittorrent. Habilite-o no qBittorrent ou selecione ‘Último’ como prioridade.", + "DownloadClientRTorrentProviderMessage": "O rTorrent não pausará os torrents quando eles atenderem aos critérios de seed. {appName} lidará com a remoção automática de torrents com base nos critérios de propagação atuais em Configurações - Indexadores somente quando a remoção concluída estiver ativada.", + "DownloadClientRTorrentSettingsAddStopped": "Adicionar parado", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "A ativação adicionará torrents e magnets ao rTorrent em um estado parado. Isso pode quebrar os arquivos magnéticos.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do rTorrent", + "DownloadClientRTorrentSettingsUrlPath": "Caminho da URL", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Desative a opção ‘Verificar antes do download’ no Sabnbzd", + "DownloadClientSabnzbdValidationDevelopVersion": "Versão de desenvolvimento do Sabnzbd, assumindo a versão 3.0.0 ou superior.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} pode não ser compatível com novos recursos adicionados ao SABnzbd ao executar versões de desenvolvimento.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Desabilitar ordenação por data", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Você deve desativar a ordenação por data para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Desabilitar Ordenação de Filmes", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desabilitar Ordenação para TV", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Você deve desativar a ordenação de TV para a categoria usada por {appName} para evitar problemas de importação. Vá para Sabnzbd para consertar.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar pastas de trabalho", + "DownloadClientSabnzbdValidationUnknownVersion": "Versão desconhecida: {rawVersion}", + "DownloadClientSettingsAddPaused": "Adicionar Pausado", + "DownloadClientSettingsCategoryHelpText": "Adicionar uma categoria específica para {appName} evita conflitos com downloads não relacionados que não sejam de {appName}. Usar uma categoria é opcional, mas altamente recomendado.", + "DownloadClientSettingsDestinationHelpText": "Especifica manualmente o destino do download, deixe em branco para usar o padrão", + "DownloadClientSettingsInitialState": "Estado Inicial", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial dos torrents adicionados ao {clientName}", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar há mais de 14 dias", + "DownloadClientSettingsPostImportCategoryHelpText": "Categoria para {appName} definir após importar o download. {appName} não removerá torrents nessa categoria mesmo que a propagação seja concluída. Deixe em branco para manter a mesma categoria.", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridade de uso ao baixar episódios que foram ao ar nos últimos 14 dias", + "DownloadClientSettingsOlderPriority": "Prioridade para os mais antigos", + "DownloadClientSettingsRecentPriority": "Prioridade para os mais recentes", + "DownloadClientUTorrentTorrentStateError": "uTorrent está relatando um erro", + "DownloadClientValidationApiKeyIncorrect": "Chave de API incorreta", + "DownloadClientValidationApiKeyRequired": "Chave de API necessária", + "DownloadClientValidationAuthenticationFailure": "Falha de autenticação", + "DownloadClientValidationCategoryMissing": "A categoria não existe", + "DownloadClientValidationCategoryMissingDetail": "A categoria inserida não existe em {clientName}. Crie-a primeiro em {clientName}.", + "DownloadClientValidationErrorVersion": "A versão de {clientName} deve ser pelo menos {requiredVersion}. A versão informada é {reportedVersion}", + "DownloadClientValidationGroupMissing": "O grupo não existe", + "DownloadClientValidationGroupMissingDetail": "O grupo inserido não existe em {clientName}. Crie-a primeiro em {clientName}.", + "DownloadClientValidationSslConnectFailure": "Não é possível conectar através de SSL", + "DownloadClientValidationTestNzbs": "Falha ao obter a lista de NZBs: {exceptionMessage}", + "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: {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.", + "DownloadStationStatusExtracting": "Extraindo: {progress}%", + "TorrentBlackholeSaveMagnetFilesExtension": "Salvar extensão de arquivos magnéticos", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensão a ser usada para links magnéticos, o padrão é '.magnet'", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Somente para Leitura", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Em vez de mover arquivos, isso instruirá {appName} a copiar ou vincular (dependendo das configurações/configuração do sistema)", + "TorrentBlackholeTorrentFolder": "Pasta do Torrent", + "UseSsl": "Usar SSL", + "UsenetBlackholeNzbFolder": "Pasta Nzb", + "IndexerIPTorrentsSettingsFeedUrl": "URL do Feed", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "A URL completa do feed RSS gerado pelo IPTorrents, usando apenas as categorias que você selecionou (HD, SD, x264, etc...)", + "IndexerSettingsAdditionalParameters": "Parâmetros Adicionais", + "IndexerSettingsAdditionalParametersNyaa": "Parâmetros Adicionais", + "IndexerSettingsAllowZeroSize": "Permitir tamanho zero", + "IndexerSettingsAnimeCategories": "Categorias de anime", + "IndexerSettingsAnimeStandardFormatSearch": "Formato de Pesquisa Padrão para Anime", + "IndexerSettingsApiPath": "Caminho da API", + "IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}", + "IndexerSettingsApiUrl": "URL da API", + "IndexerSettingsApiUrlHelpText": "Não mude isso a menos que você saiba o que está fazendo. Já que sua chave API será enviada para esse host.", + "IndexerSettingsCategories": "Categorias", + "IndexerSettingsCategoriesHelpText": "Lista suspensa, deixe em branco para desativar programas padrão/diários", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsMinimumSeeders": "Mínimo de Semeadores", + "IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semeadores necessário.", + "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsRssUrl": "URL do RSS", + "IndexerSettingsSeasonPackSeedTime": "Tempo de Seed de Pack de Temporada", + "IndexerSettingsSeedRatio": "Taxa de Semeação", + "IndexerSettingsSeedTime": "Tempo de Semeação", + "IndexerSettingsSeedTimeHelpText": "O tempo que um torrent deve ser propagado antes de parar, vazio usa o padrão do cliente de download", + "IndexerSettingsWebsiteUrl": "URL do Website", + "IndexerValidationCloudFlareCaptchaExpired": "O token CloudFlare CAPTCHA expirou, atualize-o.", + "IndexerValidationFeedNotSupported": "O feed do indexador não é compatível: {exceptionMessage}", + "IndexerValidationJackettAllNotSupportedHelpText": "Todos os endpoints de Jackett não são suportados. Adicione indexadores individualmente", + "IndexerValidationJackettNoResultsInConfiguredCategories": "Consulta bem-sucedida, mas nenhum resultado nas categorias configuradas foi retornado do seu indexador. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", + "IndexerValidationJackettNoRssFeedQueryAvailable": "Nenhuma consulta de feed RSS disponível. Isso pode ser um problema com o indexador ou com as configurações de categoria do indexador.", + "IndexerValidationRequestLimitReached": "Limite de solicitações atingido: {exceptionMessage}", + "IndexerValidationSearchParametersNotSupported": "O indexador não oferece suporte aos parâmetros de pesquisa obrigatórios", + "IndexerValidationUnableToConnectResolutionFailure": "Não é possível conectar-se à falha de conexão do indexador. Verifique sua conexão com o servidor e DNS do indexador. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Não foi possível conectar-se ao indexador, o servidor do indexador não está disponível. Tente mais tarde. {exceptionMessage}.", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Observe que se você alterar a categoria, terá que adicionar regras obrigatórias/restritas sobre os subgrupos para evitar lançamentos em idiomas estrangeiros.", + "IndexerSettingsAllowZeroSizeHelpText": "Ativar isso permitirá que você use feeds que não especificam o tamanho do lançamento, mas tenha cuidado, pois verificações relacionadas ao tamanho não serão realizadas.", + "IndexerSettingsAnimeCategoriesHelpText": "Lista suspensa, deixe em branco para desativar o anime", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Pesquise também por animes usando a numeração padrão", + "IndexerSettingsCookieHelpText": "se o seu site exigir um cookie de login para acessar o RSS, você terá que recuperá-lo por meio de um navegador.", + "IndexerSettingsRssUrlHelpText": "Insira a URL de um feed RSS compatível com {indexer}", + "IndexerSettingsSeasonPackSeedTimeHelpText": "O tempo que um torrent de pack de temporada deve ser semeado antes de parar, vazio usa o padrão do cliente de download", + "IndexerSettingsSeedRatioHelpText": "A proporção que um torrent deve atingir antes de parar, vazio usa o padrão do cliente de download. A proporção deve ser de pelo menos 1,0 e seguir as regras dos indexadores", + "IndexerValidationCloudFlareCaptchaRequired": "Site protegido por CloudFlare CAPTCHA. É necessário um token CAPTCHA válido.", + "IndexerValidationInvalidApiKey": "Chave de API inválida", + "IndexerValidationJackettAllNotSupported": "Todos os endpoints de Jackett não são suportados. Adicione indexadores individualmente", + "IndexerValidationQuerySeasonEpisodesNotSupported": "O indexador não oferece suporte à consulta atual. Verifique se as categorias e/ou busca por temporadas/episódios são suportadas. Verifique o registro para mais detalhes.", + "IndexerValidationTestAbortedDueToError": "O teste foi abortado devido a um erro: {exceptionMessage}", + "IndexerValidationUnableToConnect": "Não foi possível conectar-se ao indexador: {exceptionMessage}. Verifique o registro em torno deste erro para obter detalhes", + "IndexerValidationUnableToConnectHttpError": "Não foi possível conectar-se ao indexador. Verifique suas configurações de DNS e certifique-se de que o IPv6 esteja funcionando ou desativado. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "Não foi possível conectar-se ao indexador, credenciais inválidas. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Não foi possível conectar-se ao indexador, possivelmente devido ao tempo limite. Tente novamente ou verifique as configurações de rede. {exceptionMessage}.", + "IndexerHDBitsSettingsCategories": "Categorias", + "IndexerHDBitsSettingsCategoriesHelpText": "se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsCodecs": "Codecs", + "IndexerHDBitsSettingsCodecsHelpText": "Se não for especificado, todas as opções serão usadas.", + "IndexerHDBitsSettingsMediums": "Meio", + "IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.", + "ClearBlocklist": "Limpar lista de bloqueio", + "MonitorRecentEpisodesDescription": "Monitore episódios exibidos nos últimos 90 dias e episódios futuros", + "ClearBlocklistMessageText": "Tem certeza de que deseja limpar todos os itens da lista de bloqueio?", + "PasswordConfirmation": "Confirmação Da Senha", + "MonitorPilotEpisodeDescription": "Monitore apenas o primeiro episódio da primeira temporada", + "MonitorNoNewSeasonsDescription": "Não monitore nenhuma nova temporada automaticamente", + "MonitorAllSeasons": "Todas as Temporadas", + "MonitorAllSeasonsDescription": "Monitorar todas as novas temporadas automaticamente", + "MonitorLastSeason": "Última Temporada", + "MonitorLastSeasonDescription": "Monitorar todos os episódios da última temporada", + "MonitorNewSeasons": "Monitorar Novas Temporadas", + "MonitorNewSeasonsHelpText": "Quais novas temporadas devem ser monitoradas automaticamente", + "MonitorRecentEpisodes": "Episódios Recentes", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirme a nova senha" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 2d9e25fd2..68252092b 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -20,13 +20,13 @@ "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 {0}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}.", "CloneCustomFormat": "Clonați format personalizat", "Close": "Închide", "Delete": "Șterge", "Added": "Adăugat", "CountSeasons": "{count} sezoane", - "DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare indisponibili datorită erorilor: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare indisponibili datorită erorilor: {downloadClientNames}", "EnableAutomaticSearch": "Activați căutarea automată", "EnableInteractiveSearch": "Activați căutarea interactivă", "Enabled": "Activat", @@ -69,7 +69,7 @@ "DotNetVersion": ".NET", "Download": "Descarca", "DownloadClient": "Client de descărcare", - "DownloadClientRootFolderHealthCheckMessage": "Clientul de descărcare {0} plasează descărcările în folderul rădăcină {1}. Nu trebuie să descărcați într-un folder rădăcină.", + "DownloadClientRootFolderHealthCheckMessage": "Clientul de descărcare {downloadClientName} plasează descărcările în folderul rădăcină {rootFolderPath}. Nu trebuie să descărcați într-un folder rădăcină.", "Episode": "Episod", "EpisodeTitle": "Titlu episod", "Episodes": "Episoade", @@ -125,12 +125,12 @@ "AddQualityProfileError": "Imposibil de adăugat un nou profil de calitate, încercați din nou.", "AnalyseVideoFiles": "Analizați fișierele video", "Analytics": "Statistici", - "AnalyticsEnabledHelpText": "Trimiteți informații anonime privind utilizarea și erorile către serverele Sonarr. Aceasta include informații despre browserul dvs., ce pagini WebUI Sonarr utilizați, raportarea erorilor, precum și sistemul de operare și versiunea de execuție. Vom folosi aceste informații pentru a acorda prioritate caracteristicilor și remedierilor de erori.", + "AnalyticsEnabledHelpText": "Trimiteți informații anonime privind utilizarea și erorile către serverele {appName}. Aceasta include informații despre browserul dvs., ce pagini WebUI {appName} utilizați, raportarea erorilor, precum și sistemul de operare și versiunea de execuție. Vom folosi aceste informații pentru a acorda prioritate caracteristicilor și remedierilor de erori.", "ApiKey": "Cheie API", "ApplicationURL": "URL aplicație", "AuthBasic": "Basic (fereastră pop-up browser)", "AuthForm": "Formulare (Pagina de autentificare)", - "AuthenticationMethodHelpText": "Solicitați nume utilizator și parola pentru a accesa Sonarr", + "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.", @@ -151,7 +151,7 @@ "InteractiveImportNoImportMode": "Un mod de import trebuie selectat", "InteractiveImportNoFilesFound": "Nu au fost găsite fișiere video în folderul selectat", "InteractiveImportLoadError": "Imposibil de încărcat articole de import manual", - "NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {notificationNames}", "ParseModalUnableToParse": "Nu se poate analiza titlul furnizat, vă rugăm să încercați din nou.", "ParseModalErrorParsing": "Eroare la analizare, încercați din nou.", "Parse": "Analiza", @@ -159,5 +159,45 @@ "SelectFolderModalTitle": "{modalTitle} - Selectați folder", "SelectDownloadClientModalTitle": "{modalTitle} - Selectați clientul de descărcare", "SelectDropdown": "Selectați...", - "True": "Adevărat" + "True": "Adevărat", + "FormatAgeMinutes": "minute", + "TablePageSize": "Mărimea Paginii", + "QueueLoadError": "Nu s-a putut încărca coada de așteptare", + "DownloadIgnored": "Descărcarea ignorată", + "BlocklistLoadError": "Imposibil de încărcat lista neagră", + "FullColorEvents": "Evenimente pline de culoare", + "InfoUrl": "URL informații", + "AddConnection": "Adăugați conexiune", + "DeletedReasonUpgrade": "Fișierul a fost șters pentru a importa un upgrade", + "CustomFormatJson": "Format JSON personalizat", + "HistoryLoadError": "Istoricul nu poate fi încărcat", + "DefaultNameCopiedSpecification": "{name} - Copie", + "DefaultNameCopiedProfile": "{name} - Copie", + "DeletedReasonManual": "Fișierul a fost șters prin interfața de utilizare", + "NoHistoryFound": "Nu s-a găsit istoric", + "Or": "sau", + "PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil", + "ReleaseProfilesLoadError": "Nu se pot încărca profilurile", + "UnknownEventTooltip": "Eveniment necunoscut", + "Unknown": "Necunoscut", + "AddConnectionImplementation": "Adăugați conexiune - {implementationName}", + "AddDownloadClientImplementation": "Adăugați client de descărcare - {implementationName}", + "AddIndexerImplementation": "Adăugați Indexator - {implementationName}", + "Umask": "Umask", + "AuthenticationRequiredPasswordHelpTextWarning": "Introduceți o parolă nouă", + "FormatAgeHours": "ore", + "FormatAgeHour": "oră", + "FormatAgeDays": "zile", + "FormatAgeDay": "zi", + "TablePageSizeHelpText": "Numărul de articole de afișat pe fiecare pagină", + "RemoveSelectedBlocklistMessageText": "Sigur doriți să eliminați elementele selectate din lista neagră?", + "AuthenticationRequiredUsernameHelpTextWarning": "Introduceți un nou nume de utilizator", + "FirstDayOfWeek": "Prima zi a săptămânii", + "ShortDateFormat": "Format scurt de dată", + "ShowRelativeDates": "Afișați datele relative", + "LongDateFormat": "Format de dată lungă", + "AppUpdated": "{appName} actualizat", + "ShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute", + "WeekColumnHeader": "Antetul coloanei săptămânii", + "TimeFormat": "Format ora" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index b071eb0b4..f37e97d20 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1,16 +1,16 @@ { - "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {0} символов. Вы можете сделать это через настройки или файл конфигурации", - "DownloadClientSortingHealthCheckMessage": "В клиенте загрузки {0} включена сортировка {1} для категории Sonarr. Вам следует отключить сортировку в вашем клиенте загрузки, чтобы избежать проблем с импортом.", - "IndexerJackettAllHealthCheckMessage": "Используется не поддерживаемый в Jackett конечный параметр 'all' в индексаторе: {0}", - "IndexerSearchNoAutomaticHealthCheckMessage": "Нет доступных индексаторов с включенным автоматическим поиском, Sonarr не будет предоставлять результаты автоматического поиска", + "ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {length} символов. Вы можете сделать это через настройки или файл конфигурации", + "DownloadClientSortingHealthCheckMessage": "В клиенте загрузки {downloadClientName} включена сортировка {sortingMode} для категории {appName}. Вам следует отключить сортировку в вашем клиенте загрузки, чтобы избежать проблем с импортом.", + "IndexerJackettAllHealthCheckMessage": "Используется не поддерживаемый в Jackett конечный параметр 'all' в индексаторе: {indexerNames}", + "IndexerSearchNoAutomaticHealthCheckMessage": "Нет доступных индексаторов с включенным автоматическим поиском, {appName} не будет предоставлять результаты автоматического поиска", "Added": "Добавлено", "AppDataLocationHealthCheckMessage": "Обновление будет не возможно, во избежание удаления данных программы во время обновления", "ApplyChanges": "Применить изменения", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Ни один загрузчик не доступен", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Невозможно связаться с {0}.", - "DownloadClientRootFolderHealthCheckMessage": "Клиент загрузки {0} помещает загрузки в корневую папку {1}. Вы не должны загружать в корневую папку.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Невозможно связаться с {downloadClientName}. {errorMessage}", + "DownloadClientRootFolderHealthCheckMessage": "Клиент загрузки {downloadClientName} помещает загрузки в корневую папку {rootFolderPath}. Вы не должны загружать в корневую папку.", "DownloadClientStatusAllClientHealthCheckMessage": "Все клиенты загрузки недоступны из-за сбоев", - "DownloadClientStatusSingleClientHealthCheckMessage": "Клиенты для скачивания недоступны из-за ошибок: {0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "Клиенты для скачивания недоступны из-за ошибок: {downloadClientNames}", "EditSelectedDownloadClients": "Редактировать выбранные клиенты загрузки", "EditSelectedImportLists": "Редактировать выбранные списки импорта", "EditSeries": "Редактировать серию", @@ -19,28 +19,28 @@ "Enabled": "Включено", "HiddenClickToShow": "Скрыто, нажмите чтобы показать", "HideAdvanced": "Скрыть расширенные", - "ImportListRootFolderMissingRootHealthCheckMessage": "Отсутствует корневая папка для импортирования списка(ов): {0}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {0}", + "ImportListRootFolderMissingRootHealthCheckMessage": "Отсутствует корневая папка для импортирования списка(ов): {rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {rootFoldersInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Все листы недоступны из-за ошибок", - "ImportListStatusUnavailableHealthCheckMessage": "Листы недоступны из-за ошибок: {0}", + "ImportListStatusUnavailableHealthCheckMessage": "Листы недоступны из-за ошибок: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Включить обработку завершенной загрузки, если это возможно", "ImportMechanismHandlingDisabledHealthCheckMessage": "Включить обработку завершенных скачиваний", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {0}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Все RSS индексаторы временно выключены из-за ошибок", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "Нет доступных индексаторов с включенной синхронизацией RSS, Sonarr не будет автоматически получать новые выпуски", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Нет доступных индексаторов с включенной синхронизацией RSS, {appName} не будет автоматически получать новые выпуски", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Все индексаторы с возможностью поиска временно выключены из-за ошибок", "DeleteSelectedImportListsMessageText": "Вы уверены, что хотите удалить {count} выбранных списков импорта?", "DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить {count} выбранных индексатора?", "EditConditionImplementation": "Редактировать условие - {implementationName}", "EditImportListImplementation": "Редактировать импорт лист - {implementationName}", "Implementation": "Реализация", - "IndexerDownloadClientHealthCheckMessage": "Индексаторы с недопустимыми клиентами загрузки: {0}.", + "IndexerDownloadClientHealthCheckMessage": "Индексаторы с недопустимыми клиентами загрузки: {indexerNames}.", "ManageClients": "Управление клиентами", "ManageIndexers": "Управление индексаторами", "MoveAutomatically": "Перемещать автоматически", "NoDownloadClientsFound": "Клиенты для загрузки не найдены", - "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {0}", + "NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {notificationNames}", "CountIndexersSelected": "{count} выбранных индексаторов", "EditAutoTag": "Редактировать автоматическую маркировку", "NoHistoryBlocklist": "Нет истории блокировок", @@ -146,5 +146,55 @@ "LanguagesLoadError": "Невозможно загрузить языки", "RemoveQueueItem": "Удалить - {sourceTitle}", "ResetQualityDefinitionsMessageText": "Вы уверены, что хотите сбросить определения качества?", - "RemoveSelectedItemsQueueMessageText": "Вы действительно хотите удалить {selectedCount} элементов из очереди?" + "RemoveSelectedItemsQueueMessageText": "Вы действительно хотите удалить {selectedCount} элементов из очереди?", + "AuthenticationMethod": "Способ авторизации", + "AuthenticationRequiredPasswordHelpTextWarning": "Введите новый пароль", + "AuthenticationRequiredUsernameHelpTextWarning": "Введите новое имя пользователя", + "Activity": "Активность", + "Add": "Добавить", + "Actions": "Действия", + "About": "Об", + "AddConnection": "Добавить подключение", + "AddConditionError": "Невозможно добавить новое условие, попробуйте еще раз.", + "AddCustomFormat": "Добавить свой формат", + "AddCondition": "Добавить условие", + "AddCustomFormatError": "Невозможно добавить новый пользовательский формат, попробуйте ещё раз.", + "AddDelayProfile": "Добавить профиль задержки", + "AddDownloadClient": "Добавить программу для скачивания", + "AddDownloadClientError": "Не удалось добавить новый клиент загрузки, попробуйте еще раз.", + "AddConnectionImplementation": "Добавить подключение - {implementationName}", + "AddCustomFilter": "Добавить специальный фильтр", + "Absolute": "Абсолютный", + "AddConditionImplementation": "Добавить условие - {implementationName}", + "AddNew": "Добавить", + "AddRootFolder": "Добавить корневой каталог", + "AgeWhenGrabbed": "Возраст (когда захвачен)", + "Age": "Возраст", + "All": "Все", + "AddList": "Добавить список", + "AddListError": "Не удалось добавить новый список, попробуйте еще раз.", + "AddIndexerError": "Не удалось добавить новый индексатор, повторите попытку.", + "AddImportList": "Добавить список импорта", + "AddQualityProfile": "Добавить профиль качества", + "AddQualityProfileError": "Не удалось добавить новый профиль качества. Повторите попытку.", + "AfterManualRefresh": "После обновления вручную", + "AddExclusion": "Добавить исключение", + "AddIndexer": "Добавить индексатор", + "AddImportListExclusion": "Добавить исключение из списка импорта", + "AddNewRestriction": "Добавить новое ограничение", + "AddNotificationError": "Невозможно добавить новое уведомление, попробуйте еще раз.", + "AllResultsAreHiddenByTheAppliedFilter": "Все результаты скрыты фильтром", + "ParseModalHelpTextDetails": "{appName} попытается определить название и показать подробную информацию о нем", + "AddDownloadClientImplementation": "Добавить клиент загрузки - {implementationName}", + "AddImportListImplementation": "Добавить список импорта - {implementationName}", + "AddIndexerImplementation": "Добавить индексатор - {implementationName}", + "AddNewSeriesError": "Не удалось загрузить результат поиска, попробуйте еще раз.", + "AddListExclusionSeriesHelpText": "Запретить добавление серий в {appName} по спискам", + "AddToDownloadQueue": "Добавить в очередь загрузки", + "AddedDate": "Добавлено: {date}", + "AddedToDownloadQueue": "Добавлено в очередь на скачивание", + "AllFiles": "Все файлы", + "AllSeriesAreHiddenByTheAppliedFilter": "Все результаты скрыты фильтром", + "AlreadyInYourLibrary": "Уже в вашей библиотеке", + "Always": "Всегда" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 6db0abcbe..73d3828cd 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -1,4 +1,8 @@ { "About": "Hakkında", - "Absolute": "Mutlak" + "Absolute": "Mutlak", + "AddCondition": "Koşul Ekle", + "AddAutoTag": "Otomatik Etiket Ekle", + "AddConnection": "Bağlantı Ekle", + "AddConditionImplementation": "Koşul Ekle - {uygulama Adı}" } diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 0d4b4e5c9..493d34b23 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -2,7 +2,7 @@ "BlocklistRelease": "Chặn bản phát hành", "BlocklistReleases": "Phát hành danh sách đen", "Added": "Đã thêm", - "ApiKeyValidationHealthCheckMessage": "Hãy cập nhật mã API để dài ít nhất {0} kí tự. Bạn có thể làm điều này trong cài đặt hoặc trong tập config", + "ApiKeyValidationHealthCheckMessage": "Hãy cập nhật mã API để dài ít nhất {length} kí tự. Bạn có thể làm điều này trong cài đặt hoặc trong tập config", "AppDataLocationHealthCheckMessage": "Việc cập nhật sẽ không xảy ra để tránh xóa AppData khi cập nhật", "ApplyChanges": "Áp dụng thay đổi", "AutomaticAdd": "Tự động thêm" diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index b9095865b..676afd879 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -2,10 +2,10 @@ "CloneCondition": "克隆条件", "DeleteCondition": "删除条件", "DeleteConditionMessageText": "你确定要删除条件 “{name}” 吗?", - "DeleteCustomFormatMessageText": "是否确实要删除条件“{0}”?", - "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{0}个字符长。您可以通过设置或配置文件执行此操作", + "DeleteCustomFormatMessageText": "是否确实要删除条件“{customFormatName}”?", + "ApiKeyValidationHealthCheckMessage": "请将API密钥更新为至少{length}个字符长。您可以通过设置或配置文件执行此操作", "RemoveSelectedItemQueueMessageText": "您确定要从队列中删除一个项目吗?", - "RemoveSelectedItemsQueueMessageText": "您确定要从队列中删除 {selectedCount} 个项目吗?", + "RemoveSelectedItemsQueueMessageText": "您确定要从队列中删除{selectedCount}项吗?", "ApplyChanges": "应用更改", "AutomaticAdd": "自动添加", "EditSelectedDownloadClients": "编辑选定的下载客户端", @@ -47,7 +47,7 @@ "ApplyTagsHelpTextHowToApplyIndexers": "如何将标签应用到已选择的索引器", "AppDataLocationHealthCheckMessage": "正在更新期间的 AppData 不会被更新删除", "BlocklistRelease": "黑名单版本", - "BlocklistReleaseHelpText": "再次启动对此集的搜索并阻止再次获取此版本", + "BlocklistReleaseSearchEpisodeAgainHelpText": "再次启动对此集的搜索并阻止再次获取此版本", "BlocklistReleases": "黑名单版本", "CloneCustomFormat": "复制自定义命名格式", "Close": "关闭", @@ -63,43 +63,43 @@ "Metadata": "元数据", "CountSeasons": "季{count}", "DownloadClientCheckNoneAvailableHealthCheckMessage": "无可用的下载客户端", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{0}进行通讯。", - "DownloadClientRootFolderHealthCheckMessage": "下载客户端{0}将下载内容放在根文件夹{1}中。您不应该下载到根文件夹。", - "DownloadClientSortingHealthCheckMessage": "下载客户端{0}已为Sonarr的类别启用{1}排序。您应该在下载客户端中禁用排序,以避免导入问题。", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{downloadClientName}进行通讯", + "DownloadClientRootFolderHealthCheckMessage": "下载客户端{downloadClientName}将下载内容放在根文件夹{rootFolderPath}中。您不应该下载到根文件夹。", + "DownloadClientSortingHealthCheckMessage": "下载客户端{downloadClientName}已为{appName}的类别启用{sortingMode}排序。您应该在下载客户端中禁用排序,以避免导入问题。", "DownloadClientStatusAllClientHealthCheckMessage": "所有下载客户端都不可用", - "DownloadClientStatusSingleClientHealthCheckMessage": "所有下载客户端都不可用:{0}", + "DownloadClientStatusSingleClientHealthCheckMessage": "所有下载客户端都不可用:{downloadClientNames}", "EditSeries": "编辑剧集", "Enabled": "已启用", "Ended": "已完结", - "ImportListRootFolderMissingRootHealthCheckMessage": "在导入列表中缺少根目录文件夹:{0}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{0}", + "ImportListRootFolderMissingRootHealthCheckMessage": "在导入列表中缺少根目录文件夹:{rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{rootFoldersInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "所有的列表因错误不可用", - "ImportListStatusUnavailableHealthCheckMessage": "列表因错误不可用:{0}", + "ImportListStatusUnavailableHealthCheckMessage": "列表因错误不可用:{importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "如果可能,启用完整的下载处理(不支持多台计算机)", "ImportMechanismHandlingDisabledHealthCheckMessage": "启用下载完成处理", - "IndexerJackettAllHealthCheckMessage": "使用 Jackett 不受支持的“全部”终点的索引器:{0}", - "IndexerLongTermStatusUnavailableHealthCheckMessage": "由于故障6小时,下列索引器都已不可用:{0}", + "IndexerJackettAllHealthCheckMessage": "使用 Jackett 不受支持的“全部”终点的索引器:{indexerNames}", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "由于故障6小时,下列索引器都已不可用:{indexerNames}", "IndexerRssNoIndexersAvailableHealthCheckMessage": "由于最近的索引器错误,所有支持rss的索引器暂时不可用", - "IndexerRssNoIndexersEnabledHealthCheckMessage": "没有启用RSS同步的索引器,Sonarr不会自动抓取新版本", - "IndexerSearchNoAutomaticHealthCheckMessage": "没有启用自动搜索的索引器,Sonarr不会提供任何自动搜索结果", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "没有启用RSS同步的索引器,{appName}不会自动抓取新版本", + "IndexerSearchNoAutomaticHealthCheckMessage": "没有启用自动搜索的索引器,{appName}不会提供任何自动搜索结果", "IndexerStatusAllUnavailableHealthCheckMessage": "所有搜刮器都因错误不可用", - "MountHealthCheckMessage": "挂载的媒体路径是只读的: ", + "MountSeriesHealthCheckMessage": "挂载的媒体路径是只读的: ", "Network": "网络", "NextAiring": "下一次上映", "OneSeason": "季1", "OriginalLanguage": "原语言", "Priority": "优先级", - "ProxyBadRequestHealthCheckMessage": "测试代理失败。状态代码:{0}", - "RecycleBinUnableToWriteHealthCheckMessage": "配置文件夹:{0}无法写入,检查此路径是否存在,并且是否可由Sonarr写入", + "ProxyBadRequestHealthCheckMessage": "测试代理失败。状态代码:{statusCode}", + "RecycleBinUnableToWriteHealthCheckMessage": "配置文件夹:{path}无法写入,检查此路径是否存在,并且是否可由{appName}写入", "QualityProfile": "质量配置", "RefreshSeries": "刷新节目", - "RemotePathMappingBadDockerPathHealthCheckMessage": "您正在使用docker;下载客户端{0}下载目录为{1},但这不是有效的{2}路径。查看Docker路径映射并为下载客户端重新配置。", - "RemotePathMappingDownloadPermissionsHealthCheckMessage": "Sonarr已存在剧集目录{0}但无法访问。可能是权限错误。", + "RemotePathMappingBadDockerPathHealthCheckMessage": "您正在使用docker;下载客户端{downloadClientName}下载目录为{path},但这不是有效的{osName}路径。查看Docker路径映射并为下载客户端重新配置。", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName}已存在剧集目录{path}但无法访问。可能是权限错误。", "Mode": "模式", "MoreInfo": "更多信息", "New": "新的", "NoUpdatesAreAvailable": "无可用更新", - "OnLatestVersion": "已安装最新版的Sonarr", + "OnLatestVersion": "已安装最新版的{appName}", "NextExecution": "下一次执行", "NoBackupsAreAvailable": "无备份可用", "NoEventsFound": "无事件", @@ -107,7 +107,7 @@ "NoLeaveIt": "不,就这样", "NoLogFiles": "没有日志文件", "Options": "选项", - "RemotePathMappingDockerFolderMissingHealthCheckMessage": "您正在使用docker;下载客户端{0}下载目录为{1},但容器中似乎不存在此目录。请检查Docker路径映射。", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "您正在使用docker;下载客户端$1{downloadClientName}下载目录为{path},但容器中似乎不存在此目录。请检查Docker路径映射。", "AirDate": "播出日期", "Daily": "日常的", "FullSeason": "全部剧集", @@ -115,7 +115,7 @@ "ImportLists": "导入列表", "ExistingTag": "已有标签", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "由于故障超过6小时,所有索引器均不可用", - "IndexerStatusUnavailableHealthCheckMessage": "搜刮器因错误不可用:{0}", + "IndexerStatusUnavailableHealthCheckMessage": "搜刮器因错误不可用:{indexerNames}", "IndexerSearchNoAvailableIndexersHealthCheckMessage": "由于最近的索引器错误,所有支持搜索的索引器暂时不可用", "LogFiles": "日志文件", "MatchedToEpisodes": "与剧集匹配", @@ -132,11 +132,11 @@ "Proper": "合适的", "ReleaseGroup": "发布组", "ReleaseTitle": "发行版标题", - "RemotePathMappingFileRemovedHealthCheckMessage": "文件{0} 在处理的过程中被部分删除。", + "RemotePathMappingFileRemovedHealthCheckMessage": "文件{path} 在处理的过程中被部分删除。", "AutoAdd": "自动添加", "Cancel": "取消", - "DeleteSelectedDownloadClientsMessageText": "是否确实要删除 {count} 个选定的下载客户端?", - "DeleteSelectedImportListsMessageText": "是否确实要删除 {count} 个选定的导入列表?", + "DeleteSelectedDownloadClientsMessageText": "您确定要删除{count}选定的下载客户端吗?", + "DeleteSelectedImportListsMessageText": "您确定要删除选定的{count}导入列表吗?", "Details": "详情", "Name": "名称", "No": "否", @@ -144,7 +144,7 @@ "LibraryImport": "媒体库导入", "PreviousAiring": "上一次播出", "Profiles": "配置", - "ProxyResolveIpHealthCheckMessage": "无法解析已设置的代理服务器主机{0}的IP地址", + "ProxyResolveIpHealthCheckMessage": "无法解析已设置的代理服务器主机{proxyHostName}的IP地址", "Quality": "媒体质量", "Queue": "队列", "Real": "真的", @@ -166,10 +166,10 @@ "Date": "日期", "DeleteBackup": "删除备份", "DeleteCustomFormat": "删除自定义命名格式", - "DeleteSelectedIndexersMessageText": "是否确实要删除 {count} 个选定的索引器?", + "DeleteSelectedIndexersMessageText": "您确定要删除{count}选定的索引器吗?", "Deleted": "已删除", "Disabled": "禁用", - "Discord": "Discord", + "Discord": "分歧", "DiskSpace": "硬盘空间", "Docker": "Docker", "DockerUpdater": "更新Docker容器以更新应用", @@ -219,11 +219,11 @@ "RelativePath": "相对路径", "ReleaseGroups": "制作团队", "Reload": "重新加载", - "DeleteBackupMessageText": "您确定要删除备份 '{name}' 吗?", + "DeleteBackupMessageText": "您确定要删除备份“{name}”吗?", "EnableAutomaticSearch": "启用自动搜索", "EpisodeAirDate": "剧集播出日期", - "IndexerSearchNoInteractiveHealthCheckMessage": "没有启用交互式搜索的索引器,Sonarr将不提供任何交互式搜索结果", - "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {0}", + "IndexerSearchNoInteractiveHealthCheckMessage": "没有启用交互式搜索的索引器,{appName}将不提供任何交互式搜索结果", + "ProxyFailedToTestHealthCheckMessage": "测试代理失败: {url}", "About": "关于", "Actions": "动作", "AppDataDirectory": "AppData目录", @@ -236,7 +236,7 @@ "Clear": "清除", "CountDownloadClientsSelected": "已选择 {count} 个下载客户端", "Exception": "例外", - "ExternalUpdater": "Sonarr配置为使用外部更新机制", + "ExternalUpdater": "{appName}配置为使用外部更新机制", "FailedToFetchUpdates": "无法获取更新", "EnableInteractiveSearch": "启用手动搜索", "EpisodeInfo": "剧集信息", @@ -274,43 +274,42 @@ "Tags": "标签", "Series": "节目", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "如果可能,在下载完成后自动处理", - "RemotePathMappingGenericPermissionsHealthCheckMessage": "下载客户端{0}将文件下载在{1}中,但Sonarr无法找到此目录。您可能需要调整文件夹的权限。", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "本地下载客户端{0}将文件下载在{1}中,但这不是有效的{2}路径。查看您的下载客户端设置。", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "远程下载客户端{0}报告了{1}中的文件,但此目录似乎不存在。可能缺少远程路径映射。", - "IRCLinkText": "#Libera上的Sonarr", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}将文件下载在{path}中,但{appName}无法找到此目录。您可能需要调整文件夹的权限。", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "本地下载客户端{downloadClientName}将文件下载在{path}中,但这不是有效的{osName}路径。查看您的下载客户端设置。", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "远程下载客户端{downloadClientName}报告了{path}中的文件,但此目录似乎不存在。可能缺少远程路径映射。", + "IRCLinkText": "#Libera上的{appName}", "LiberaWebchat": "Libera聊天", - "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "您正在使用Docker;下载客户端{0}报告了{1}中的文件,但这不是有效的{2}中的路径。查看Docker路径映射并更新下载客户端设置。", - "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "下载客户端{0}报告的文件在{1},但Sonarr无法查看此目录。您可能需要调整文件夹的权限。", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "您正在使用Docker;下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}中的路径。查看Docker路径映射并更新下载客户端设置。", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}报告的文件在{path},但{appName}无法查看此目录。您可能需要调整文件夹的权限。", "RemovedFromTaskQueue": "从任务队列中删除", - "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "本地下载客户端{0}报告了{1}中的文件,但这不是有效的{2}路径。查看您的下载客户端设置。", - "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "远程下载客户端{0}报告了{1}中的文件,但这不是有效的{2}路径。查看远程路径映射并更新下载客户端设置。", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "本地下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}路径。查看您的下载客户端设置。", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "远程下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}路径。查看远程路径映射并更新下载客户端设置。", "Restart": "重启", - "RestartReloadNote": "注意:Sonarr将在恢复过程中自动重启并重新加载UI。", + "RestartReloadNote": "注意:{appName}将在恢复过程中自动重启并重新加载UI。", "Restore": "恢复", "RootFolder": "根目录", "RemoveSelectedItems": "删除所选项目", - "RemovedSeriesSingleRemovedHealthCheckMessage": "节目{0}已从TVDB中删除", - "RootFolderMissingHealthCheckMessage": "缺少根目录: {0}", - "RootFolderMultipleMissingHealthCheckMessage": "多个根目录缺失:{0}", - "SkipRedownloadHelpText": "阻止Sonarr尝试下载此项目的替代版本", + "RemovedSeriesSingleRemovedHealthCheckMessage": "节目{series}已从TVDB中删除", + "RootFolderMissingHealthCheckMessage": "缺少根目录: {rootFolderPath}", + "RootFolderMultipleMissingHealthCheckMessage": "多个根目录缺失:{rootFolderPaths}", + "SkipRedownloadHelpText": "阻止{appName}尝试下载此项目的替代版本", "Tasks": "任务", "Wanted": "想要的", "Yes": "确定", - "UpdateUINotWritableHealthCheckMessage": "无法安装升级,因为用户“{1}”不可写入界面文件夹“{0}”。", "AbsoluteEpisodeNumbers": "准确的集数", "RemoveCompleted": "移除已完成", "RemoveFailed": "删除失败", - "RemovedSeriesMultipleRemovedHealthCheckMessage": "已从TVDB中删除节目{0}", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "下载目录{0}已存在但Sonarr无法访问。可能是权限错误。", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "已从TVDB中删除节目{series}", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "下载目录{downloadPath}已存在但{appName}无法访问。可能是权限错误。", "RemovingTag": "移除标签", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "远程下载客户端{0}将文件下载在{1}中,但此目录似乎不存在。可能缺少或不正确的远程路径映射。", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "远程下载客户端{downloadClientName}将文件下载在{path}中,但此目录似乎不存在。可能缺少或不正确的远程路径映射。", "Replace": "替换", "Repack": "重新打包", "Version": "版本", "Special": "特色", - "RemotePathMappingImportFailedHealthCheckMessage": "Sonarr无法导入剧集。查看日志以了解详细信息。", - "RemotePathMappingWrongOSPathHealthCheckMessage": "远程下载客户端{0}将文件下载在{1}中,但这不是有效的{2}路径。查看远程路径映射并更新下载客户端设置。", - "RemoveFromDownloadClientHelpTextWarning": "移除操作会从下载客户端中删除任务和已下载文件。", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName}无法导入剧集。查看日志以了解详细信息。", + "RemotePathMappingWrongOSPathHealthCheckMessage": "远程下载客户端{downloadClientName}将文件下载在{path}中,但这不是有效的{osName}路径。查看远程路径映射并更新下载客户端设置。", + "RemoveFromDownloadClientHelpTextWarning": "删除将从下载客户端删除下载和文件。", "Renamed": "已重命名", "RootFolderPath": "根目录路径", "Runtime": "时长", @@ -325,7 +324,7 @@ "Title": "标题", "Type": "类型", "UnmonitoredOnly": "监控中", - "UpdateStartupTranslocationHealthCheckMessage": "无法安装更新,因为启动文件夹“{0}”在一个应用程序迁移文件夹。", + "UpdateStartupTranslocationHealthCheckMessage": "无法安装更新,因为启动文件夹“{startupFolder}”在一个应用程序迁移文件夹。", "Updates": "更新", "VideoCodec": "视频编码", "VideoDynamicRange": "视频动态范围", @@ -334,7 +333,7 @@ "SeasonCount": "季数量", "SystemTimeHealthCheckMessage": "系统时间相差超过1天。在纠正时间之前,计划的任务可能无法正确运行", "TestAll": "测试全部", - "UpdateStartupNotWritableHealthCheckMessage": "无法安装更新,因为用户“{1}”对于启动文件夹“{0}”没有写入权限。", + "UpdateStartupNotWritableHealthCheckMessage": "无法安装更新,因为用户“{userName}”对于启动文件夹“{startupFolder}”没有写入权限。", "SeriesTitle": "节目标题", "SetTags": "设置标签", "Size": "大小", @@ -357,7 +356,7 @@ "SeriesEditor": "节目编辑", "Twitter": "Twitter", "UnableToLoadBackups": "无法加载备份", - "UnableToUpdateSonarrDirectly": "无法直接更新Sonarr,", + "UnableToUpdateSonarrDirectly": "无法直接更新{appName},", "Unmonitored": "未监控", "TheLogLevelDefault": "默认的日志等级为“信息”,可以在[常规设置](/settings/general)中修改", "Time": "时间", @@ -384,10 +383,10 @@ "AddDownloadClient": "添加下载客户端", "AddDownloadClientError": "无法添加下载客户端,请稍后重试。", "AddConditionImplementation": "添加条件 - {implementationName}", - "AddConnectionImplementation": "添加连接 - {implementationName}", - "AddCustomFilter": "添加自定义过滤", - "AddDownloadClientImplementation": "添加下载客户端 - {implementationName}", - "AddImportListImplementation": "添加导入列表 - {implementationName}", + "AddConnectionImplementation": "添加连接- {implementationName}", + "AddCustomFilter": "添加自定义过滤器", + "AddDownloadClientImplementation": "添加下载客户端- {implementationName}", + "AddImportListImplementation": "添加导入列表- {implementationName}", "AddIndexerImplementation": "添加索引器 - {implementationName}", "AddNewRestriction": "添加新限制", "AddNewSeriesRootFolderHelpText": "将自动创建 '{folder}' 子文件夹", @@ -401,19 +400,19 @@ "AddRemotePathMappingError": "无法添加远程路径映射,请稍后重试。", "AddSeriesWithTitle": "添加 {title}", "AddToDownloadQueue": "添加到下载队列", - "AddedToDownloadQueue": "已添加到下载队列", + "AddedToDownloadQueue": "已加入下载队列", "AllFiles": "全部文件", "AlreadyInYourLibrary": "已经在你的库中", "Always": "总是", - "AnimeEpisodeFormat": "动漫集格式", - "AnimeTypeFormat": "绝对集数 ({format})", + "AnimeEpisodeFormat": "动漫单集格式", + "AnimeEpisodeTypeFormat": "绝对集数 ({format})", "AppUpdated": "{appName} 升级", "AppUpdatedVersion": "{appName} 已经更新到 {version} 版本,重新加载 {appName} 使更新生效 ", - "AuthenticationRequired": "需要认证", + "AuthenticationRequired": "需要身份验证", "AutomaticUpdatesDisabledDocker": "不支持在使用 Docker 容器时直接升级。你需要升级 {appName} 容器镜像或使用脚本(script)", "BackupIntervalHelpText": "自动备份时间间隔", "Branch": "分支", - "BranchUpdate": "更新Sonarr的分支", + "BranchUpdate": "更新{appName}的分支", "BranchUpdateMechanism": "外部更新机制使用的分支", "BrowserReloadRequired": "浏览器需重新加载", "BuiltIn": "内置的", @@ -421,23 +420,23 @@ "BypassDelayIfHighestQualityHelpText": "当发布版本的质量中含有已启用的最高质量首选协议时跳过延迟", "BypassProxyForLocalAddresses": "对局域网地址不使用代理", "CalendarFeed": "{appName} 日历订阅", - "CalendarLegendDownloadedTooltip": "集已下载完成并完成排序", - "CalendarLegendDownloadingTooltip": "集正在下载中", - "CalendarLegendFinaleTooltip": "剧集或季完结", - "CalendarLegendMissingTooltip": "集已播出,但硬盘中缺失", - "CalendarLegendOnAirTooltip": "集正在播出", - "CalendarLegendPremiereTooltip": "剧集或季首播", - "CalendarLegendUnairedTooltip": "集尚未播出", - "CalendarLegendUnmonitoredTooltip": "集未被监控", + "CalendarLegendEpisodeDownloadedTooltip": "集已下载完成并完成排序", + "CalendarLegendEpisodeDownloadingTooltip": "集正在下载中", + "CalendarLegendSeriesFinaleTooltip": "剧集或季完结", + "CalendarLegendEpisodeMissingTooltip": "集已播出,但硬盘中缺失", + "CalendarLegendEpisodeOnAirTooltip": "集正在播出", + "CalendarLegendSeriesPremiereTooltip": "剧集或季首播", + "CalendarLegendEpisodeUnairedTooltip": "集尚未播出", + "CalendarLegendEpisodeUnmonitoredTooltip": "集未被监控", "CalendarOptions": "日历选项", "CancelProcessing": "取消进行中", "CertificateValidation": "验证证书", - "ChooseImportMode": "选择导入模式", - "ChownGroupHelpTextWarning": "这只在Sonarr程序是文件所有者的情况下才有效。最好确保下载客户端使用与Sonarr相同的用户组。", + "ChooseImportMode": "选择导入方式", + "ChownGroupHelpTextWarning": "这只在{appName}程序是文件所有者的情况下才有效。最好确保下载客户端使用与{appName}相同的用户组。", "ClickToChangeEpisode": "点击修改集", - "ClickToChangeQuality": "点击修改质量", + "ClickToChangeQuality": "点击更改质量", "ClickToChangeLanguage": "点击更换语言", - "ClickToChangeReleaseGroup": "点击修改发布组", + "ClickToChangeReleaseGroup": "单击更改发布组", "ClickToChangeSeason": "点击修改季", "ClickToChangeSeries": "点击修改剧集", "CloneAutoTag": "复制自动标签", @@ -448,31 +447,31 @@ "ConnectSettingsSummary": "通知、与媒体服务器/播放器的链接、自定义脚本", "ConnectionLost": "连接丢失", "ConnectionLostReconnect": "{appName} 将会尝试自动连接,您也可以点击下方的重新加载。", - "ConnectionLostToBackend": "{appName} 与后端的链接已断开,需要重新加载恢复功能。", + "ConnectionLostToBackend": "{appName}失去了与后端的连接,需要重新加载以恢复功能。", "Connections": "连接", "Continuing": "仍在继续", "CopyToClipboard": "复制到剪贴板", - "CopyUsingHardlinksHelpTextWarning": "有时候,文件锁可能会阻止对正在做种的文件进行重命名。您可以暂时禁用做种功能,并使用Sonarr的重命名功能作为解决方案。", + "CopyUsingHardlinksHelpTextWarning": "有时候,文件锁可能会阻止对正在做种的文件进行重命名。您可以暂时禁用做种功能,并使用{appName}的重命名功能作为解决方案。", "CouldNotFindResults": "找不到 '{term}' 的任何结果", "CreateEmptySeriesFolders": "创建空剧集文件夹", "CreateEmptySeriesFoldersHelpText": "在扫描硬盘时创建缺失的剧集文件夹", "Custom": "自定义", "CreateGroup": "创建组", - "CustomFilters": "自定义过滤", + "CustomFilters": "自定义过滤器", "CustomFormatUnknownCondition": "未知自定义格式条件 '{implementation}'", "CustomFormatUnknownConditionOption": "未知的条件“{key}”的选项“{implementation}”", "CustomFormatsLoadError": "无法加载自定义格式", "CustomFormatsSettings": "自定义格式设置", "CustomFormatsSettingsSummary": "自定义格式和设置", "Cutoff": "截止", - "DailyTypeFormat": "日期 ({format})", + "DailyEpisodeTypeFormat": "日期 ({format})", "Day": "天", "Default": "默认", - "DefaultDelayProfile": "这是默认配置,它适用于所有没有明确配置的剧集。", - "DefaultNotFoundMessage": "你一定迷路了,这里什么都没有。", + "DefaultDelayProfileSeries": "这是默认配置,它适用于所有没有明确配置的剧集。", + "DefaultNotFoundMessage": "你一定是迷路了,这里没什么可看的。", "DelayMinutes": "{delay} 分钟", "DelayProfile": "延时配置", - "DelayProfileTagsHelpText": "至少有一个匹配的标签应用于剧集", + "DelayProfileSeriesTagsHelpText": "至少有一个匹配的标签应用于剧集", "DelayProfiles": "延迟配置", "DelayProfileProtocol": "协议:{preferredProtocol}", "DeleteAutoTag": "删除自动标签", @@ -480,48 +479,48 @@ "DeleteDownloadClient": "删除下载客户端", "DeleteDownloadClientMessageText": "你确定要删除下载客户端 “{name}” 吗?", "DeleteEmptyFolders": "删除空目录", - "DeleteEmptyFoldersHelpText": "如果集文件被删除,当磁盘扫描时删除剧集或季的空目录", + "DeleteEmptySeriesFoldersHelpText": "如果集文件被删除,当磁盘扫描时删除剧集或季的空目录", "DeleteEpisodeFile": "删除集文件", - "DeleteEpisodeFileMessage": "你确定要删除 “{path}” 吗?", - "DeleteEpisodeFromDisk": "从磁盘中删除集", + "DeleteEpisodeFileMessage": "您确定要删除“{path}”吗?", + "DeleteEpisodeFromDisk": "从磁盘中删除剧集", "DeleteImportList": "删除导入的列表", "DeleteQualityProfile": "删除质量配置", - "DeleteQualityProfileMessageText": "你确定要删除质量配置 “{name}” 吗?", + "DeleteQualityProfileMessageText": "您确定要删除质量配置“{name}”吗?", "DeleteReleaseProfile": "删除发布组配置", - "DeleteReleaseProfileMessageText": "你确定要删除发布组配置 “{name}” 吗?", - "DeleteRemotePathMapping": "删除远程映射路径", + "DeleteReleaseProfileMessageText": "您确定要删除此发布配置文件“{name}”吗?", + "DeleteRemotePathMapping": "删除远程路径映射", "DeleteRemotePathMappingMessageText": "你确定要删除此远程路径映射吗?", "DeleteRootFolderMessageText": "你确定要删除根目录 “{path}” 吗?", - "DeleteSelectedEpisodeFilesHelpText": "你确定要删除选中的集文件吗?", + "DeleteSelectedEpisodeFilesHelpText": "您确定要删除选定的剧集文件吗?", "DeleteTag": "删除标签", "DownloadClientOptionsLoadError": "无法加载下载客户端选项", "DownloadClientSettings": "下载客户端设置", "DownloadFailed": "下载失败", - "DownloadFailedTooltip": "集下载失败", + "DownloadFailedEpisodeTooltip": "集下载失败", "DownloadWarning": "下载警告:{warningMessage}", "Downloaded": "已下载", "Downloading": "下载中", - "FailedToLoadCustomFiltersFromApi": "无法从 API 中加载自定义过滤器", - "FailedToLoadQualityProfilesFromApi": "无法从 API 中加载质量配置", - "FailedToLoadSeriesFromApi": "无法从 API 中加载剧集", - "FailedToLoadSonarr": "无法加载 Sonarr", - "FailedToLoadSystemStatusFromApi": "无法从 API 中加载系统状态", - "FailedToLoadTagsFromApi": "无法从 API 中加载标签", - "FailedToLoadTranslationsFromApi": "无法从 API 中加载翻译", - "FailedToLoadUiSettingsFromApi": "无法从 API 中加载 UI 设置", + "FailedToLoadCustomFiltersFromApi": "未能从API加载自定义过滤器", + "FailedToLoadQualityProfilesFromApi": "未能从API加载质量配置文件", + "FailedToLoadSeriesFromApi": "未能从API加载系列", + "FailedToLoadSonarr": "无法加载 {appName}", + "FailedToLoadSystemStatusFromApi": "未能从API加载系统状态", + "FailedToLoadTagsFromApi": "未能从API加载标签", + "FailedToLoadTranslationsFromApi": "未能从API加载翻译", + "FailedToLoadUiSettingsFromApi": "未能从API加载UI设置", "File": "文件", "FileBrowser": "文件浏览器", - "FileBrowserPlaceholderText": "输入路径或者从下面选择", + "FileBrowserPlaceholderText": "开始输入或选择下面的路径", "FileManagement": "文件管理", "FileNameTokens": "文件名标记", "FileNames": "文件名", "Filter": "过滤", - "UpdateMechanismHelpText": "使用 Sonarr 内置的更新程序或脚本", - "AuthenticationRequiredHelpText": "更改需要身份验证的请求。除非您了解风险,否则请勿更改。", + "UpdateMechanismHelpText": "使用 {appName} 内置的更新程序或脚本", + "AuthenticationRequiredHelpText": "更改身份验证的请求。除非您了解风险,否则请勿更改。", "AnEpisodeIsDownloading": "集正在下载", - "AuthenticationRequiredWarning": "为了防止在没有身份验证的情况下进行远程访问,{appName} 需要启用身份验证。请配置您的身份验证方法和凭据。您可以选择从本地地址禁用身份验证。", + "AuthenticationRequiredWarning": "为了防止未经身份验证的远程访问,{appName} 现在需要启用身份验证。您可以禁用本地地址的身份验证。", "AutomaticSearch": "自动搜索", - "BackupFolderHelpText": "相对路径将在Sonarr的AppData目录下", + "BackupFolderHelpText": "相对路径将在{appName}的AppData目录下", "BindAddress": "绑定地址", "BindAddressHelpText": "有效的 IP 地址、localhost、或以'*'代表所有接口", "BlocklistLoadError": "无法加载黑名单", @@ -529,7 +528,7 @@ "AddNewSeriesError": "读取搜索结果失败,请稍后重试。", "CloneProfile": "复制配置", "ColonReplacement": "替换冒号", - "ColonReplacementFormatHelpText": "修改Sonarr如何处理冒号的替换", + "ColonReplacementFormatHelpText": "修改{appName}如何处理冒号的替换", "CollectionsLoadError": "不能加载收藏", "CompletedDownloadHandling": "完成下载处理", "DeleteDelayProfile": "删除延迟配置", @@ -539,14 +538,14 @@ "AllSeriesInRootFolderHaveBeenImported": "{path} 中的所有剧集都已导入", "Analytics": "分析", "Anime": "动漫", - "AnalyseVideoFilesHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要Sonarr读取文件,可能导致扫描期间磁盘或网络出现高负载。", - "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到Sonarr的服务器。这包括有关您的浏览器的信息、您使用的Sonarr WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", + "AnalyseVideoFilesHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要{appName}读取文件,可能导致扫描期间磁盘或网络出现高负载。", + "AnalyticsEnabledHelpText": "将匿名使用情况和错误信息发送到{appName}的服务器。这包括有关您的浏览器的信息、您使用的{appName} WebUI页面、错误报告以及操作系统和运行时版本。我们将使用此信息来确定功能和错误修复的优先级。", "AnalyseVideoFiles": "分析视频文件", "ApplicationURL": "应用程序 URL", - "AnimeTypeDescription": "使用绝对集数发布的集数", - "ApiKey": "API 密钥", + "AnimeEpisodeTypeDescription": "使用绝对集数发布的集数", + "ApiKey": "API Key", "ApplicationUrlHelpText": "此应用的外部URL,包含 http(s)://、端口和基本URL", - "AuthenticationMethodHelpText": "需要用户名和密码来访问 {appName}", + "AuthenticationMethodHelpText": "需要用户名和密码以访问 {appName}", "AuthBasic": "基础(浏览器弹出对话框)", "AuthForm": "表单(登陆页面)", "Authentication": "认证", @@ -555,7 +554,7 @@ "Automatic": "自动化", "AutoTaggingRequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自动标记规则。否则,一个 {implementationName} 匹配就足够了。", "AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。", - "BackupRetentionHelpText": "早于保留周期的自动备份将被自动清除", + "BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理", "BackupsLoadError": "无法加载备份", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "最小自定义格式分数", "BypassDelayIfHighestQuality": "如果达到最高质量,则跳过", @@ -564,7 +563,7 @@ "ChmodFolderHelpText": "八进制,当导入和重命名媒体文件夹和文件时应用(不带执行位)", "CheckDownloadClientForDetails": "查看下载客户端了解更多详细信息", "ChmodFolder": "修改文件夹权限", - "ChmodFolderHelpTextWarning": "这只在Sonarr程序是文件所有者的情况下才有效。最好确保下载客户端正确设置权限。", + "ChmodFolderHelpTextWarning": "这只在{appName}程序是文件所有者的情况下才有效。最好确保下载客户端正确设置权限。", "ChownGroup": "修改组权限", "ChooseAnotherFolder": "选择其他文件夹", "ChownGroupHelpText": "组名称或GID。对于远程文件系统请使用GID。", @@ -574,24 +573,24 @@ "Debug": "调试", "DelayProfilesLoadError": "无法加载延时配置", "DeleteImportListExclusion": "删除导入排除列表", - "DeleteIndexerMessageText": "您确定要删除索引器 “{name}” 吗?", + "DeleteIndexerMessageText": "您确定要删除索引器“{name}”吗?", "DeleteImportListExclusionMessageText": "你确定要删除这个导入排除列表吗?", "DeleteImportListMessageText": "您确定要删除列表 “{name}” 吗?", "DeleteNotification": "删除消息推送", - "DeleteNotificationMessageText": "您确定要删除消息推送 “{name}” 吗?", + "DeleteNotificationMessageText": "您确定要删除通知“{name}”吗?", "DeleteIndexer": "删除索引器", "DeleteTagMessageText": "您确定要删除标签 '{label}' 吗?", "DeletedReasonUpgrade": "升级时删除原文件", "DestinationRelativePath": "目的相对路径", - "DisabledForLocalAddresses": "对局域网内禁用", + "DisabledForLocalAddresses": "在本地地址上禁用", "DestinationPath": "目的路径", "DoneEditingGroups": "完成编辑组", "DoNotPrefer": "不要首选", "DoNotUpgradeAutomatically": "不要自动升级", - "DownloadIgnored": "下载被忽略", - "DownloadIgnoredTooltip": "集下载被忽略", + "DownloadIgnored": "忽略下载", + "DownloadIgnoredEpisodeTooltip": "集下载被忽略", "EditAutoTag": "编辑自动标签", - "AddAutoTagError": "无法添加一个新自动标签,请重试。", + "AddAutoTagError": "无法添加新的自动标签,请重试。", "AddImportListExclusionError": "无法添加新排除列表,请再试一次。", "AddIndexer": "添加索引器", "AddImportList": "添加导入列表", @@ -605,27 +604,27 @@ "CalendarLoadError": "无法加载日历", "Agenda": "日程表", "CertificateValidationHelpText": "改变HTTPS证书验证的严格程度。不要更改除非您了解风险。", - "CopyUsingHardlinksHelpText": "硬链接 (Hardlinks) 允许 Sonarr 将还在做种中的剧集文件(夹)导入而不占用额外的存储空间或者复制文件(夹)的全部内容。硬链接 (Hardlinks) 仅能在源文件和目标文件在同一磁盘卷中使用", + "CopyUsingHardlinksSeriesHelpText": "硬链接 (Hardlinks) 允许 {appName} 将还在做种中的剧集文件(夹)导入而不占用额外的存储空间或者复制文件(夹)的全部内容。硬链接 (Hardlinks) 仅能在源文件和目标文件在同一磁盘卷中使用", "CustomFormat": "自定义命名格式", - "CustomFormatHelpText": "Sonarr会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数,有相同或更高的影片质量,则Sonarr会抓取该发布版本。", + "CustomFormatHelpText": "{appName}会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数,有相同或更高的影片质量,则{appName}会抓取该发布版本。", "DeleteAutoTagHelpText": "你确定要删除 “{name}” 自动标签吗?", - "DownloadClientTagHelpText": "仅将此下载客户端用于至少具有一个匹配标签的剧集。留空可用于所有剧集。", - "Absolute": "准确的", + "DownloadClientSeriesTagHelpText": "仅将此下载客户端用于至少具有一个匹配标签的剧集。留空可用于所有剧集。", + "Absolute": "绝对", "AddANewPath": "添加一个新的目录", "AbsoluteEpisodeNumber": "准确的集数", - "DeleteSelectedEpisodeFiles": "删除选中的集文件", + "DeleteSelectedEpisodeFiles": "删除选定的剧集文件", "Donate": "捐赠", "DownloadPropersAndRepacksHelpTextCustomFormat": "使用“不要首选”按 优化版(Propers) / 重制版(Repacks) 中的自定义格式分数排序", "DownloadPropersAndRepacksHelpTextWarning": "使用自定义格式自动升级到优化版/重制版", - "EditConditionImplementation": "编辑条件 - {implementationName}", - "EditConnectionImplementation": "编辑连接 - {implementationName}", - "Duplicate": "重复", + "EditConditionImplementation": "编辑条件- {implementationName}", + "EditConnectionImplementation": "编辑连接- {implementationName}", + "Duplicate": "副本", "EditDelayProfile": "编辑延时配置", - "EditDownloadClientImplementation": "编辑下载客户端 - {implementationName}", + "EditDownloadClientImplementation": "编辑下载客户端- {implementationName}", "EditCustomFormat": "编辑自定义格式", "EditGroups": "编辑组", - "EditImportListImplementation": "编辑导入列表 - {implementationName}", - "EditIndexerImplementation": "编辑索引器 - {implementationName}", + "EditImportListImplementation": "编辑导入列表- {implementationName}", + "EditIndexerImplementation": "编辑索引器- {implementationName}", "EditListExclusion": "编辑排除列表", "EditImportListExclusion": "编辑导入排除列表", "EditMetadata": "编辑 {metadataType} 元数据", @@ -639,19 +638,19 @@ "EnableHelpText": "启用此元数据类型的元数据文件创建", "EpisodeFileRenamed": "集文件已被重命名", "EpisodeFileRenamedTooltip": "集文件已被重命名", - "EpisodeHistoryLoadError": "无法加载集的历史记录", + "EpisodeHistoryLoadError": "无法载入剧集历史记录", "EpisodeImported": "集已被导入", "EpisodeImportedTooltip": "集下载成功并从下载客户端获取", - "EpisodeHasNotAired": "集尚未播出", + "EpisodeHasNotAired": "这一集还没有播出", "EpisodeIsNotMonitored": "集未被监控", "EpisodeMissingAbsoluteNumber": "集没有准确的集数", "EpisodeIsDownloading": "集正在下载", "EpisodeSearchResultsLoadError": "无法加载此集的搜索结果。稍后再试", "EpisodeNaming": "集命名", "EpisodeTitleRequired": "需要集标题", - "EpisodesLoadError": "无法加载集", - "ErrorLoadingContents": "读取内容错误", - "ErrorLoadingContent": "加载此内容时出错", + "EpisodesLoadError": "无法加载剧集", + "ErrorLoadingContents": "加载内容出错", + "ErrorLoadingContent": "加载此内容时出现错误", "Existing": "已存在", "ExistingSeries": "已存在剧集", "External": "外部的", @@ -660,10 +659,10 @@ "ExtraFileExtensionsHelpText": "导入逗号分隔其他文件(.nfo将做为.nfo-orig被导入)", "FilterContains": "包含", "FilterDoesNotContain": "不包含", - "FilterEndsWith": "以…结尾", + "FilterEndsWith": "以…完结", "FilterEqual": "相等", - "FilterDoesNotStartWith": "不以…开头", - "FilterEpisodesPlaceholder": "通过标题或数字过滤集", + "FilterDoesNotStartWith": "不以...开始", + "FilterEpisodesPlaceholder": "按标题或编号过滤剧集", "FilterInLast": "在最后", "FilterInNext": "在下一个", "FilterNotInLast": "不在最后", @@ -676,7 +675,7 @@ "Folder": "文件夹", "GeneralSettingsLoadError": "无法加载通用设置", "GeneralSettingsSummary": "端口、SSL、用户名/密码、代理、分析、更新", - "GrabReleaseMessageText": "Sonarr无法确定这个发布版本是哪部剧集的哪一集,Sonarr可能无法自动导入此版本,你想要获取“{title}”吗?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName}无法确定这个发布版本是哪部剧集的哪一集,{appName}可能无法自动导入此版本,你想要获取“{title}”吗?", "GrabRelease": "抓取版本", "Here": "这里", "Group": "组", @@ -686,7 +685,7 @@ "ICalFeed": "iCal订阅地址", "Host": "主机", "ICalFeedHelpText": "将此URL复制到您的客户端,如果您的浏览器支持webcal,请直接点击订阅按钮", - "ICalIncludeUnmonitoredHelpText": "在iCal订阅中包含未监控的集", + "ICalIncludeUnmonitoredEpisodesHelpText": "在iCal订阅中包含未监控的集", "ICalLink": "iCal链接", "ICalShowAsAllDayEvents": "作为全天事件显示", "ICalShowAsAllDayEventsHelpText": "事件将以全天事件的形式显示在日历中", @@ -711,21 +710,21 @@ "DeleteSpecification": "删除规范", "DeleteSpecificationHelpText": "您确定要删除规范 '{name}' 吗?", "DeletedReasonManual": "文件已通过 UI 删除", - "DeletedReasonMissingFromDisk": "Sonarr 在磁盘上找不到该文件,因此已取消数据库中和该文件的集关联", + "DeletedReasonEpisodeMissingFromDisk": "{appName} 在磁盘上找不到该文件,因此已取消数据库中和该文件的集关联", "DownloadPropersAndRepacks": "优化版和重制版", "DownloadPropersAndRepacksHelpText": "是否自动更新至优化版和重制版", "Enable": "启用", "EnableMetadataHelpText": "启用此元数据类型的元数据文件创建", "EnableSsl": "启用SSL", - "EnableRssHelpText": "当Sonarr定期通过RSS同步查找发布时使用", + "EnableRssHelpText": "当{appName}定期通过RSS同步查找发布时使用", "EnableSslHelpText": "需要以管理员身份重新启动才能生效", "HistoryLoadError": "无法加载历史记录", - "CustomFormatJson": "自定义格式 JSON", + "CustomFormatJson": "自定义格式JSON", "Database": "数据库", "EditRemotePathMapping": "编辑远程映射路径", "EditRestriction": "编辑限制", "EnableAutomaticAdd": "启用自动添加", - "EnableAutomaticSearchHelpText": "当自动搜索通过 UI 或 Sonarr 执行时将被使用", + "EnableAutomaticSearchHelpText": "当自动搜索通过 UI 或 {appName} 执行时将被使用", "EnableAutomaticSearchHelpTextWarning": "当手动搜索启用时使用", "EnableColorImpairedMode": "启用色障模式", "EnableInteractiveSearchHelpTextWarning": "该索引器不支持搜索", @@ -733,9 +732,9 @@ "EpisodeFileDeleted": "集文件已删除", "EpisodeFileDeletedTooltip": "集文件已删除", "EpisodeMissingFromDisk": "磁盘中缺少集", - "ErrorLoadingItem": "加载此项目时出错", - "ErrorLoadingPage": "加载此页时出错", - "FilterDoesNotEndWith": "不以…结尾", + "ErrorLoadingItem": "加载此项目时出现错误", + "ErrorLoadingPage": "加载此页时出现错误", + "FilterDoesNotEndWith": "不以...结尾", "FilterGreaterThan": "大于", "FilterGreaterThanOrEqual": "大于等于", "FilterIs": "是", @@ -743,39 +742,39 @@ "FilterIsBefore": "在之前", "FilterIsNot": "不是", "FilterLessThan": "小于", - "FilterLessThanOrEqual": "小于等于", - "FilterStartsWith": "以…开头", + "FilterLessThanOrEqual": "小于或等于", + "FilterStartsWith": "以...开头", "FinaleTooltip": "剧集或季完结", "Global": "全局", "Grab": "抓取", "GrabId": "抓取ID", "GrabSelected": "抓取已选", - "ICalTagsHelpText": "剧集至少要匹配一个标签才会出现在订阅中", + "ICalTagsSeriesHelpText": "剧集至少要匹配一个标签才会出现在订阅中", "IconForSpecials": "特别节目的图标", "IconForSpecialsHelpText": "为特别节目(季0)显示图标", "ImdbId": "IMDb ID", - "ImportExtraFilesHelpText": "导入集文件后导入匹配的额外文件(字幕/nfo等)", + "ImportExtraFilesEpisodeHelpText": "导入集文件后导入匹配的额外文件(字幕/nfo等)", "ImportListSettings": "导入列表设置", "ImportListsLoadError": "无法加载导入列表", - "EnableAutomaticAddHelpText": "当通过 UI 或 Sonarr 执行同步时,将剧集添加到 Sonarr", + "EnableAutomaticAddSeriesHelpText": "当通过 UI 或 {appName} 执行同步时,将剧集添加到 {appName}", "EnableInteractiveSearchHelpText": "当手动搜索启用时使用", - "EnableMediaInfoHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要Sonarr读取文件,可能导致扫描期间磁盘或网络出现高负载。", - "GrabbedHistoryTooltip": "集抓取自 {indexer} 并发送至 {downloadClient}", - "HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查您的[日志]({link})来查找有关这些健康检查的更多信息。如果您在解读这些信息时遇到困难,可以通过以下链接联系我们获得支持。", - "StandardEpisodeFormat": "标准剧集格式", + "EnableMediaInfoHelpText": "从文件中提取视频信息,如分辨率、运行时间和编解码器信息。这需要{appName}读取文件,可能导致扫描期间磁盘或网络出现高负载。", + "EpisodeGrabbedTooltip": "集抓取自 {indexer} 并发送至 {downloadClient}", + "HealthMessagesInfoBox": "您可以通过单击行尾的wiki链接(图书图标)或检查[日志]({link})来查找有关这些运行状况检查消息原因的更多信息。如果你在理解这些信息方面有困难,你可以通过下面的链接联系我们的支持。", + "StandardEpisodeFormat": "标准单集格式", "DefaultNameCopiedSpecification": "{name} - 复制", - "AddListExclusion": "添加排除列表", - "AddListExclusionHelpText": "防止剧集通过列表添加到Sonarr", + "AddListExclusion": "添加列表例外", + "AddListExclusionSeriesHelpText": "防止剧集通过列表添加到{appName}", "AddNewSeriesSearchForCutoffUnmetEpisodes": "开始搜索未达截止条件的集", - "AddedDate": "新增: {date}", + "AddedDate": "加入于:{date}", "AllSeriesAreHiddenByTheAppliedFilter": "所有结果都被应用的过滤器隐藏", "AlternateTitles": "备选标题", "Any": "任何", - "AuthenticationMethodHelpTextWarning": "请选择有效的认证方式", - "AuthenticationMethod": "", - "AuthenticationRequiredPasswordHelpTextWarning": "输入一个新的密码", - "AuthenticationRequiredUsernameHelpTextWarning": "输入一个新的用户名", - "DailyEpisodeFormat": "每日剧集格式", + "AuthenticationMethodHelpTextWarning": "请选择一个有效的身份验证方式", + "AuthenticationMethod": "认证方式", + "AuthenticationRequiredPasswordHelpTextWarning": "请输入新密码", + "AuthenticationRequiredUsernameHelpTextWarning": "请输入新用户名", + "DailyEpisodeFormat": "每日单集格式", "MediaManagementSettingsSummary": "命名,文件管理和根文件夹设置", "ReplaceIllegalCharacters": "替换非法字符", "RenameFiles": "重命名文件", @@ -784,20 +783,20 @@ "SeriesCannotBeFound": "对不起,这个系列找不到。", "SeriesEditRootFolderHelpText": "将系列移动到相同的根文件夹可用于重命名系列文件夹以匹配已更新的标题或命名格式", "ReplaceWithSpaceDash": "替换为空格破折号", - "RenameEpisodesHelpText": "如果禁用重命名,Sonarr 将使用现有的文件名", + "RenameEpisodesHelpText": "如果禁用重命名,{appName} 将使用现有的文件名", "RenameEpisodes": "重命名剧集", "ReplaceWithDash": "替换为破折号", "ReplaceWithSpaceDashSpace": "替换为空格破折号空格", - "StandardTypeFormat": "季数和集数({format})", - "DailyTypeDescription": "使用年-月-日的每天或更少频率发布的剧集(2023-08-04)", + "StandardEpisodeTypeFormat": "季数和集数({format})", + "DailyEpisodeTypeDescription": "使用年-月-日的每天或更少频率发布的剧集(2023-08-04)", "Dash": "破折号", - "DelayingDownloadUntil": "将下载延迟到 {date} 的 {time}", + "DelayingDownloadUntil": "将下载推迟到 {date} 的 {time}", "SelectLanguage": "选择语言", "SelectLanguageModalTitle": "{modalTitle} - 选择语言", "DefaultNameCopiedProfile": "{name} - 复制", "SeriesFinale": "大结局", - "SeriesFolderFormat": "集文件夹格式", - "ReplaceIllegalCharactersHelpText": "替换非法字符。如果未选中,Sonarr将删除它们", + "SeriesFolderFormat": "剧集文件夹格式", + "ReplaceIllegalCharactersHelpText": "替换非法字符。如果未选中,{appName}将删除它们", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "剧集和剧集信息由TheTVDB.com提供。[请考虑支持他们](https://www.thetvdb.com/subscribe)。", "DeleteSelectedSeries": "删除选中的剧集", "ProxyBypassFilterHelpText": "使用“ , ”作为分隔符,和“ *. ”作为二级域名的通配符", @@ -808,56 +807,56 @@ "FormatAgeDays": "天", "RegularExpressionsTutorialLink": "有关正则表达式的更多详细信息,请参阅[此处](https://www.regular-expressions.info/tutorial.html)。", "FormatDateTime": "{formattedDate} {formattedTime}", - "ReleaseProfileTagHelpText": "发布配置将应用于至少有一个匹配标记的剧集。留空适用于所有剧集", + "ReleaseProfileTagSeriesHelpText": "发布配置将应用于至少有一个匹配标记的剧集。留空适用于所有剧集", "FormatShortTimeSpanHours": "{hours} 时", "RemotePathMappingHostHelpText": "与您为远程下载客户端指定的主机相同", "HideEpisodes": "隐藏集", "HistorySeason": "查看本季历史记录", "ImportScriptPathHelpText": "用于导入的脚本的路径", "IncludeCustomFormatWhenRenaming": "重命名时包含自定义格式", - "RemotePathMappingsInfo": "很少需要远程路径映射,如果{app}和您的下载客户端在同一系统上,最好匹配您的路径。有关详细信息,请参阅[wiki]({wikiLink})", - "IndexerPriorityHelpText": "索引器优先级从1(最高)到50(最低),默认25。当资源连接中断时寻找同等资源时使用,否则Sonarr将依旧使用已启用的索引器进行RSS同步并搜索", - "IndexerTagHelpText": "仅对至少有一个匹配标记的剧集使用此索引器。留空则适用于所有剧集。", - "InteractiveImportNoFilesFound": "在选中文件夹中找不到视频文件", - "RemoveSelectedBlocklistMessageText": "您确定要从阻止列表中删除所选项目吗?", - "InteractiveImportNoQuality": "必须为每个选中的文件选择质量", - "RestartSonarr": "重启Sonarr", + "RemotePathMappingsInfo": "很少需要远程路径映射,如果{appName}和您的下载客户端在同一系统上,则最好匹配您的路径。更多信息,请参阅[wiki]({wikiLink})", + "IndexerPriorityHelpText": "索引器优先级从1(最高)到50(最低),默认25。当资源连接中断时寻找同等资源时使用,否则{appName}将依旧使用已启用的索引器进行RSS同步并搜索", + "IndexerTagSeriesHelpText": "仅对至少有一个匹配标记的剧集使用此索引器。留空则适用于所有剧集。", + "InteractiveImportNoFilesFound": "在所选文件夹中找不到视频文件", + "RemoveSelectedBlocklistMessageText": "你确定你想从过滤清单中删除选中的项目吗?", + "InteractiveImportNoQuality": "必须为每个选定的文件选择质量", + "RestartSonarr": "重启{appName}", "InteractiveSearchModalHeaderSeason": "手动搜索 - {season}", "SearchMonitored": "搜索已监控", - "KeyboardShortcutsOpenModal": "打开该弹窗", - "SelectEpisodes": "选择集", + "KeyboardShortcutsOpenModal": "打开此模式", + "SelectEpisodes": "选择剧集", "SelectSeason": "选择季", - "LibraryImportTipsQualityInFilename": "确保您的文件在其文件名中包含质量。例如:`episode.s02e15.bluray.mkv`", - "LibraryImportTipsUseRootFolder": "将Sonarr指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExamp}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", + "LibraryImportTipsQualityInEpisodeFilename": "确保您的文件在其文件名中包含质量。例如:`episode.s02e15.bluray.mkv`", + "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExamp}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", "ListQualityProfileHelpText": "质量配置列表项将添加", "SeriesIndexFooterMissingMonitored": "缺失集(剧集被监控)", "SeriesIsMonitored": "剧集被监控", - "SearchForCutoffUnmet": "搜索所有未达截止条件的集", + "SearchForCutoffUnmetEpisodes": "搜索所有Cutoff Unmet的剧集", "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (总计: {totalEpisodeCount}, 下载中: {downloadingCount})", "SeriesTitleToExcludeHelpText": "要排除的剧集名称", "Umask750Description": "{octal} - 所有者写入,组读取", "SeriesTypesHelpText": "剧集类型用于重命名、解析和搜索", "MediaManagementSettingsLoadError": "无法加载媒体管理设置", "SourceRelativePath": "源相对路径", - "MetadataSourceSettingsSummary": "Sonarr从哪里获得剧集和集信息的总结", + "MetadataSourceSettingsSeriesSummary": "{appName}从哪里获得剧集和集信息的总结", "SslCertPassword": "SSL证书密码", - "UpdateUiNotWritableHealthCheckMessage": "无法安装更新,因为用户“{1}”无法写入 UI 文件夹“{0}”。", + "UpdateUiNotWritableHealthCheckMessage": "无法安装更新,因为用户“{userName}”无法写入 UI 文件夹“{uiFolder}”。", "MinimumCustomFormatScoreHelpText": "允许下载的最小自定义格式分数", "SslCertPasswordHelpText": "pfx文件密码", "UpgradeUntilCustomFormatScore": "升级直到自定义格式分数满足", "MonitorExistingEpisodes": "现有集", - "StopSelecting": "停止选中", + "StopSelecting": "停止选择", "MonitorFirstSeasonDescription": "监控第一季的所有集。所有其他季都将被忽略", - "SupportedCustomConditions": "Sonarr支持针对以下发布属性的自定义条件。", - "MonitorSpecials": "监控特别节目", - "SupportedDownloadClients": "Sonarr支持许多流行的torrent和usenet下载客户端。", - "UpgradeUntilCustomFormatScoreHelpText": "一旦达到此自定义格式分数,Sonarr 将不再抓取集的其他版本", + "SupportedCustomConditions": "{appName}支持针对以下发布属性的自定义条件。", + "MonitorSpecialEpisodes": "监控特别节目", + "SupportedDownloadClients": "{appName}支持许多流行的torrent和usenet下载客户端。", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "一旦达到此自定义格式分数,{appName} 将不再抓取集的其他版本", "MoveFiles": "移动文件", "SupportedImportListsMoreInfo": "若需要查看有关导入列表的详细信息,请点击“更多信息”按钮。", "MoveSeriesFoldersDontMoveFiles": "不,我自己移动文件", "SupportedIndexersMoreInfo": "若需要查看有关索引器的详细信息,请点击“更多信息”按钮。", "NoEpisodeInformation": "没有可用的集信息。", - "TableColumnsHelpText": "选择显示哪些列并排序", + "TableColumnsHelpText": "选择哪些列可见以及它们的显示顺序", "NoMinimumForAnyRuntime": "运行环境没有最小限制", "Theme": "主题", "UseSeasonFolderHelpText": "将集排序整理到季文件夹中", @@ -865,28 +864,27 @@ "VisitTheWikiForMoreDetails": "访问wiki获取更多详细信息: ", "AirsTbaOn": "时间待定,在 {networkLabel} 播出", "AirsTimeOn": "{time} 在 {networkLabel} 播出", - "NotificationStatusAllClientHealthCheckMessage": "由于故障所有通知都不可用", + "NotificationStatusAllClientHealthCheckMessage": "由于故障,所有通知都不可用", "Yesterday": "昨天", "OnGrab": "抓取中", "PendingChangesStayReview": "留下检查更改", - "SearchForCutoffUnmetConfirmationCount": "你确定要搜索所有共 {totalRecords} 个未达截止条件的集?", + "SearchForCutoffUnmetEpisodesConfirmationCount": "您确定要搜索所有{totalRecords}Cutoff Unmet的剧集吗?", "IncludeHealthWarnings": "包含健康度警告", "IndexerPriority": "索引器优先级", "IndexerOptionsLoadError": "无法加载索引器选项", "LastUsed": "上次使用", "MarkAsFailed": "标记为失败", "MediaManagementSettings": "媒体管理设置", - "MetadataSettingsSummary": "导入或刷新剧集时创建元数据文件", + "MetadataSettingsSeriesSummary": "导入或刷新剧集时创建元数据文件", "MetadataSourceSettings": "元数据源设置", - "Mixed": "混合的", - "MonitorLatestSeason": "最新季", + "Mixed": "混合", "MonitorFutureEpisodesDescription": "监控尚未播出的集", - "MonitorSpecialsDescription": "监控所有特别节目,而不更改其他集的监控状态", + "MonitorSpecialEpisodesDescription": "监控所有特别节目,而不更改其他集的监控状态", "OnSeriesDelete": "剧集删除时", "OnlyForBulkSeasonReleases": "仅适用于批量季发布", "OnlyTorrent": "只有torrent", "OnlyUsenet": "只有usenet", - "OrganizeNamingPattern": "命名规则: `{episodeFormat}`", + "OrganizeNamingPattern": "命名格式: \"{episodeFormat}\"", "Parse": "解析", "OrganizeSelectedSeriesModalHeader": "整理选定的剧集", "Original": "原始的", @@ -899,10 +897,10 @@ "QualitiesHelpText": "列表中的质量排序越高优先级也越高。同组内的质量优先级相同。质量只有选中才标记为需要", "QualityDefinitionsLoadError": "无法加载质量定义", "RemotePathMappingRemotePathHelpText": "下载客户端访问的目录的根路径", - "RemoveFilter": "移除过滤条件", + "RemoveFilter": "移除过滤器", "RestartNow": "马上重启", - "SetReleaseGroupModalTitle": "{modalTitle} - 设定发布组", - "Shutdown": "关闭", + "SetReleaseGroupModalTitle": "{modalTitle} - 设置发布组", + "Shutdown": "关机", "Style": "类型", "TableColumns": "列", "SupportedListsMoreInfo": "若需要查看有关列表的详细信息,请点击“更多信息”按钮。", @@ -910,68 +908,68 @@ "Today": "今天", "Titles": "标题", "TorrentDelay": "Torrent延时", - "ToggleUnmonitoredToMonitored": "未监控,单击可监控", + "ToggleUnmonitoredToMonitored": "未监控,单击进行监控", "Upcoming": "即将播出", - "ProgressBarProgress": "进度条位于 {progress}%", + "ProgressBarProgress": "进度栏位于{Progress}%", "Usenet": "Usenet", "Week": "周", "Standard": "标准", "Sunday": "星期日", "AirsDateAtTimeOn": "{date} {time} 在 {networkLabel} 播出", "AirsTomorrowOn": "明天 {time} 在 {networkLabel} 播出", - "Airs": "放送", - "CollapseAll": "收缩所有", + "Airs": "播出", + "CollapseAll": "全部收起", "CollapseMultipleEpisodes": "收缩多集", "CollapseMultipleEpisodesHelpText": "收缩同一天播出的多集", "Connection": "连接", "ContinuingSeriesDescription": "预计会有更多集/下一季", - "CountSelectedFile": "所中 {selectedCount} 个文件", - "CountSelectedFiles": "所中 {selectedCount} 个文件", + "CountSelectedFile": "{selectedCount} 选中的文件", + "CountSelectedFiles": "{selectedCount}选中的文件", "CountSeriesSelected": "所中 {count} 个剧集", - "DeleteEpisodesFiles": "删除 {episodeFileCount} 个集文件", + "DeleteEpisodesFiles": "删除{episodeFileCount}个剧集文件", "DeleteEpisodesFilesHelpText": "删除集文件和剧集文件夹", "DeleteSeriesFolder": "删除剧集文件夹", "DeleteSeriesFolderConfirmation": "剧集文件夹 `{path}` 及所含内容将会被删除。", "DeleteSeriesFolderCountWithFilesConfirmation": "你确定要删除选中的 {count} 个剧集及其所含的所有文件吗?", "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} 个集文件,总计 {size}", "DeleteSeriesFolders": "删除剧集文件夹", - "ExpandAll": "展开所有", + "ExpandAll": "展开全部", "False": "否", "FullColorEvents": "全彩事件", - "FullColorEventsHelpText": "更改样式为将整个事件填充为状态颜色,而不仅仅是左边缘。 不适用于议程", + "FullColorEventsHelpText": "改变样式,用状态颜色为整个事件着色,而不仅仅是左边缘。不适用于议程", "HourShorthand": "时", "ImportedTo": "导入到", "Importing": "导入中", - "IndexerDownloadClientHealthCheckMessage": "有无效下载客户端的索引器:{0}。", + "IndexerDownloadClientHealthCheckMessage": "有无效下载客户端的索引器:{indexerNames}。", "IndexersSettingsSummary": "索引器和索引器选项", "InteractiveImport": "手动导入", "InstanceNameHelpText": "选项卡及日志应用名称", "InteractiveImportLoadError": "无法加载手动导入项目", - "InteractiveImportNoEpisode": "必须为每个选中的文件选择一个或多个集", + "InteractiveImportNoEpisode": "必须为每个选定的文件选择一个或多个剧集", "InteractiveImportNoImportMode": "必须选择导入模式", - "InteractiveImportNoLanguage": "必须为每个选中的文件选择语言", + "InteractiveImportNoLanguage": "必须为每个选定的文件选择语言", "InteractiveImportNoSeries": "必须为每个选中的文件选择剧集", - "InteractiveSearchResultsFailedErrorMessage": "搜索失败,因为 {message}。请尝试刷新剧集信息,并验证是否存在必要信息,再尝试进行搜索。", + "InteractiveSearchResultsSeriesFailedErrorMessage": "搜索失败,因为 {message}。请尝试刷新剧集信息,并验证是否存在必要信息,再尝试进行搜索。", "InteractiveSearchSeason": "手动搜索本季所有集", - "KeyboardShortcutsConfirmModal": "接受确认弹窗", - "KeyboardShortcutsCloseModal": "关闭当前弹窗", - "KeyboardShortcutsFocusSearchBox": "聚焦搜索框", + "KeyboardShortcutsConfirmModal": "接受确认模式", + "KeyboardShortcutsCloseModal": "关闭当前模式", + "KeyboardShortcutsFocusSearchBox": "焦点搜索框", "Large": "大", "Level": "等级", - "LibraryImportHeader": "导入您已有的剧集", + "LibraryImportSeriesHeader": "导入您已有的剧集", "LibraryImportTips": "一些小提示以确保导入顺利进行:", "Links": "链接", "ListExclusionsLoadError": "无法加载排除列表", "ListOptionsLoadError": "无法加载列表选项", "ListWillRefreshEveryInterval": "列表将每隔 {refreshInterval} 刷新一次", "Local": "本地", - "LocalStorageIsNotSupported": "不支持或禁用本地存储。插件或无痕浏览可能已将其禁用。", + "LocalStorageIsNotSupported": "不支持或禁用本地存储。插件或私人浏览可能已将其禁用。", "ManualGrab": "手动抓取", "ManualImport": "手动导入", "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server)获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", - "MarkAsFailedConfirmation": "你确定要将 '{sourceTitle}' 标记为失败吗?", + "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", "Max": "最大的", "MaximumSingleEpisodeAgeHelpText": "在整季搜索期间,当该季的最后一集比此设置旧时,只允许获取整季包。仅限标准剧集。填写 0 可禁用此设置。", "MaximumSize": "最大文件体积", @@ -997,13 +995,13 @@ "Monitoring": "监控中", "MonitoringOptions": "监控选项", "MoreDetails": "更多详细信息", - "More": "更多的", + "More": "更多", "MoveAutomatically": "自动移动", "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{originalPath}' ?", "MoveSeriesFoldersMoveFiles": "是,移动文件", "MultiEpisode": "多集", "MultiEpisodeStyle": "多集风格", - "MultiLanguages": "多语言", + "MultiLanguages": "多种语言", "MultiEpisodeInvalidFormat": "多集:无效格式", "MustNotContain": "必须不包含", "MustContain": "必须包含", @@ -1013,7 +1011,7 @@ "Never": "永不", "NoChanges": "无修改", "NoDelay": "无延迟", - "NoEpisodeHistory": "没有集的历史记录", + "NoEpisodeHistory": "无片段历史记录", "NoEpisodesFoundForSelectedSeason": "未找到所选季的集", "NoEpisodesInThisSeason": "本季没有集", "NoHistory": "无历史记录", @@ -1028,7 +1026,7 @@ "NoSeriesFoundImportOrAdd": "找不到剧集,您需要导入现有剧集或添加新剧集以开始使用。", "NoTagsHaveBeenAddedYet": "未添加标签", "NoSeriesHaveBeenAdded": "您尚未添加任何剧集,是否要先导入部分或全部剧集?", - "NotificationsTagsHelpText": "剧集至少有一个标签匹配时发送通知", + "NotificationsTagsSeriesHelpText": "剧集至少有一个标签匹配时发送通知", "Ok": "完成", "OnEpisodeFileDelete": "集文件删除时", "OnEpisodeFileDeleteForUpgrade": "集文件因升级删除时", @@ -1036,21 +1034,21 @@ "OnRename": "重命名中", "OnSeriesAdd": "剧集添加时", "Or": "或", - "OrganizeModalHeaderSeason": "整理&重命名 - {season}", - "OrganizeNothingToRename": "成功了!任务已完成,没有文件重命名。", + "OrganizeModalHeaderSeason": "整理并重命名 - {season}", + "OrganizeNothingToRename": "重命名成功!已没有需要重命名的文件。", "OrganizeRelativePaths": "所有路径都相对于: `{path}`", - "OrganizeRenamingDisabled": "重命名已关闭,没有文件重新命名", + "OrganizeRenamingDisabled": "重命名已禁用,无需重命名", "OrganizeSelectedSeriesModalAlert": "提示:要预览重命名,请点击“取消”,然后点击任何剧集标题并使用此图标:", "OrganizeSelectedSeriesModalConfirmation": "你确定要整理 {count} 个选定剧集中的所有文件?", "OverrideAndAddToDownloadQueue": "覆盖并添加到下载队列", "OverrideGrabModalTitle": "覆盖并抓取 - {title}", - "OverrideGrabNoEpisode": "必须至少选择一集", - "OverrideGrabNoLanguage": "必须至少选择一种语言", + "OverrideGrabNoEpisode": "请选择至少一集", + "OverrideGrabNoLanguage": "请选择至少一种语言", "OverrideGrabNoQuality": "必须选择质量", "OverrideGrabNoSeries": "必须选择剧集", - "Overview": "概述", - "OverviewOptions": "概述选项", - "ParseModalHelpText": "在上面的输入框中输入发布标题", + "Overview": "概览", + "OverviewOptions": "概览选项", + "ParseModalHelpText": "在上面的输入框中输入一个发行版标题", "ParseModalErrorParsing": "解析错误,请重试。", "ParseModalUnableToParse": "无法解析提供的标题,请重试。", "Paused": "暂停", @@ -1061,7 +1059,7 @@ "Port": "端口", "PortNumber": "端口号", "PosterOptions": "海报选项", - "PosterSize": "海报尺寸", + "PosterSize": "海报大小", "PreferProtocol": "首选 {preferredProtocol}", "PreferAndUpgrade": "首选并升级", "PreferTorrent": "首选Torrent", @@ -1069,8 +1067,8 @@ "Preferred": "首选的", "Presets": "预设", "PreferredSize": "首选影片大小", - "PreviewRename": "预览重命名", - "PreviewRenameSeason": "预览此季重命名", + "PreviewRename": "重命名预览", + "PreviewRenameSeason": "重命名此季的预览", "PreviousAiringDate": "上一次播出: {date}", "PrioritySettings": "优先级: {priority}", "ProfilesSettingsSummary": "质量、元数据、延迟、发行配置", @@ -1084,27 +1082,27 @@ "Reason": "原因", "RecyclingBin": "回收站", "RecyclingBinCleanupHelpTextWarning": "回收站中的文件在超出选择的天数后会被自动清理", - "RefreshAndScan": "刷新&扫描", + "RefreshAndScan": "刷新并扫描", "RefreshAndScanTooltip": "刷新信息并扫描磁盘", "ReleaseProfileIndexerHelpText": "指定配置文件应用于哪个索引器", "ReleaseProfileIndexerHelpTextWarning": "使用有发布配置的特定索引器可能会导致重复获取发布", - "ReleaseRejected": "版本被拒绝", + "ReleaseRejected": "发布被拒绝", "ReleaseSceneIndicatorAssumingTvdb": "推测TVDB编号。", "ReleaseSceneIndicatorMappedNotRequested": "此搜索中未请求映射的剧集。", - "ReleaseSceneIndicatorSourceMessage": "{message} 发布时编号不明确,无法准确地识别集。", - "ReleaseSceneIndicatorUnknownSeries": "未知的集或剧集。", - "RemotePathMappingLocalPathHelpText": "Sonarr用于访问远程路径的本地路径", + "ReleaseSceneIndicatorSourceMessage": "{message}的版本中存在模糊的编号,无法正确地识别剧集。", + "ReleaseSceneIndicatorUnknownSeries": "未知的剧集或系列。", + "RemotePathMappingLocalPathHelpText": "{appName}用于访问远程路径的本地路径", "RemoveFromBlocklist": "从黑名单中移除", "RemoveCompletedDownloadsHelpText": "从下载客户端记录中移除已导入的下载", "RemoveFromQueue": "从队列中移除", - "RemoveQueueItem": "移除 - {sourceTitle}", + "RemoveQueueItem": "删除- {sourceTitle}", "RemoveTagsAutomatically": "自动删除标签", "Repeat": "重复", "ResetDefinitions": "重置定义", - "RescanAfterRefreshHelpText": "刷新剧集信息后重新扫描剧集文件夹", + "RescanAfterRefreshSeriesHelpText": "刷新剧集信息后重新扫描剧集文件夹", "ResetAPIKey": "重置API Key", "RestartRequiredHelpTextWarning": "需重启以生效", - "RestartRequiredWindowsService": "根据运行Sonarr的用户,在服务自动启动之前,您可能需要以管理员身份重新启动Sonarr一次。", + "RestartRequiredWindowsService": "根据运行{appName}的用户,在服务自动启动之前,您可能需要以管理员身份重新启动{appName}一次。", "RootFolderSelectFreeSpace": "{freeSpace} 空闲", "RootFolders": "根目录", "RootFoldersLoadError": "无法加载根目录", @@ -1116,19 +1114,19 @@ "SceneInfo": "场景信息", "Search": "搜索", "SearchFailedError": "搜索失败,请稍后重试。", - "SearchForMonitoredEpisodesSeason": "搜索本季监控的集", + "SearchForMonitoredEpisodesSeason": "搜索所有监控的剧集", "Season": "季", "SeasonDetails": "季详情", "SeasonFinale": "季完结", "SeasonInformation": "季信息", "SeasonNumberToken": "季 {seasonNumber}", - "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} 集已下载", - "SeasonPassTruncated": "只显示最新的25季,查看详情获取所有季信息", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} 的剧集被下载", + "SeasonPassTruncated": "只显示最新的25季,点击详情查看所有的季", "SeasonPremieresOnly": "仅限季首播", "SelectDropdown": "选择...", - "SelectEpisodesModalTitle": "{modalTitle} - 选择集", + "SelectEpisodesModalTitle": "{modalTitle} - 选择剧集", "SelectFolderModalTitle": "{modalTitle} - 选择文件夹", - "SelectQuality": "选择质量", + "SelectQuality": "选择品质", "SelectReleaseGroup": "选择发布组", "SeriesDetailsGoTo": "转到 {title}", "SeriesDetailsNoEpisodeFiles": "没有集文件", @@ -1141,27 +1139,27 @@ "SeriesMonitoring": "剧集监控中", "SeriesPremiere": "剧集首播", "ShortDateFormat": "短日期格式", - "ShowEpisodes": "显示集", + "ShowEpisodes": "显示剧集", "ShowMonitored": "显示监控中的", "ShowMonitoredHelpText": "在海报下显示监控状态", - "ShowNetwork": "显示电视网", + "ShowNetwork": "显示网络", "ShowPreviousAiring": "显示上一次播出", "ShowQualityProfileHelpText": "在海报下方显示媒体质量配置", - "ShowQualityProfile": "显示媒体质量配置", - "ShowSearch": "显示搜索按钮", - "ShowSearchHelpText": "在选项中显示搜索框", - "ShowSeasonCount": "显示季计数", + "ShowQualityProfile": "显示质量配置文件", + "ShowSearch": "显示搜索", + "ShowSearchHelpText": "悬停时显示搜索按钮", + "ShowSeasonCount": "显示季数", "ShowTitle": "显示标题", - "ShowTitleHelpText": "在海报下显示剧集标题", + "ShowSeriesTitleHelpText": "在海报下显示剧集标题", "ShowUnknownSeriesItems": "实现未知剧集项目", - "ShowUnknownSeriesItemsHelpText": "显示队列中没有剧集的项目,这可能包括已删除的剧集、电影或 Sonarr 类别中的任何其他内容", - "SkipFreeSpaceCheckWhenImportingHelpText": "当 Sonarr 无法从您的剧集根文件夹中检测到空闲空间时使用", + "ShowUnknownSeriesItemsHelpText": "显示队列中没有剧集的项目,这可能包括已删除的剧集、电影或 {appName} 类别中的任何其他内容", + "SkipFreeSpaceCheckWhenImportingHelpText": "在文件导入期间,当{appName}无法检测根文件夹的空闲空间时使用", "Small": "小", "SingleEpisodeInvalidFormat": "单集:非法格式", "SkipFreeSpaceCheck": "跳过剩余空间检查", "Space": "空间", "SpecialEpisode": "特别集", - "StandardTypeDescription": "以SxxEyy模式发布的集", + "StandardEpisodeTypeDescription": "以SxxEyy模式发布的集", "StartImport": "开始导入", "StartProcessing": "开始处理", "Table": "表格", @@ -1169,36 +1167,36 @@ "TablePageSizeHelpText": "每页显示的项目数", "TagDetails": "标签详情 - {label}", "TagsSettingsSummary": "显示全部标签和标签使用情况,可删除未使用的标签", - "Tba": "待定", + "Tba": "待宣布", "Test": "测试", "ThemeHelpText": "改变应用界面主题,选择“自动”主题会通过操作系统主题来自适应白天黑夜模式。(受Theme.Park启发)", "TimeFormat": "时间格式", "ToggleMonitoredSeriesUnmonitored ": "当系列不受监控时,无法切换监控状态", - "ToggleMonitoredToUnmonitored": "已监控,单击可取消监控", + "ToggleMonitoredToUnmonitored": "已监视,单击可取消监视", "Total": "全部的", "TorrentsDisabled": "Torrents关闭", "TvdbIdExcludeHelpText": "要排除的剧集 TVDB ID", "TvdbId": "TVDB ID", "TypeOfList": "{typeOfList} 列表", - "UiLanguageHelpText": "Sonarr将用于UI的语言", + "UiLanguageHelpText": "{appName}将用于UI的语言", "UiSettings": "UI设置", "UiLanguage": "UI界面语言", "Umask770Description": "{octal} - 所有者和组写入", "Umask775Description": "{octal} - 所有者和组写入,其他读取", - "Umask777Description": "{octal} - 全部可以写入", + "Umask777Description": "{octal} - 每个人都写", "UnableToLoadAutoTagging": "无法加载自动标记", "Unavailable": "不可用", "Ungroup": "未分组", "Unknown": "未知", "Unlimited": "无限制", - "UnmappedFilesOnly": "仅未映射的文件", + "UnmappedFilesOnly": "仅限未映射的文件", "UnmonitorDeletedEpisodes": "取消监控已删除的集", - "UnmonitorSpecialsDescription": "取消监控所有特别节目而不改变其他集的监控状态", - "UnmonitorDeletedEpisodesHelpText": "从磁盘删除的集将在 Sonarr 中自动取消监控", - "UnmonitorSpecials": "取消监控特别节目", - "UpdateAll": "全部更新", + "UnmonitorSpecialsEpisodesDescription": "取消监控所有特别节目而不改变其他集的监控状态", + "UnmonitorDeletedEpisodesHelpText": "从磁盘删除的集将在 {appName} 中自动取消监控", + "UnmonitorSpecialEpisodes": "取消监控特别节目", + "UpdateAll": "更新全部", "UpdateAutomaticallyHelpText": "自动下载并安装更新。你还可以在“系统:更新”中安装", - "UpdateSelected": "更新已选", + "UpdateSelected": "更新选择的内容", "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到影片质量超出或者满足", "UpgradesAllowed": "允许升级", "UpgradesAllowedHelpText": "如关闭,则质量不做升级", @@ -1221,7 +1219,7 @@ "ReleaseSceneIndicatorAssumingScene": "推测场景编号。", "ReleaseSceneIndicatorUnknownMessage": "这一集的编号各不相同,版本与任何已知的映射都不匹配。", "RemoveFailedDownloadsHelpText": "从下载客户端中删除已失败的下载", - "RemoveTagsAutomaticallyHelpText": "如果不满足条件,则自动删除标签", + "RemoveTagsAutomaticallyHelpText": "如果条件不满足,则自动移除标签", "ResetAPIKeyMessageText": "您确定要重置您的 API 密钥吗?", "Rss": "RSS", "SceneInformation": "场景信息", @@ -1241,18 +1239,18 @@ "UiSettingsLoadError": "无法加载UI设置", "UnknownEventTooltip": "未知事件", "UpdateScriptPathHelpText": "自定义脚本的路径,该脚本处理获取的更新包并处理更新过程的其余部分", - "UpdateSonarrDirectlyLoadError": "无法直接更新Sonarr,", - "View": "视图", + "UpdateSonarrDirectlyLoadError": "无法直接更新{appName},", + "View": "查看", "Negate": "相反的", "ListTagsHelpText": "从此列表导入时将添加标记", "ManageEpisodesSeason": "管理本季的集文件", "ManualImportItemsLoadError": "无法加载手动导入项目", - "MassSearchCancelWarning": "一旦启动,如果不重启Sonarr或禁用所有索引器,就无法取消此操作。", + "MassSearchCancelWarning": "一旦启动,如果不重启{appName}或禁用所有索引器,就无法取消此操作。", "ListsLoadError": "无法加载列表", "LocalAirDate": "当地播出日期", "LocalPath": "本地路径", "LogLevel": "日志等级", - "OpenBrowserOnStartHelpText": " 在应用程序启动时,打开浏览器并导航到Sonarr主页。", + "OpenBrowserOnStartHelpText": " 在应用程序启动时,打开浏览器并导航到{appName}主页。", "OpenBrowserOnStart": "启动时打开浏览器", "Period": "时期", "PrefixedRange": "前缀范围", @@ -1262,7 +1260,7 @@ "OpenSeries": "打开剧集", "OptionalName": "可选名称", "Organize": "整理", - "OrganizeLoadError": "读取预告片错误", + "OrganizeLoadError": "载入预览时出错", "FormatAgeHour": "小时", "FormatAgeHours": "小时", "FormatAgeMinute": "分钟", @@ -1284,14 +1282,14 @@ "InteractiveSearch": "手动搜索", "InteractiveSearchModalHeader": "手动搜索", "InvalidFormat": "格式不合法", - "InvalidUILanguage": "您的UI设置为无效语言,请更正并保存您的设置", - "KeyboardShortcuts": "键盘快捷键", + "InvalidUILanguage": "您的UI设置的语言无效,请纠正它并保存设置", + "KeyboardShortcuts": "快捷键", "KeyboardShortcutsSaveSettings": "保存设置", "ListRootFolderHelpText": "根目录文件夹列表项需添加", - "Logout": "登出", + "Logout": "注销", "ManageEpisodes": "管理集", "MaximumSizeHelpText": "抓取影片最大多少MB,设置为0则不限制", - "MetadataProvidedBy": "元数据由 {provider} 提供", + "MetadataProvidedBy": "元数据由{provider}提供", "MidseasonFinale": "季中完结", "MinimumFreeSpaceHelpText": "如果导入的磁盘空间不足,则禁止导入", "MinimumLimits": "最小限制", @@ -1299,19 +1297,19 @@ "MissingNoItems": "没有缺失项目", "MonitorExistingEpisodesDescription": "监控有文件或尚未播出的集", "MonitorFirstSeason": "第一季", - "MonitorNone": "无", - "MonitorNoneDescription": "没有集被监控", + "MonitorNoEpisodes": "无", + "MonitorNoEpisodesDescription": "没有集被监控", "MonitorPilotEpisode": "试播集", "MonitorSelected": "监控选中的", "MonitorSeries": "监控剧集", - "MonitoredHelpText": "下载本剧集中监控的集", + "MonitoredEpisodesHelpText": "下载本剧集中监控的集", "Month": "月", "MoveSeriesFoldersToRootFolder": "是否将剧集文件夹移动到 '{destinationRootFolder}' ?", "MustNotContainHelpText": "如版本包含一个或多个条件则丢弃(无视大小写)", "MyComputer": "我的电脑", "NamingSettings": "命名设置", - "NoEpisodeOverview": "没有集摘要", - "NotificationStatusSingleClientHealthCheckMessage": "由于故障通知不可用: {0}", + "NoEpisodeOverview": "无片段概述", + "NotificationStatusSingleClientHealthCheckMessage": "由于失败导致通知不可用:{notificationNames}", "NotificationTriggers": "通知触发器", "NotificationsLoadError": "无法加载通知连接", "OnApplicationUpdate": "程序更新时", @@ -1320,10 +1318,10 @@ "OnImport": "导入中", "Password": "密码", "PendingDownloadClientUnavailable": "挂起 - 下载客户端不可用", - "QueueLoadError": "读取队列失败", + "QueueLoadError": "加载队列失败", "QuickSearch": "快速搜索", - "ReleaseProfiles": "发行配置文件", - "ReleaseProfilesLoadError": "无法加载发行配置文件", + "ReleaseProfiles": "发行版概要", + "ReleaseProfilesLoadError": "无法加载发行版概要", "RemotePath": "远程路径", "RemoveDownloadsAlert": "移除设置被移至上表中的单个下载客户端设置。", "RemoveRootFolder": "移除根目录", @@ -1332,7 +1330,7 @@ "RestartLater": "稍后重启", "RestrictionsLoadError": "无法加载限制条件", "RssSync": "RSS同步", - "SearchForAllMissingConfirmationCount": "你确定要搜索所有共 {totalRecords} 缺失的集?", + "SearchForAllMissingEpisodesConfirmationCount": "您确定要搜索所有{totalRecords}缺失的剧集吗?", "SearchIsNotSupportedWithThisIndexer": "该索引器不支持搜索", "Security": "安全", "SelectFolder": "选择文件夹", @@ -1341,55 +1339,54 @@ "SeriesType": "剧集类型", "SeriesTypes": "剧集类型", "SetPermissions": "设定权限", - "SetReleaseGroup": "设定发布组", + "SetReleaseGroup": "设置发布组", "SomeResultsAreHiddenByTheAppliedFilter": "部分结果已被过滤隐藏", "Sort": "排序", "Specials": "特别节目", - "SpecialsFolderFormat": "特别节目文件夹格式", + "SpecialsFolderFormat": "特殊季文件夹格式", "SslCertPath": "SSL证书路径", "SslCertPathHelpText": "pfx文件路径", - "SupportedAutoTaggingProperties": "Sonarr支持自动标记规则的以下属性", + "SupportedAutoTaggingProperties": "{appName}支持自动标记规则的以下属性", "SupportedDownloadClientsMoreInfo": "若需要查看有关下载客户端的详细信息,请点击“更多信息”按钮。", - "SupportedIndexers": "Sonarr支持任何使用Newznab标准的索引器,以及下面列出的其他索引器。", - "SupportedLists": "Sonarr支持将多个列表中的剧集导入数据库。", + "SupportedIndexers": "{appName}支持任何使用Newznab标准的索引器,以及下面列出的其他索引器。", + "SupportedListsSeries": "{appName}支持将多个列表中的剧集导入数据库。", "TableOptionsButton": "表格选项按钮", "TheTvdb": "TheTVDB", "Tomorrow": "明天", "Ui": "UI", - "Umask755Description": "{octal} - 所有者写入,其他人读取", + "Umask755Description": "{octal} - 所有者写,其他人读", "Umask": "掩码", "True": "是", "UpgradeUntil": "升级直至", - "UpgradeUntilHelpText": "一旦达到此质量,Sonarr 将不再下载集的其他版本", + "UpgradeUntilEpisodeHelpText": "一旦达到此质量,{appName} 将不再下载集的其他版本", "UrlBaseHelpText": "对于反向代理支持,默认为空", "UseHardlinksInsteadOfCopy": "使用硬链接代替复制", "UsenetDelay": "Usenet延时", "UsenetDelayHelpText": "延迟几分钟才能等待从Usenet获取发布", - "OrganizeModalHeader": "整理&重命名", + "OrganizeModalHeader": "整理并重命名", "RssSyncIntervalHelpText": "间隔时间以分钟为单位,设置为0则关闭该功能(会停止所有剧集的自动抓取下载)", "RssSyncIntervalHelpTextWarning": "这将适用于所有索引器,请遵循他们所制定的规则", "SceneNumberNotVerified": "场景编号未确认", "RssSyncInterval": "RSS同步间隔", "SearchAll": "搜索全部", "AgeWhenGrabbed": "年龄(在被抓取后)", - "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "跳过首选协议延迟所需的最低自定义格式分数", - "BypassDelayIfAboveCustomFormatScoreHelpText": "当抓取发布版本的分数高于配置的最低自定义格式分数时跳过延时", - "SearchForAllMissing": "搜索所有缺失的集", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "绕过首选协议延迟所需的最小自定义格式分数", + "BypassDelayIfAboveCustomFormatScoreHelpText": "当发布版本的评分高于配置的最小自定义格式评分时,跳过延时", + "SearchForAllMissingEpisodes": "搜索所有缺失的剧集", "SearchForMissing": "搜索缺少", - "SearchForQuery": "搜索 {query}", + "SearchForQuery": "搜索{query}", "ImportUsingScriptHelpText": "使用脚本复制文件以进行导入(例如用于转码)", "LanguagesLoadError": "无法加载语言", - "MonitorLatestSeasonDescription": "监控过去90天内播出的最新一季的所有集以及未来的所有集", "MonitorMissingEpisodes": "缺少集", "OneMinute": "1分钟", "OnUpgrade": "升级中", "RemotePathMappings": "远程路径映射", "RemotePathMappingsLoadError": "无法加载远程路径映射", - "RemoveQueueItemConfirmation": "你确定要从队列中移除 '{sourceTitle}' 吗?", + "RemoveQueueItemConfirmation": "您确定要从队列中删除'{sourceTitle}'吗?", "RequiredHelpText": "此 {implementationName} 条件必须匹配才能应用自定义格式。 否则,单个 {implementationName} 匹配就足够了。", "RescanSeriesFolderAfterRefresh": "刷新后重新扫描剧集文件夹", - "RestartRequiredToApplyChanges": "Sonarr需要重新启动才能应用更改,您想现在重新启动吗?", - "RescanAfterRefreshHelpTextWarning": "当没有设置为“总是”时,Sonarr将不会自动检测文件的更改", + "RestartRequiredToApplyChanges": "{appName}需要重新启动才能应用更改,您想现在重新启动吗?", + "RescanAfterRefreshHelpTextWarning": "当没有设置为“总是”时,{appName}将不会自动检测文件的更改", "Retention": "保留", "RetentionHelpText": "仅限Usenet:设置为零以设置无限保留", "RetryingDownloadOn": "于 {date} {time} 重试下载", @@ -1403,7 +1400,7 @@ "SeriesFolderImportedTooltip": "从剧集文件夹导入的集", "Socks4": "Socks4", "Socks5": "Socks5 (支持TOR)", - "SonarrTags": "Sonarr标签", + "SonarrTags": "{appName}标签", "SourcePath": "来源路径", "SmartReplaceHint": "短划线或空格短划线取决于名称", "TagIsNotUsedAndCanBeDeleted": "标签未被使用,可删除", @@ -1415,7 +1412,7 @@ "TorrentDelayHelpText": "延迟几分钟等待获取torrent", "Underscore": "下划线", "SelectAll": "选择全部", - "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载器", + "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载客户端", "WantMoreControlAddACustomFormat": "想要更好地控制首选下载吗?添加[自定义格式](/settings/customformats)", "WeekColumnHeader": "日期格式", "WaitingToImport": "等待导入", @@ -1423,52 +1420,51 @@ "SendAnonymousUsageData": "发送匿名使用数据", "UnmonitorSelected": "取消监控选中的", "UnsavedChanges": "未保存更改", - "UnselectAll": "全不选", + "UnselectAll": "取消选择全部", "UpcomingSeriesDescription": "剧集已宣布,但尚未确定具体的播出日期", - "UpdateFiltered": "更新过滤", - "UpdateMonitoring": "更新监控中的", + "UpdateFiltered": "更新已过滤的内容", + "UpdateMonitoring": "更新监控的内容", "IndexerSettings": "索引器设置", "LogLevelTraceHelpTextWarning": "追踪日志只应该暂时启用", "Logging": "日志记录中", "LongDateFormat": "长时间格式", "Lowercase": "小写字母", "ProxyType": "代理类型", - "QualityLimitsHelpText": "根据剧集运行时间和文件中的集数自动调整限制。", + "QualityLimitsSeriesRuntimeHelpText": "根据剧集运行时间和文件中的集数自动调整限制。", "DeleteSeriesFoldersHelpText": "删除剧集文件夹及其所含文件", - "EpisodeTitleRequiredHelpText": "如果集标题为命名格式,并且集标题为「待定」,则 48 小时内禁用导入", + "EpisodeTitleRequiredHelpText": "如果单集标题为命名格式且单集标题为「待定」,则 在48 小时内禁用导入", "LibraryImportTipsDontUseDownloadsFolder": "不要使用该方法从下载客户端导入影片,本方法只限于导入现有的已整理的库,不能导入未整理的文件。", "MinimumFreeSpace": "最小剩余空间", "Monday": "星期一", "Monitor": "是否监控", "NotificationTriggersHelpText": "选择触发此通知的事件", - "ImportListsSettingsSummary": "从另一个Sonarr或Trakt列表导入并管理排除列表", - "ParseModalHelpTextDetails": "Sonarr将尝试解析标题并向您展示有关它的详细信息", + "ImportListsSettingsSummary": "从另一个{appName}或Trakt列表导入并管理排除列表", + "ParseModalHelpTextDetails": "{appName}将尝试解析标题并向显示详细信息", "Proxy": "代理", "ImportScriptPath": "导入脚本路径", "IncludeCustomFormatWhenRenamingHelpText": "在 {Custom Formats} 中包含重命名格式", - "ShowSizeOnDisk": "显示占用磁盘体积", + "ShowSizeOnDisk": "显示已用空间", "SingleEpisode": "单集", "SmartReplace": "智能替换", "ShowBanners": "显示横幅", - "ShowBannersHelpText": "显示横幅而不是名称", - "ShowDateAdded": "显示添加日期", + "ShowBannersHelpText": "显示横幅而不是标题", + "ShowDateAdded": "显示加入时间", "ShowEpisodeInformation": "显示集信息", "ShowEpisodeInformationHelpText": "显示集号和标题", "ShowPath": "显示路径", - "QualityProfileInUse": "无法删除已指定给剧集、列表、收藏的质量配置", + "QualityProfileInUseSeriesListCollection": "无法删除已指定给剧集、列表、收藏的质量配置", "QualityProfiles": "媒体质量配置", "QualityProfilesLoadError": "无法加载质量配置文件", "RecentChanges": "最近修改", "RecyclingBinCleanupHelpText": "设置为0关闭自动清理", "RecyclingBinHelpText": "集文件将在删除时移动到此处,而不是永久删除", - "RedownloadFailed": "重新下载失败", "RegularExpressionsCanBeTested": "正则表达式可在[此处](http://regexstorm.net/tester)测试。", "SslPort": "SSL端口", "TablePageSizeMinimum": "页面大小必须至少为 {minimumValue}", "TorrentDelayTime": "Torrent延时:{torrentDelay}", "Torrents": "种子", - "TotalFileSize": "总文件体积", - "TotalRecords": "总记录数:{totalRecords}", + "TotalFileSize": "文件总大小", + "TotalRecords": "记录总数: {totalRecords}", "Trace": "追踪", "CutoffUnmetLoadError": "加载未达截止条件项目错误", "CutoffUnmetNoItems": "没有未达截止条件的项目", @@ -1480,8 +1476,15 @@ "EditSeriesModalHeader": "编辑 - {title}", "DetailedProgressBar": "详细的进度条", "EndedSeriesDescription": "剧集已完结,预计不会有其他集或季", - "EpisodeFilesLoadError": "无法加载集文件", + "EpisodeFilesLoadError": "无法加载剧集文件", "Files": "文件", "SeriesDetailsOneEpisodeFile": "1个集文件", - "UrlBase": "基本URL" + "UrlBase": "基本URL", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "下载客户端{downloadClientName}设置为删除已完成的下载。这可能导致在{appName}可以导入下载之前从您的客户端删除下载。", + "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", + "AutoRedownloadFailed": "重新下载失败", + "AutoRedownloadFailedFromInteractiveSearch": "从交互式搜索中重新下载失败", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "当从交互式搜索中获取失败的版本时,自动搜索并尝试下载其他版本", + "ImportListSearchForMissingEpisodes": "搜索缺失集", + "QueueFilterHasNoItems": "选定的队列过滤器没有项目" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index e075c19dd..3ec1abed9 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -1,6 +1,6 @@ { "BlocklistRelease": "封鎖清單版本", "BlocklistReleases": "封鎖清單版本", - "ApiKeyValidationHealthCheckMessage": "請將您的API金鑰更新為至少{0}個字元長。您可以通過設定或配置文件進行此操作。", + "ApiKeyValidationHealthCheckMessage": "請將您的API金鑰更新為至少{length}個字元長。您可以通過設定或配置文件進行此操作。", "AppDataLocationHealthCheckMessage": "為了避免在更新過程中刪除AppData,將無法進行更新。" } diff --git a/src/NzbDrone.Core/Localization/LocalizationService.cs b/src/NzbDrone.Core/Localization/LocalizationService.cs index d0c5c8eb7..9e8df41f3 100644 --- a/src/NzbDrone.Core/Localization/LocalizationService.cs +++ b/src/NzbDrone.Core/Localization/LocalizationService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using NLog; using NzbDrone.Common.Cache; @@ -18,14 +19,17 @@ namespace NzbDrone.Core.Localization public interface ILocalizationService { Dictionary GetLocalizationDictionary(); + string GetLocalizedString(string phrase); - string GetLocalizedString(string phrase, string language); + string GetLocalizedString(string phrase, Dictionary tokens); string GetLanguageIdentifier(); } public class LocalizationService : ILocalizationService, IHandleAsync { private const string DefaultCulture = "en"; + private static readonly Regex TokenRegex = new Regex(@"(?:\{)(?[a-z0-9]+)(?:\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private readonly ICached> _cache; @@ -53,23 +57,18 @@ namespace NzbDrone.Core.Localization public string GetLocalizedString(string phrase) { - var language = GetLanguageFileName(); - - return GetLocalizedString(phrase, language); + return GetLocalizedString(phrase, new Dictionary()); } - public string GetLocalizedString(string phrase, string language) + public string GetLocalizedString(string phrase, Dictionary tokens) { + var language = GetLanguageFileName(); + if (string.IsNullOrEmpty(phrase)) { throw new ArgumentNullException(nameof(phrase)); } - if (language.IsNullOrWhiteSpace()) - { - language = GetLanguageFileName(); - } - if (language == null) { language = DefaultCulture; @@ -79,7 +78,7 @@ namespace NzbDrone.Core.Localization if (dictionary.TryGetValue(phrase, out var value)) { - return value; + return ReplaceTokens(value, tokens); } return phrase; @@ -98,6 +97,20 @@ namespace NzbDrone.Core.Localization return language; } + private string ReplaceTokens(string input, Dictionary tokens) + { + tokens.TryAdd("appName", "Sonarr"); + + return TokenRegex.Replace(input, (match) => + { + var tokenName = match.Groups["token"].Value; + + tokens.TryGetValue(tokenName, out var token); + + return token?.ToString() ?? $"{{{tokenName}}}"; + }); + } + private string GetLanguageFileName() { return GetLanguageIdentifier().Replace("-", "_").ToLowerInvariant(); diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs b/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs index 8f6ed8c9a..fa489171b 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; @@ -12,7 +14,7 @@ namespace NzbDrone.Core.MediaCover string RegisterUrl(string url); string GetUrl(string hash); - byte[] GetImage(string hash); + Task GetImage(string hash); } public class MediaCoverProxy : IMediaCoverProxy @@ -30,6 +32,11 @@ namespace NzbDrone.Core.MediaCover public string RegisterUrl(string url) { + if (url.IsNullOrWhiteSpace()) + { + return null; + } + var hash = url.SHA256Hash(); _cache.Set(hash, url, TimeSpan.FromHours(24)); @@ -52,13 +59,14 @@ namespace NzbDrone.Core.MediaCover return result; } - public byte[] GetImage(string hash) + public async Task GetImage(string hash) { var url = GetUrl(hash); var request = new HttpRequest(url); + var response = await _httpClient.GetAsync(request); - return _httpClient.Get(request).ResponseData; + return response.ResponseData; } } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 589a5c1c4..4c92b0d93 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -121,7 +121,7 @@ namespace NzbDrone.Core.MediaFiles } CleanMediaFiles(series, new List()); - CompletedScanning(series); + CompletedScanning(series, new List()); return; } @@ -174,8 +174,11 @@ 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); - CompletedScanning(series); + CompletedScanning(series, possibleExtraFiles); } private void CleanMediaFiles(Series series, List mediaFileList) @@ -184,10 +187,10 @@ namespace NzbDrone.Core.MediaFiles _mediaFileTableCleanupService.Clean(series, mediaFileList); } - private void CompletedScanning(Series series) + private void CompletedScanning(Series series, List possibleExtraFiles) { _logger.Info("Completed scanning disk for {0}", series.Title); - _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); + _eventAggregator.PublishEvent(new SeriesScannedEvent(series, possibleExtraFiles)); } public string[] GetVideoFiles(string path, bool allDirectories = true) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 3ea8a9fc1..eac485671 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -130,6 +130,7 @@ namespace NzbDrone.Core.MediaFiles try { MoveEpisodeFile(episodeFile, series, episodeFile.Episodes); + localEpisode.FileRenamedAfterScriptImport = true; } catch (SameFilenameException) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index e2981c748..b7c225eb2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -67,7 +67,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return DetectSampleResult.Sample; } - _logger.Debug("Runtime is over 90 seconds"); + _logger.Debug("[{0}] does not appear to be a sample. Runtime {1} seconds is more than minimum of {2} seconds", path, runTime, minimumRuntime); return DetectSampleResult.NotSample; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a77cc8957..5d4bb77f6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IUpgradeMediaFiles _episodeFileUpgrader; private readonly IMediaFileService _mediaFileService; private readonly IExtraService _extraService; + private readonly IExistingExtraFiles _existingExtraFiles; private readonly IDiskProvider _diskProvider; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; @@ -34,6 +35,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, IMediaFileService mediaFileService, IExtraService extraService, + IExistingExtraFiles existingExtraFiles, IDiskProvider diskProvider, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, @@ -42,6 +44,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _episodeFileUpgrader = episodeFileUpgrader; _mediaFileService = mediaFileService; _extraService = extraService; + _existingExtraFiles = existingExtraFiles; _diskProvider = diskProvider; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; @@ -130,7 +133,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { - _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); + if (localEpisode.ScriptImported) + { + _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles); + + if (localEpisode.FileRenamedAfterScriptImport) + { + _extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile); + } + } + + if (!localEpisode.ScriptImported || localEpisode.ShouldImportExtras) + { + _extraService.ImportEpisode(localEpisode, episodeFile, copyOnly); + } } _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, oldFiles, newDownload, downloadClientItem)); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs index 891cef89a..e7a62650b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs @@ -67,6 +67,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime()); } } + else + { + _logger.Debug("Episode file previously imported at {0}", lastImported.Date); + return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime()); + } } return Decision.Accept(); diff --git a/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs index e07bbd75f..a76348b39 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/SeriesScannedEvent.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common.Messaging; +using System.Collections.Generic; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.Events @@ -6,10 +7,12 @@ namespace NzbDrone.Core.MediaFiles.Events public class SeriesScannedEvent : IEvent { public Series Series { get; private set; } + public List PossibleExtraFiles { get; set; } - public SeriesScannedEvent(Series series) + public SeriesScannedEvent(Series series, List possibleExtraFiles) { Series = series; + PossibleExtraFiles = possibleExtraFiles; } } } diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs index c626d5b44..45ab383d7 100644 --- a/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportDecider.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IProcessProvider _processProvider; private readonly IConfigService _configService; private readonly ITagRepository _tagRepository; + private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public ImportScriptService(IProcessProvider processProvider, @@ -32,6 +35,7 @@ namespace NzbDrone.Core.MediaFiles IConfigService configService, IConfigFileProvider configFileProvider, ITagRepository tagRepository, + IDiskProvider diskProvider, Logger logger) { _processProvider = processProvider; @@ -39,9 +43,73 @@ namespace NzbDrone.Core.MediaFiles _configService = configService; _configFileProvider = configFileProvider; _tagRepository = tagRepository; + _diskProvider = diskProvider; _logger = logger; } + private static readonly Regex OutputRegex = new Regex(@"^(?:\[(?:(?MediaFile)|(?ExtraFile))\]\s?(?.+)|(?\[PreventExtraImport\])|\[MoveStatus\]\s?(?:(?DeferMove)|(?MoveComplete)|(?RenameRequested)))$", RegexOptions.Compiled); + + private ScriptImportInfo ProcessOutput(List processOutputLines) + { + var possibleExtraFiles = new List(); + string mediaFile = null; + var decision = ScriptImportDecision.MoveComplete; + var importExtraFiles = true; + + foreach (var line in processOutputLines) + { + var match = OutputRegex.Match(line.Content); + + if (match.Groups["mediaFile"].Success) + { + if (mediaFile is not null) + { + throw new ScriptImportException("Script output contains multiple media files. Only one media file can be returned."); + } + + mediaFile = match.Groups["fileName"].Value; + + if (!MediaFileExtensions.Extensions.Contains(Path.GetExtension(mediaFile))) + { + throw new ScriptImportException("Script output contains invalid media file: {0}", mediaFile); + } + else if (!_diskProvider.FileExists(mediaFile)) + { + throw new ScriptImportException("Script output contains non-existent media file: {0}", mediaFile); + } + } + else if (match.Groups["extraFile"].Success) + { + var fileName = match.Groups["fileName"].Value; + + if (!_diskProvider.FileExists(fileName)) + { + _logger.Warn("Script output contains non-existent possible extra file: {0}", fileName); + } + + possibleExtraFiles.Add(fileName); + } + else if (match.Groups["moveComplete"].Success) + { + decision = ScriptImportDecision.MoveComplete; + } + else if (match.Groups["renameRequested"].Success) + { + decision = ScriptImportDecision.RenameRequested; + } + else if (match.Groups["deferMove"].Success) + { + decision = ScriptImportDecision.DeferMove; + } + else if (match.Groups["preventExtraImport"].Success) + { + importExtraFiles = false; + } + } + + return new ScriptImportInfo(possibleExtraFiles, mediaFile, decision, importExtraFiles); + } + public ScriptImportDecision TryImport(string sourcePath, string destinationFilePath, LocalEpisode localEpisode, EpisodeFile episodeFile, TransferMode mode) { var series = localEpisode.Series; @@ -115,22 +183,37 @@ namespace NzbDrone.Core.MediaFiles var processOutput = _processProvider.StartAndCapture(_configService.ScriptImportPath, $"\"{sourcePath}\" \"{destinationFilePath}\"", environmentVariables); - _logger.Debug("Executed external script: {0} - Status: {1}", _configService.ScriptImportPath, processOutput.ExitCode); _logger.Debug("Script Output: \r\n{0}", string.Join("\r\n", processOutput.Lines)); - switch (processOutput.ExitCode) + if (processOutput.ExitCode != 0) { - case 0: // Copy complete - return ScriptImportDecision.MoveComplete; - case 2: // Copy complete, file potentially changed, should try renaming again - episodeFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(destinationFilePath); - episodeFile.Path = null; - return ScriptImportDecision.RenameRequested; - case 3: // Let Sonarr handle it - return ScriptImportDecision.DeferMove; - default: // Error, fail to import - throw new ScriptImportException("Moving with script failed! Exit code {0}", processOutput.ExitCode); + throw new ScriptImportException("Script exited with non-zero exit code: {0}", processOutput.ExitCode); } + + var scriptImportInfo = ProcessOutput(processOutput.Lines); + + var mediaFile = scriptImportInfo.MediaFile ?? destinationFilePath; + localEpisode.PossibleExtraFiles = scriptImportInfo.PossibleExtraFiles; + + episodeFile.RelativePath = series.Path.GetRelativePath(mediaFile); + episodeFile.Path = mediaFile; + + var exitCode = processOutput.ExitCode; + + localEpisode.ShouldImportExtras = scriptImportInfo.ImportExtraFiles; + + if (scriptImportInfo.Decision != ScriptImportDecision.DeferMove) + { + localEpisode.ScriptImported = true; + } + + if (scriptImportInfo.Decision == ScriptImportDecision.RenameRequested) + { + episodeFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(mediaFile); + episodeFile.Path = null; + } + + return scriptImportInfo.Decision; } } } diff --git a/src/NzbDrone.Core/MediaFiles/ScriptImportInfo.cs b/src/NzbDrone.Core/MediaFiles/ScriptImportInfo.cs new file mode 100644 index 000000000..6c861fa8d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/ScriptImportInfo.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MediaFiles +{ + public struct ScriptImportInfo + { + public List PossibleExtraFiles { get; set; } + public string MediaFile { get; set; } + public ScriptImportDecision Decision { get; set; } + public bool ImportExtraFiles { get; set; } + + public ScriptImportInfo(List possibleExtraFiles, string mediaFile, ScriptImportDecision decision, bool importExtraFiles) + { + PossibleExtraFiles = possibleExtraFiles; + MediaFile = mediaFile; + Decision = decision; + ImportExtraFiles = importExtraFiles; + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs index 94d30f166..f8aef8654 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs @@ -8,5 +8,6 @@ namespace NzbDrone.Core.MetadataSource List SearchForNewSeries(string title); List SearchForNewSeriesByImdbId(string imdbId); List SearchForNewSeriesByAniListId(int aniListId); + List SearchForNewSeriesByTmdbId(int tmdbId); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs index a44137f5a..c7acd354a 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource // public string Language { get; set; } public string Slug { get; set; } public string FirstAired { get; set; } + public string LastAired { get; set; } public int? TvRageId { get; set; } public int? TvMazeId { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 4e5077c02..76efea07d 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 SearchForNewSeriesByTmdbId(int tmdbId) + { + var results = SearchForNewSeries($"tmdb:{tmdbId}"); + + return results; + } + public List SearchForNewSeries(string title) { try @@ -189,6 +196,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook series.Year = series.FirstAired.Value.Year; } + if (show.LastAired != null) + { + series.LastAired = DateTime.ParseExact(show.LastAired, "yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + series.Overview = show.Overview; if (show.Runtime != null) diff --git a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs deleted file mode 100644 index ef5850637..000000000 --- a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Core.Notifications.Boxcar -{ - public class Boxcar : NotificationBase - { - private readonly IBoxcarProxy _proxy; - - public Boxcar(IBoxcarProxy proxy) - { - _proxy = proxy; - } - - public override string Link => "https://boxcar.io/client"; - public override string Name => "Boxcar"; - - public override void OnGrab(GrabMessage grabMessage) - { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); - } - - public override void OnDownload(DownloadMessage message) - { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); - } - - public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) - { - _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); - } - - public override void OnSeriesAdd(SeriesAddMessage message) - { - _proxy.SendNotification(SERIES_ADDED_TITLE, message.Message, Settings); - } - - public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) - { - _proxy.SendNotification(SERIES_DELETED_TITLE, deleteMessage.Message, Settings); - } - - public override void OnHealthIssue(HealthCheck.HealthCheck message) - { - _proxy.SendNotification(HEALTH_ISSUE_TITLE, message.Message, Settings); - } - - public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) - { - _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); - } - - public override void OnApplicationUpdate(ApplicationUpdateMessage message) - { - _proxy.SendNotification(APPLICATION_UPDATE_TITLE, message.Message, Settings); - } - - public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) - { - _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings); - } - - public override ValidationResult Test() - { - var failures = new List(); - - failures.AddIfNotNull(_proxy.Test(Settings)); - - return new ValidationResult(failures); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarException.cs b/src/NzbDrone.Core/Notifications/Boxcar/BoxcarException.cs deleted file mode 100644 index 6108d4aab..000000000 --- a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Notifications.Boxcar -{ - public class BoxcarException : NzbDroneException - { - public BoxcarException(string message) - : base(message) - { - } - - public BoxcarException(string message, Exception innerException, params object[] args) - : base(message, innerException, args) - { - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs b/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs deleted file mode 100644 index fda8dc71e..000000000 --- a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Net; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Http; - -namespace NzbDrone.Core.Notifications.Boxcar -{ - public interface IBoxcarProxy - { - void SendNotification(string title, string message, BoxcarSettings settings); - ValidationFailure Test(BoxcarSettings settings); - } - - public class BoxcarProxy : IBoxcarProxy - { - private const string URL = "https://new.boxcar.io/api/notifications"; - - private readonly IHttpClient _httpClient; - private readonly Logger _logger; - - public BoxcarProxy(IHttpClient httpClient, Logger logger) - { - _httpClient = httpClient; - _logger = logger; - } - - public void SendNotification(string title, string message, BoxcarSettings settings) - { - try - { - ProcessNotification(title, message, settings); - } - catch (BoxcarException ex) - { - _logger.Error(ex, "Unable to send message"); - throw new BoxcarException("Unable to send Boxcar notifications"); - } - } - - public ValidationFailure Test(BoxcarSettings settings) - { - try - { - const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; - - SendNotification(title, body, settings); - return null; - } - catch (HttpException ex) - { - if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) - { - _logger.Error(ex, "Access Token is invalid"); - return new ValidationFailure("Token", "Access Token is invalid"); - } - - _logger.Error(ex, "Unable to send test message"); - return new ValidationFailure("Token", "Unable to send test message"); - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to send test message"); - return new ValidationFailure("", "Unable to send test message"); - } - } - - private void ProcessNotification(string title, string message, BoxcarSettings settings) - { - try - { - var requestBuilder = new HttpRequestBuilder(URL).Post(); - - var request = requestBuilder.AddFormParameter("user_credentials", settings.Token) - .AddFormParameter("notification[title]", title) - .AddFormParameter("notification[long_message]", message) - .AddFormParameter("notification[source_name]", BuildInfo.AppName) - .AddFormParameter("notification[icon_url]", "https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/64.png") - .Build(); - - _httpClient.Post(request); - } - catch (HttpException ex) - { - if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) - { - _logger.Error(ex, "Access Token is invalid"); - throw; - } - - throw new BoxcarException("Unable to send text message: " + ex.Message, ex); - } - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarSettings.cs b/src/NzbDrone.Core/Notifications/Boxcar/BoxcarSettings.cs deleted file mode 100644 index b6b9fd96d..000000000 --- a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarSettings.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Notifications.Boxcar -{ - public class BoxcarSettingsValidator : AbstractValidator - { - public BoxcarSettingsValidator() - { - RuleFor(c => c.Token).NotEmpty(); - } - } - - public class BoxcarSettings : IProviderConfig - { - private static readonly BoxcarSettingsValidator Validator = new BoxcarSettingsValidator(); - - [FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpText = "Your Access Token, from your Boxcar account settings: https://new.boxcar.io/account/edit", HelpLink = "https://new.boxcar.io/account/edit")] - public string Token { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 1976904d7..e085b4165 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -363,14 +363,6 @@ namespace NzbDrone.Core.Notifications.CustomScript failures.Add(new NzbDroneValidationFailure("Path", "File does not exist")); } - foreach (var systemFolder in SystemFolders.GetSystemFolders()) - { - if (systemFolder.IsParentPath(Settings.Path)) - { - failures.Add(new NzbDroneValidationFailure("Path", $"Must not be a descendant of '{systemFolder}'")); - } - } - if (failures.Empty()) { try diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs index f4d4d7803..ce5a398ed 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications.CustomScript public CustomScriptSettingsValidator() { RuleFor(c => c.Path).IsValidPath(); + RuleFor(c => c.Path).SetValidator(new SystemFolderValidator()).WithMessage("Must not be a descendant of '{systemFolder}'"); RuleFor(c => c.Arguments).Empty().WithMessage("Arguments are no longer supported for custom scripts"); } } diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs index 81d41b862..b308460eb 100644 --- a/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs +++ b/src/NzbDrone.Core/Notifications/Notifiarr/NotifiarrProxy.cs @@ -53,8 +53,10 @@ namespace NzbDrone.Core.Notifications.Notifiarr _logger.Error("HTTP 401 - API key is invalid"); throw new NotifiarrException("API key is invalid"); case 400: + // 400 responses shouldn't be treated as an actual error because it's a misconfiguration + // between Sonarr and Notifiarr for a specific event, but shouldn't stop all events. _logger.Error("HTTP 400 - Unable to send notification. Ensure Sonarr Integration is enabled & assigned a channel on Notifiarr"); - throw new NotifiarrException("Unable to send notification. Ensure Sonarr Integration is enabled & assigned a channel on Notifiarr"); + break; case 502: case 503: case 504: diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index ca7fa6215..35823feca 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using FluentValidation.Results; using NLog; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; @@ -193,5 +194,26 @@ namespace NzbDrone.Core.Notifications definition.SupportsOnApplicationUpdate = provider.SupportsOnApplicationUpdate; definition.SupportsOnManualInteractionRequired = provider.SupportsOnManualInteractionRequired; } + + public override ValidationResult Test(NotificationDefinition definition) + { + var result = base.Test(definition); + + if (definition.Id == 0) + { + return result; + } + + if (result == null || result.IsValid) + { + _notificationStatusService.RecordSuccess(definition.Id); + } + else + { + _notificationStatusService.RecordFailure(definition.Id); + } + + return result; + } } } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs index 29fa466aa..ba247ce8f 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Notifications.Webhook AirDate = episode.AirDate; AirDateUtc = episode.AirDateUtc; SeriesId = episode.SeriesId; + TvdbId = episode.TvdbId; } public int Id { get; set; } @@ -29,5 +30,6 @@ namespace NzbDrone.Core.Notifications.Webhook public string AirDate { get; set; } public DateTime? AirDateUtc { get; set; } public int SeriesId { get; set; } + public int TvdbId { get; set; } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 5b638cfbd..112232521 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -5,6 +5,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Diacritical; +using DryIoc.ImTools; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; @@ -353,6 +355,17 @@ namespace NzbDrone.Core.Organizer return TitlePrefixRegex.Replace(title, "$2, $1$3"); } + public static string CleanTitleThe(string title) + { + if (TitlePrefixRegex.IsMatch(title)) + { + var splitResult = TitlePrefixRegex.Split(title); + return $"{CleanTitle(splitResult[2]).Trim()}, {splitResult[1]}{CleanTitle(splitResult[3])}"; + } + + return CleanTitle(title); + } + public static string TitleYear(string title, int year) { // Don't use 0 for the year. @@ -370,6 +383,25 @@ namespace NzbDrone.Core.Organizer return $"{title} ({year})"; } + public static string CleanTitleTheYear(string title, int year) + { + // Don't use 0 for the year. + if (year == 0) + { + return CleanTitleThe(title); + } + + // Regex match incase the year in the title doesn't match the year, for whatever reason. + if (YearRegex.IsMatch(title)) + { + var splitReturn = YearRegex.Split(title); + var yearMatch = YearRegex.Match(title); + return $"{CleanTitleThe(splitReturn[0].Trim())} {yearMatch.Value[1..5]}"; + } + + return $"{CleanTitleThe(title)} {year}"; + } + public static string TitleWithoutYear(string title) { title = YearRegex.Replace(title, ""); @@ -377,6 +409,23 @@ namespace NzbDrone.Core.Organizer return title; } + public static string TitleFirstCharacter(string title) + { + if (char.IsLetterOrDigit(title[0])) + { + return title.Substring(0, 1).ToUpper().RemoveDiacritics()[0].ToString(); + } + + // Try the second character if the first was non alphanumeric + if (char.IsLetterOrDigit(title[1])) + { + return title.Substring(1, 1).ToUpper().RemoveDiacritics()[0].ToString(); + } + + // Default to "_" if no alphanumeric character can be found in the first 2 positions + return "_"; + } + public static string CleanFileName(string name) { return CleanFileName(name, NamingConfig.Default); @@ -445,14 +494,17 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Series Title}"] = m => series.Title; tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); + tokenHandlers["{Series TitleYear}"] = m => TitleYear(series.Title, series.Year); tokenHandlers["{Series CleanTitleYear}"] = m => CleanTitle(TitleYear(series.Title, series.Year)); + tokenHandlers["{Series TitleWithoutYear}"] = m => TitleWithoutYear(series.Title); tokenHandlers["{Series CleanTitleWithoutYear}"] = m => CleanTitle(TitleWithoutYear(series.Title)); tokenHandlers["{Series TitleThe}"] = m => TitleThe(series.Title); - tokenHandlers["{Series TitleYear}"] = m => TitleYear(series.Title, series.Year); - tokenHandlers["{Series TitleWithoutYear}"] = m => TitleWithoutYear(series.Title); + tokenHandlers["{Series CleanTitleThe}"] = m => CleanTitleThe(series.Title); tokenHandlers["{Series TitleTheYear}"] = m => TitleYear(TitleThe(series.Title), series.Year); + tokenHandlers["{Series CleanTitleTheYear}"] = m => CleanTitleTheYear(series.Title, series.Year); tokenHandlers["{Series TitleTheWithoutYear}"] = m => TitleWithoutYear(TitleThe(series.Title)); - tokenHandlers["{Series TitleFirstCharacter}"] = m => TitleThe(series.Title).Substring(0, 1).FirstCharToUpper(); + tokenHandlers["{Series CleanTitleTheWithoutYear}"] = m => CleanTitleThe(TitleWithoutYear(series.Title)); + tokenHandlers["{Series TitleFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(series.Title)); tokenHandlers["{Series Year}"] = m => series.Year.ToString(); } diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index c55e4d07b..e9390a766 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -19,7 +19,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|_)(?\b(?:ing|eng)\b)|(?\b(?:ita|italian)\b)|(?german\b|videomann|ger[. ]dub)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?\brus\b)|(?\b(?:HUNDUB|HUN)\b)|(?\bHebDub\b)|(?\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?\bbgaudio\b)|(?\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?\b(?:ukr)\b)|(?\b(?:THAI)\b)|(?\b(?:RoDubbed|ROMANIAN)\b)|(?[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)", + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?\b(?:ing|eng)\b)|(?\b(?:ita|italian)\b)|(?german\b|videomann|ger[. ]dub)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VF|VF2|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?\brus\b)|(?\b(?:HUNDUB|HUN)\b)|(?\bHebDub\b)|(?\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?\bbgaudio\b)|(?\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?\b(?:ukr)\b)|(?\b(?:THAI)\b)|(?\b(?:RoDubbed|ROMANIAN)\b)|(?[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?\bLT\b)|(?\bCZ\b)|(?\bPL\b)|(?\bBG\b)|(?\bSK\b))(?:(?i)(?![\W|_|^]SUB))", @@ -362,7 +362,7 @@ namespace NzbDrone.Core.Parser languages.Add(Language.Thai); } - if (match.Groups["romainian"].Success) + if (match.Groups["romanian"].Success) { languages.Add(Language.Romanian); } diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index bcb9a5132..470310b7c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -40,6 +40,10 @@ namespace NzbDrone.Core.Parser.Model public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public GrabbedReleaseInfo Release { get; set; } + public bool ScriptImported { get; set; } + public bool FileRenamedAfterScriptImport { get; set; } + public bool ShouldImportExtras { get; set; } + public List PossibleExtraFiles { get; set; } public int SeasonNumber { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 15a8c73c8..df0cb00a7 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -199,10 +199,10 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Partial season pack - new Regex(@"^(?.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)", + new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Anime - 4 digit absolute episode number + // Anime - 4 digit absolute episode number new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -254,6 +254,11 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[-_. ]S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[-_. ]?[ex]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // TODO: Before this + // Single or multi episode releases with multiple titles, each followed by season and episode numbers in brackets + new Regex(@"^(?<title>.*?)[ ._]\(S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?\)(?:[ ._]\/[ ._])(?<title>.*?)[ ._]\(", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Single episode season or episode S1E1 or S1-E1 or S1.Ep1 or S01.Ep.01 new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?Ep?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -330,6 +335,9 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Turkish tracker releases (01 BLM, 3. Blm, 04.Bolum, etc) + new Regex(@"^(?<title>.+?)[_. ](?<absoluteepisode>\d{1,4})(?:[_. ]+)(?:BLM|B[oö]l[uü]m)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes with airdate (04.28.2018) new Regex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -363,7 +371,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Spanish tracker releases - new Regex(@"^(?<title>.+?)(?:[-_. ]+?Temporada.+?\[Cap[-_.])(?<season>(?<!\d+)\d{1,2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9]))(?:\])", + new Regex(@"^(?<title>.+?)(?:(?:[-_. ]+?Temporada.+?|\[.+?\])\[Cap[-_.])(?<season>(?<!\d+)\d{1,2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9]))(?:\])", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime Range - Title Absolute Episode Number (ep01-12) @@ -514,7 +522,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); // groups whose releases end with RlsGroup) or RlsGroup] - private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<releasegroup>(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<releasegroup>(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)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -883,7 +891,7 @@ namespace NzbDrone.Core.Parser return title; } - private static SeriesTitleInfo GetSeriesTitleInfo(string title) + private static SeriesTitleInfo GetSeriesTitleInfo(string title, MatchCollection matchCollection) { var seriesTitleInfo = new SeriesTitleInfo(); seriesTitleInfo.Title = title; @@ -906,6 +914,10 @@ namespace NzbDrone.Core.Parser { seriesTitleInfo.AllTitles = matchComponents.Groups["title"].Captures.OfType<Capture>().Select(v => v.Value).ToArray(); } + else if (matchCollection[0].Groups["title"].Captures.Count > 1) + { + seriesTitleInfo.AllTitles = matchCollection[0].Groups["title"].Captures.Select(c => c.Value.Replace('.', ' ').Replace('_', ' ')).ToArray(); + } return seriesTitleInfo; } @@ -1112,7 +1124,7 @@ namespace NzbDrone.Core.Parser } result.SeriesTitle = seriesName; - result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); + result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle, matchCollection); Logger.Debug("Episode Parsed. {0}", result); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 22837f1b5..7631887cd 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -67,7 +67,12 @@ namespace NzbDrone.Core.Parser public static QualityModel ParseQuality(string name) { - Logger.Debug("Trying to parse quality for {0}", name); + Logger.Debug("Trying to parse quality for '{0}'", name); + + if (name.IsNullOrWhiteSpace()) + { + return new QualityModel { Quality = Quality.Unknown }; + } name = name.Trim(); diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs index 65b82eb75..c824d30e5 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs @@ -6,12 +6,12 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Profiles.Qualities { - public interface IProfileRepository : IBasicRepository<QualityProfile> + public interface IQualityProfileRepository : IBasicRepository<QualityProfile> { bool Exists(int id); } - public class QualityProfileRepository : BasicRepository<QualityProfile>, IProfileRepository + public class QualityProfileRepository : BasicRepository<QualityProfile>, IQualityProfileRepository { private readonly ICustomFormatService _customFormatService; diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index ae7446511..67b7dc18f 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -28,19 +28,19 @@ namespace NzbDrone.Core.Profiles.Qualities IHandle<CustomFormatAddedEvent>, IHandle<CustomFormatDeletedEvent> { - private readonly IProfileRepository _profileRepository; + private readonly IQualityProfileRepository _qualityProfileRepository; private readonly IImportListFactory _importListFactory; private readonly ICustomFormatService _formatService; private readonly ISeriesService _seriesService; private readonly Logger _logger; - public QualityProfileService(IProfileRepository profileRepository, + public QualityProfileService(IQualityProfileRepository qualityProfileRepository, IImportListFactory importListFactory, ICustomFormatService formatService, ISeriesService seriesService, Logger logger) { - _profileRepository = profileRepository; + _qualityProfileRepository = qualityProfileRepository; _importListFactory = importListFactory; _formatService = formatService; _seriesService = seriesService; @@ -49,38 +49,38 @@ namespace NzbDrone.Core.Profiles.Qualities public QualityProfile Add(QualityProfile profile) { - return _profileRepository.Insert(profile); + return _qualityProfileRepository.Insert(profile); } public void Update(QualityProfile profile) { - _profileRepository.Update(profile); + _qualityProfileRepository.Update(profile); } public void Delete(int id) { if (_seriesService.GetAllSeries().Any(c => c.QualityProfileId == id) || _importListFactory.All().Any(c => c.QualityProfileId == id)) { - var profile = _profileRepository.Get(id); + var profile = _qualityProfileRepository.Get(id); throw new QualityProfileInUseException(profile.Name); } - _profileRepository.Delete(id); + _qualityProfileRepository.Delete(id); } public List<QualityProfile> All() { - return _profileRepository.All().ToList(); + return _qualityProfileRepository.All().ToList(); } public QualityProfile Get(int id) { - return _profileRepository.Get(id); + return _qualityProfileRepository.Get(id); } public bool Exists(int id) { - return _profileRepository.Exists(id); + return _qualityProfileRepository.Exists(id); } public void Handle(ApplicationStartedEvent message) diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs index 5e033b582..b757db3f3 100644 --- a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs @@ -127,8 +127,16 @@ namespace NzbDrone.Core.RemotePathMappings return remotePath; } + var mappings = All(); + + if (mappings.Empty()) + { + return remotePath; + } + _logger.Trace("Evaluating remote path remote mappings for match to host [{0}] and remote path [{1}]", host, remotePath.FullPath); - foreach (var mapping in All()) + + foreach (var mapping in mappings) { _logger.Trace("Checking configured remote path mapping: {0} - {1}", mapping.Host, mapping.RemotePath); if (host.Equals(mapping.Host, StringComparison.InvariantCultureIgnoreCase) && new OsPath(mapping.RemotePath).Contains(remotePath)) @@ -150,8 +158,16 @@ namespace NzbDrone.Core.RemotePathMappings return localPath; } + var mappings = All(); + + if (mappings.Empty()) + { + return localPath; + } + _logger.Trace("Evaluating remote path local mappings for match to host [{0}] and local path [{1}]", host, localPath.FullPath); - foreach (var mapping in All()) + + foreach (var mapping in mappings) { _logger.Trace("Checking configured remote path mapping {0} - {1}", mapping.Host, mapping.RemotePath); if (host.Equals(mapping.Host, StringComparison.InvariantCultureIgnoreCase) && new OsPath(mapping.LocalPath).Contains(localPath)) diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs index 8c730bed6..5fe1507c2 100644 --- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.SeriesStats public int SeasonNumber { get; set; } public string NextAiringString { get; set; } public string PreviousAiringString { get; set; } + public string LastAiredString { get; set; } public int EpisodeFileCount { get; set; } public int EpisodeCount { get; set; } public int AvailableEpisodeCount { get; set; } @@ -65,6 +66,29 @@ namespace NzbDrone.Core.SeriesStats } } + public DateTime? LastAired + { + get + { + DateTime lastAired; + + try + { + if (!DateTime.TryParse(LastAiredString, out lastAired)) + { + return null; + } + } + catch (ArgumentOutOfRangeException) + { + // GHI 3518: Can throw on mono (6.x?) despite being a Try* + return null; + } + + return lastAired; + } + } + public List<string> ReleaseGroups { get diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs index fb6215e18..cb054da7c 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; @@ -9,6 +9,7 @@ namespace NzbDrone.Core.SeriesStats public int SeriesId { get; set; } public string NextAiringString { get; set; } public string PreviousAiringString { get; set; } + public string LastAiredString { get; set; } public int EpisodeFileCount { get; set; } public int EpisodeCount { get; set; } public int TotalEpisodeCount { get; set; } @@ -61,5 +62,28 @@ namespace NzbDrone.Core.SeriesStats return previousAiring; } } + + public DateTime? LastAired + { + get + { + DateTime lastAired; + + try + { + if (!DateTime.TryParse(LastAiredString, out lastAired)) + { + return null; + } + } + catch (ArgumentOutOfRangeException) + { + // GHI 3518: Can throw on mono (6.x?) despite being a Try* + return null; + } + + return lastAired; + } + } } } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs index d95828776..be64ca1b3 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs @@ -79,8 +79,9 @@ namespace NzbDrone.Core.SeriesStats SUM(CASE WHEN ""AirDateUtc"" <= @currentDate OR ""EpisodeFileId"" > 0 THEN 1 ELSE 0 END) AS AvailableEpisodeCount, SUM(CASE WHEN (""Monitored"" = {trueIndicator} AND ""AirDateUtc"" <= @currentDate) OR ""EpisodeFileId"" > 0 THEN 1 ELSE 0 END) AS EpisodeCount, SUM(CASE WHEN ""EpisodeFileId"" > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, - MIN(CASE WHEN ""AirDateUtc"" < @currentDate OR ""EpisodeFileId"" > 0 OR ""Monitored"" = {falseIndicator} THEN NULL ELSE ""AirDateUtc"" END) AS NextAiringString, - MAX(CASE WHEN ""AirDateUtc"" >= @currentDate OR ""EpisodeFileId"" = 0 AND ""Monitored"" = {falseIndicator} THEN NULL ELSE ""AirDateUtc"" END) AS PreviousAiringString", parameters) + MIN(CASE WHEN ""AirDateUtc"" < @currentDate OR ""Monitored"" = {falseIndicator} THEN NULL ELSE ""AirDateUtc"" END) AS NextAiringString, + MAX(CASE WHEN ""AirDateUtc"" >= @currentDate OR ""Monitored"" = {falseIndicator} THEN NULL ELSE ""AirDateUtc"" END) AS PreviousAiringString, + MAX(""AirDate"") AS LastAiredString", parameters) .GroupBy<Episode>(x => x.SeriesId) .GroupBy<Episode>(x => x.SeasonNumber); } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs index 44b5ddc16..5baef28f1 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace NzbDrone.Core.SeriesStats @@ -52,9 +52,11 @@ namespace NzbDrone.Core.SeriesStats var nextAiring = seasonStatistics.Where(s => s.NextAiring != null).MinBy(s => s.NextAiring); var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null).MaxBy(s => s.PreviousAiring); + var lastAired = seasonStatistics.Where(s => s.SeasonNumber > 0 && s.LastAired != null).MaxBy(s => s.LastAired); seriesStatistics.NextAiringString = nextAiring?.NextAiringString; seriesStatistics.PreviousAiringString = previousAiring?.PreviousAiringString; + seriesStatistics.LastAiredString = lastAired?.LastAiredString; return seriesStatistics; } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 5c4cec3dd..94c3a44fe 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -4,10 +4,11 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Dapper" Version="2.0.123" /> + <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="Servarr.FFMpegCore" Version="4.7.0-26" /> - <PackageReference Include="Servarr.FFprobe" Version="5.1.2.106" /> + <PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" /> <PackageReference Include="System.Memory" Version="4.5.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs index d83c7dfda..292bc4bc5 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Text.Json.Serialization; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ThingiProvider @@ -13,7 +14,10 @@ namespace NzbDrone.Core.ThingiProvider private IProviderConfig _settings; public string Name { get; set; } + + [JsonIgnore] public string ImplementationName { get; set; } + public string Implementation { get; set; } public string ConfigContract { get; set; } public virtual bool Enable { get; set; } diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs index 15cf84d3b..6279c6e35 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs @@ -59,6 +59,11 @@ namespace NzbDrone.Core.ThingiProvider.Status public virtual void RecordSuccess(int providerId) { + if (providerId <= 0) + { + return; + } + lock (_syncRoot) { var status = GetProviderStatus(providerId); @@ -79,6 +84,11 @@ namespace NzbDrone.Core.ThingiProvider.Status protected virtual void RecordFailure(int providerId, TimeSpan minimumBackOff, bool escalate) { + if (providerId <= 0) + { + return; + } + lock (_syncRoot) { var status = GetProviderStatus(providerId); diff --git a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs index c0351457a..41cdbcfc2 100644 --- a/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeMonitoredService.cs @@ -83,23 +83,27 @@ namespace NzbDrone.Core.Tv break; + case MonitorTypes.LastSeason: + #pragma warning disable CS0612 case MonitorTypes.LatestSeason: - if (episodes.Where(e => e.SeasonNumber == lastSeason) - .All(e => e.AirDateUtc.HasValue && - e.AirDateUtc.Value.Before(DateTime.UtcNow) && - !e.AirDateUtc.Value.InLastDays(90))) - { - _logger.Debug("[{0}] Unmonitoring all episodes because latest season aired more than 90 days ago", series.Title); - ToggleEpisodesMonitoredState(episodes, e => false); - break; - } - + #pragma warning restore CS0612 _logger.Debug("[{0}] Monitoring latest season episodes", series.Title); ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && e.SeasonNumber == lastSeason); break; + case MonitorTypes.Recent: + _logger.Debug("[{0}] Monitoring recent and future episodes", series.Title); + + ToggleEpisodesMonitoredState(episodes, e => e.SeasonNumber > 0 && + (!e.AirDateUtc.HasValue || ( + e.AirDateUtc.Value.Before(DateTime.UtcNow) && + e.AirDateUtc.Value.InLastDays(90)) + || e.AirDateUtc.Value.After(DateTime.UtcNow))); + + break; + case MonitorTypes.MonitorSpecials: _logger.Debug("[{0}] Monitoring special episodes", series.Title); ToggleEpisodesMonitoredState(episodes.Where(e => e.SeasonNumber == 0), true); @@ -128,15 +132,14 @@ namespace NzbDrone.Core.Tv { var seasonNumber = season.SeasonNumber; - // Monitor the season when: + // Monitor the last season when: // - Not specials // - The latest season - // - Not only supposed to monitor the first season + // - Set to monitor all or future episodes if (seasonNumber > 0 && seasonNumber == lastSeason && - monitoringOptions.Monitor != MonitorTypes.FirstSeason && - monitoringOptions.Monitor != MonitorTypes.Pilot && - monitoringOptions.Monitor != MonitorTypes.None) + (monitoringOptions.Monitor == MonitorTypes.All || + monitoringOptions.Monitor == MonitorTypes.Future)) { season.Monitored = true; } diff --git a/src/NzbDrone.Core/Tv/MonitoringOptions.cs b/src/NzbDrone.Core/Tv/MonitoringOptions.cs index 878a9f8e1..b49a5ee6a 100644 --- a/src/NzbDrone.Core/Tv/MonitoringOptions.cs +++ b/src/NzbDrone.Core/Tv/MonitoringOptions.cs @@ -1,3 +1,4 @@ +using System; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Tv @@ -17,10 +18,21 @@ namespace NzbDrone.Core.Tv Missing, Existing, FirstSeason, + LastSeason, + + [Obsolete] LatestSeason, + Pilot, + Recent, MonitorSpecials, UnmonitorSpecials, None } + + public enum NewItemMonitorTypes + { + All, + None + } } diff --git a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs index e2a3d41f1..893e326e1 100644 --- a/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/src/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Tv else { episodeToUpdate = new Episode(); - episodeToUpdate.Monitored = GetMonitoredStatus(episode, seasons); + episodeToUpdate.Monitored = GetMonitoredStatus(episode, seasons, series); newList.Add(episodeToUpdate); } @@ -135,9 +135,9 @@ namespace NzbDrone.Core.Tv } } - private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons) + private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons, Series series) { - if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) + if ((episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) || series.MonitorNewItems == NewItemMonitorTypes.None) { return false; } diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index a3c74b5e5..41a3b5cae 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -104,6 +104,7 @@ namespace NzbDrone.Core.Tv series.Images = seriesInfo.Images; series.Network = seriesInfo.Network; series.FirstAired = seriesInfo.FirstAired; + series.LastAired = seriesInfo.LastAired; series.Ratings = seriesInfo.Ratings; series.Actors = seriesInfo.Actors; series.Genres = seriesInfo.Genres; @@ -138,7 +139,6 @@ namespace NzbDrone.Core.Tv { var existingSeason = series.Seasons.FirstOrDefault(s => s.SeasonNumber == season.SeasonNumber); - // Todo: Should this should use the previous season's monitored state? if (existingSeason == null) { if (season.SeasonNumber == 0) @@ -148,8 +148,10 @@ namespace NzbDrone.Core.Tv continue; } - _logger.Debug("New season ({0}) for series: [{1}] {2}, setting monitored to {3}", season.SeasonNumber, series.TvdbId, series.Title, series.Monitored.ToString().ToLowerInvariant()); - season.Monitored = series.Monitored; + var monitorNewSeasons = series.MonitorNewItems == NewItemMonitorTypes.All; + + _logger.Debug("New season ({0}) for series: [{1}] {2}, setting monitored to {3}", season.SeasonNumber, series.TvdbId, series.Title, monitorNewSeasons.ToString().ToLowerInvariant()); + season.Monitored = monitorNewSeasons; } else { diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index b10ac3de5..6296d8500 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Tv public string Overview { get; set; } public string AirTime { get; set; } public bool Monitored { get; set; } + public NewItemMonitorTypes MonitorNewItems { get; set; } public int QualityProfileId { get; set; } public bool SeasonFolder { get; set; } public DateTime? LastInfoSync { get; set; } @@ -48,6 +49,7 @@ namespace NzbDrone.Core.Tv public string RootFolderPath { get; set; } public DateTime Added { get; set; } public DateTime? FirstAired { get; set; } + public DateTime? LastAired { get; set; } public LazyLoaded<QualityProfile> QualityProfile { get; set; } public Language OriginalLanguage { get; set; } @@ -70,6 +72,7 @@ namespace NzbDrone.Core.Tv SeasonFolder = otherSeries.SeasonFolder; Monitored = otherSeries.Monitored; + MonitorNewItems = otherSeries.MonitorNewItems; SeriesType = otherSeries.SeriesType; RootFolderPath = otherSeries.RootFolderPath; diff --git a/src/NzbDrone.Core/Validation/DownloadClientExistsValidator.cs b/src/NzbDrone.Core/Validation/DownloadClientExistsValidator.cs new file mode 100644 index 000000000..cf021f464 --- /dev/null +++ b/src/NzbDrone.Core/Validation/DownloadClientExistsValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation.Validators; +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.Validation +{ + public class DownloadClientExistsValidator : PropertyValidator + { + private readonly IDownloadClientFactory _downloadClientFactory; + + public DownloadClientExistsValidator(IDownloadClientFactory downloadClientFactory) + { + _downloadClientFactory = downloadClientFactory; + } + + protected override string GetDefaultMessageTemplate() => "Download Client does not exist"; + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context?.PropertyValue == null || (int)context.PropertyValue == 0) + { + return true; + } + + return _downloadClientFactory.Exists((int)context.PropertyValue); + } + } +} diff --git a/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs index 5393ce57b..327765537 100644 --- a/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation.Validators; +using FluentValidation.Validators; using NzbDrone.Common.Disk; namespace NzbDrone.Core.Validation.Paths diff --git a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs b/src/NzbDrone.Core/Validation/QualityProfileExistsValidator.cs similarity index 67% rename from src/NzbDrone.Core/Validation/ProfileExistsValidator.cs rename to src/NzbDrone.Core/Validation/QualityProfileExistsValidator.cs index b8dceb294..0bc853a56 100644 --- a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/QualityProfileExistsValidator.cs @@ -3,20 +3,20 @@ using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Validation { - public class ProfileExistsValidator : PropertyValidator + public class QualityProfileExistsValidator : PropertyValidator { private readonly IQualityProfileService _qualityProfileService; - public ProfileExistsValidator(IQualityProfileService qualityProfileService) + public QualityProfileExistsValidator(IQualityProfileService qualityProfileService) { _qualityProfileService = qualityProfileService; } - protected override string GetDefaultMessageTemplate() => "QualityProfile does not exist"; + protected override string GetDefaultMessageTemplate() => "Quality Profile does not exist"; protected override bool IsValid(PropertyValidatorContext context) { - if (context.PropertyValue == null) + if (context?.PropertyValue == null || (int)context.PropertyValue == 0) { return true; } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index a14e9f2ff..0e9ff30a4 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -22,6 +22,7 @@ using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; +using Sonarr.Http.ClientSchema; using LogLevel = Microsoft.Extensions.Logging.LogLevel; using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions; @@ -145,6 +146,8 @@ namespace NzbDrone.Host .AddNzbDroneLogger() .AddDatabase() .AddStartupContext(context); + + SchemaBuilder.Initialize(c); }) .ConfigureServices(services => { diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 4bcc75e60..eb18394fc 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -158,6 +158,8 @@ namespace NzbDrone.Host { { apikeyQuery, Array.Empty<string>() } }); + + c.DescribeAllParametersInCamelCase(); }); services diff --git a/src/NzbDrone.Integration.Test/ApiTests/SeriesEditorFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/SeriesEditorFixture.cs index 7293ce97e..0faaa429a 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/SeriesEditorFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/SeriesEditorFixture.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Integration.Test.ApiTests { private void GivenExistingSeries() { - WaitForCompletion(() => Profiles.All().Count > 0); + WaitForCompletion(() => QualityProfiles.All().Count > 0); foreach (var title in new[] { "90210", "Dexter" }) { diff --git a/src/NzbDrone.Integration.Test/ApiTests/WantedTests/CutoffUnmetFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/WantedTests/CutoffUnmetFixture.cs index 1a75068e4..97dff6948 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/WantedTests/CutoffUnmetFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/WantedTests/CutoffUnmetFixture.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests [Order(1)] public void cutoff_should_have_monitored_items() { - EnsureProfileCutoff(1, Quality.HDTV720p, true); + EnsureQualityProfileCutoff(1, Quality.HDTV720p, true); var series = EnsureSeries(266189, "The Blacklist", true); EnsureEpisodeFile(series, 1, 1, Quality.SDTV); @@ -25,7 +25,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests [Order(1)] public void cutoff_should_not_have_unmonitored_items() { - EnsureProfileCutoff(1, Quality.HDTV720p, true); + EnsureQualityProfileCutoff(1, Quality.HDTV720p, true); var series = EnsureSeries(266189, "The Blacklist", false); EnsureEpisodeFile(series, 1, 1, Quality.SDTV); @@ -38,7 +38,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests [Order(1)] public void cutoff_should_have_series() { - EnsureProfileCutoff(1, Quality.HDTV720p, true); + EnsureQualityProfileCutoff(1, Quality.HDTV720p, true); var series = EnsureSeries(266189, "The Blacklist", true); EnsureEpisodeFile(series, 1, 1, Quality.SDTV); @@ -52,11 +52,11 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests [Order(2)] public void cutoff_should_have_unmonitored_items() { - EnsureProfileCutoff(1, Quality.HDTV720p, true); + EnsureQualityProfileCutoff(1, Quality.HDTV720p, true); var series = EnsureSeries(266189, "The Blacklist", false); EnsureEpisodeFile(series, 1, 1, Quality.SDTV); - var result = WantedCutoffUnmet.GetPaged(0, 15, "airDateUtc", "desc", "monitored", "false"); + var result = WantedCutoffUnmet.GetPaged(0, 15, "airDateUtc", "desc", "monitored", false); result.Records.Should().NotBeEmpty(); } diff --git a/src/NzbDrone.Integration.Test/ApiTests/WantedTests/MissingFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/WantedTests/MissingFixture.cs index a91b954f7..bc6ef9dfa 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/WantedTests/MissingFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/WantedTests/MissingFixture.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Integration.Test.ApiTests.WantedTests { EnsureSeries(266189, "The Blacklist", false); - var result = WantedMissing.GetPaged(0, 15, "airDateUtc", "desc", "monitored", "false"); + var result = WantedMissing.GetPaged(0, 15, "airDateUtc", "desc", "monitored", false); result.Records.Should().NotBeEmpty(); } diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 8e2091f94..aa45059f6 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Integration.Test.Client return Get<List<TResource>>(request); } - public PagingResource<TResource> GetPaged(int pageNumber, int pageSize, string sortKey, string sortDir, string filterKey = null, string filterValue = null) + public PagingResource<TResource> GetPaged(int pageNumber, int pageSize, string sortKey, string sortDir, string filterKey = null, object filterValue = null) { var request = BuildRequest(); request.AddParameter("page", pageNumber); @@ -113,8 +113,7 @@ namespace NzbDrone.Integration.Test.Client if (filterKey != null && filterValue != null) { - request.AddParameter("filterKey", filterKey); - request.AddParameter("filterValue", filterValue); + request.AddParameter(filterKey, filterValue); } return Get<PagingResource<TResource>>(request); diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index a6721fdcb..9524d39d3 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using System.Threading; using NLog; using NUnit.Framework; @@ -7,7 +9,6 @@ using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; using NzbDrone.Test.Common.Datastore; -using Sonarr.Http.ClientSchema; namespace NzbDrone.Integration.Test { @@ -50,17 +51,20 @@ namespace NzbDrone.Integration.Test // Make sure tasks have been initialized so the config put below doesn't cause errors WaitForCompletion(() => Tasks.All().SelectList(x => x.TaskName).Contains("RssSync")); - Indexers.Post(new Sonarr.Api.V3.Indexers.IndexerResource + var indexer = Indexers.Schema().FirstOrDefault(i => i.Implementation == nameof(Newznab)); + + if (indexer == null) { - EnableRss = false, - EnableInteractiveSearch = false, - EnableAutomaticSearch = false, - ConfigContract = nameof(NewznabSettings), - Implementation = nameof(Newznab), - Name = "NewznabTest", - Protocol = Core.Indexers.DownloadProtocol.Usenet, - Fields = SchemaBuilder.ToSchema(new NewznabSettings()) - }); + throw new NullReferenceException("Expected valid indexer schema, found null"); + } + + indexer.EnableRss = false; + indexer.EnableInteractiveSearch = false; + indexer.EnableAutomaticSearch = false; + indexer.ConfigContract = nameof(NewznabSettings); + indexer.Implementation = nameof(Newznab); + indexer.Name = "NewznabTest"; + indexer.Protocol = Core.Indexers.DownloadProtocol.Usenet; // Change Console Log Level to Debug so we get more details. var config = HostConfig.Get(1); diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index a1c086790..3e8a8a2b0 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Integration.Test public LogsClient Logs; public ClientBase<NamingConfigResource> NamingConfig; public NotificationClient Notifications; - public ClientBase<QualityProfileResource> Profiles; + public ClientBase<QualityProfileResource> QualityProfiles; public ReleaseClient Releases; public ReleasePushClient ReleasePush; public ClientBase<RootFolderResource> RootFolders; @@ -113,7 +113,7 @@ namespace NzbDrone.Integration.Test Logs = new LogsClient(RestClient, ApiKey); NamingConfig = new ClientBase<NamingConfigResource>(RestClient, ApiKey, "config/naming"); Notifications = new NotificationClient(RestClient, ApiKey); - Profiles = new ClientBase<QualityProfileResource>(RestClient, ApiKey); + QualityProfiles = new ClientBase<QualityProfileResource>(RestClient, ApiKey); Releases = new ReleaseClient(RestClient, ApiKey); ReleasePush = new ReleasePushClient(RestClient, ApiKey); RootFolders = new ClientBase<RootFolderResource>(RestClient, ApiKey); @@ -317,10 +317,10 @@ namespace NzbDrone.Integration.Test return result.EpisodeFile; } - public QualityProfileResource EnsureProfileCutoff(int profileId, Quality cutoff, bool upgradeAllowed) + public QualityProfileResource EnsureQualityProfileCutoff(int profileId, Quality cutoff, bool upgradeAllowed) { var needsUpdate = false; - var profile = Profiles.Get(profileId); + var profile = QualityProfiles.Get(profileId); if (profile.Cutoff != cutoff.Id) { @@ -336,7 +336,7 @@ namespace NzbDrone.Integration.Test if (needsUpdate) { - profile = Profiles.Put(profile); + profile = QualityProfiles.Put(profile); } return profile; diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 6e6d84666..948994c51 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -23,6 +23,8 @@ namespace NzbDrone.Test.Common.AutoMoq SetupAutoMoqer(CreateTestContainer(new Container())); } + public IContainer Container => _container; + public virtual T Resolve<T>() { var result = _container.Resolve<T>(); diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs index b5c9392b8..2091d3cfe 100644 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs @@ -23,9 +23,9 @@ namespace Sonarr.Api.V3.Blocklist [HttpGet] [Produces("application/json")] - public PagingResource<BlocklistResource> GetBlocklist() + public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging) { - var pagingResource = Request.ReadPagingResourceFromRequest<BlocklistResource>(); + var pagingResource = new PagingResource<BlocklistResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending); return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator)); diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs index 17c98e660..eb7d76152 100644 --- a/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; using Sonarr.Http.REST; namespace Sonarr.Api.V3.Config @@ -9,6 +9,7 @@ namespace Sonarr.Api.V3.Config public bool EnableCompletedDownloadHandling { get; set; } public bool AutoRedownloadFailed { get; set; } + public bool AutoRedownloadFailedFromInteractiveSearch { get; set; } } public static class DownloadClientConfigResourceMapper @@ -20,7 +21,8 @@ namespace Sonarr.Api.V3.Config DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, - AutoRedownloadFailed = model.AutoRedownloadFailed + AutoRedownloadFailed = model.AutoRedownloadFailed, + AutoRedownloadFailedFromInteractiveSearch = model.AutoRedownloadFailedFromInteractiveSearch }; } } diff --git a/src/Sonarr.Api.V3/Config/HostConfigController.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs index 333212aff..50ef5c900 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigController.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -47,6 +47,9 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Basic || c.AuthenticationMethod == AuthenticationType.Forms); + SharedValidator.RuleFor(c => c.PasswordConfirmation) + .Must((resource, p) => IsMatchingPassword(resource)).WithMessage("Must match Password"); + SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl); @@ -81,6 +84,23 @@ namespace Sonarr.Api.V3.Config return cert != null; } + private bool IsMatchingPassword(HostConfigResource resource) + { + var user = _userService.FindUser(); + + if (user != null && user.Password == resource.Password) + { + return true; + } + + if (resource.Password == resource.PasswordConfirmation) + { + return true; + } + + return false; + } + protected override HostConfigResource GetResourceById(int id) { return GetHostConfig(); @@ -93,11 +113,10 @@ namespace Sonarr.Api.V3.Config resource.Id = 1; var user = _userService.FindUser(); - if (user != null) - { - resource.Username = user.Username; - resource.Password = user.Password; - } + + resource.Username = user?.Username ?? string.Empty; + resource.Password = user?.Password ?? string.Empty; + resource.PasswordConfirmation = string.Empty; return resource; } diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs index 3e25ae477..1b400800b 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -19,6 +19,7 @@ namespace Sonarr.Api.V3.Config public bool AnalyticsEnabled { get; set; } public string Username { get; set; } public string Password { get; set; } + public string PasswordConfirmation { get; set; } public string LogLevel { get; set; } public string ConsoleLogLevel { get; set; } public string Branch { get; set; } diff --git a/src/Sonarr.Api.V3/History/HistoryController.cs b/src/Sonarr.Api.V3/History/HistoryController.cs index cb6075676..991b2bb21 100644 --- a/src/Sonarr.Api.V3/History/HistoryController.cs +++ b/src/Sonarr.Api.V3/History/HistoryController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; @@ -61,34 +62,33 @@ namespace Sonarr.Api.V3.History [HttpGet] [Produces("application/json")] - public PagingResource<HistoryResource> GetHistory(bool includeSeries, bool includeEpisode) + public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, int? eventType, int? episodeId, string downloadId, [FromQuery] int[] seriesIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null) { - var pagingResource = Request.ReadPagingResourceFromRequest<HistoryResource>(); + var pagingResource = new PagingResource<HistoryResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EpisodeHistory>("date", SortDirection.Descending); - var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); - var episodeIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "episodeId"); - var downloadIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "downloadId"); - - if (eventTypeFilter != null) + if (eventType.HasValue) { - var filterValue = (EpisodeHistoryEventType)Convert.ToInt32(eventTypeFilter.Value); + var filterValue = (EpisodeHistoryEventType)eventType.Value; pagingSpec.FilterExpressions.Add(v => v.EventType == filterValue); } - if (episodeIdFilter != null) + if (episodeId.HasValue) { - var episodeId = Convert.ToInt32(episodeIdFilter.Value); pagingSpec.FilterExpressions.Add(h => h.EpisodeId == episodeId); } - if (downloadIdFilter != null) + if (downloadId.IsNotNullOrWhiteSpace()) { - var downloadId = downloadIdFilter.Value; pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); } - return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeSeries, includeEpisode)); + if (seriesIds != null && seriesIds.Any()) + { + pagingSpec.FilterExpressions.Add(h => seriesIds.Contains(h.SeriesId)); + } + + return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeSeries, includeEpisode)); } [HttpGet("since")] diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs index 8ea3887dd..542158d6a 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs @@ -11,13 +11,13 @@ namespace Sonarr.Api.V3.ImportLists public static readonly ImportListResourceMapper ResourceMapper = new (); public static readonly ImportListBulkResourceMapper BulkResourceMapper = new (); - public ImportListController(IImportListFactory importListFactory, ProfileExistsValidator profileExistsValidator) + public ImportListController(IImportListFactory importListFactory, QualityProfileExistsValidator qualityProfileExistsValidator) : base(importListFactory, "importlist", ResourceMapper, BulkResourceMapper) { Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath(); - SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator); } } } diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs b/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs index dad382ca2..4e4fcac11 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs @@ -7,7 +7,9 @@ namespace Sonarr.Api.V3.ImportLists public class ImportListResource : ProviderResource<ImportListResource> { public bool EnableAutomaticAdd { get; set; } + public bool SearchForMissingEpisodes { get; set; } public MonitorTypes ShouldMonitor { get; set; } + public NewItemMonitorTypes MonitorNewItems { get; set; } public string RootFolderPath { get; set; } public int QualityProfileId { get; set; } public SeriesTypes SeriesType { get; set; } @@ -29,7 +31,9 @@ namespace Sonarr.Api.V3.ImportLists var resource = base.ToResource(definition); resource.EnableAutomaticAdd = definition.EnableAutomaticAdd; + resource.SearchForMissingEpisodes = definition.SearchForMissingEpisodes; resource.ShouldMonitor = definition.ShouldMonitor; + resource.MonitorNewItems = definition.MonitorNewItems; resource.RootFolderPath = definition.RootFolderPath; resource.QualityProfileId = definition.QualityProfileId; resource.SeriesType = definition.SeriesType; @@ -51,7 +55,9 @@ namespace Sonarr.Api.V3.ImportLists var definition = base.ToModel(resource, existingDefinition); definition.EnableAutomaticAdd = resource.EnableAutomaticAdd; + definition.SearchForMissingEpisodes = resource.SearchForMissingEpisodes; definition.ShouldMonitor = resource.ShouldMonitor; + definition.MonitorNewItems = resource.MonitorNewItems; definition.RootFolderPath = resource.RootFolderPath; definition.QualityProfileId = resource.QualityProfileId; definition.SeriesType = resource.SeriesType; diff --git a/src/Sonarr.Api.V3/Indexers/IndexerController.cs b/src/Sonarr.Api.V3/Indexers/IndexerController.cs index 444993c2f..fffbc4d96 100644 --- a/src/Sonarr.Api.V3/Indexers/IndexerController.cs +++ b/src/Sonarr.Api.V3/Indexers/IndexerController.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Indexers; +using NzbDrone.Core.Validation; using Sonarr.Http; namespace Sonarr.Api.V3.Indexers @@ -9,9 +10,10 @@ namespace Sonarr.Api.V3.Indexers public static readonly IndexerResourceMapper ResourceMapper = new (); public static readonly IndexerBulkResourceMapper BulkResourceMapper = new (); - public IndexerController(IndexerFactory indexerFactory) + public IndexerController(IndexerFactory indexerFactory, DownloadClientExistsValidator downloadClientExistsValidator) : base(indexerFactory, "indexer", ResourceMapper, BulkResourceMapper) { + SharedValidator.RuleFor(c => c.DownloadClientId).SetValidator(downloadClientExistsValidator); } } } diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index 17358edd4..efd553793 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.Indexers private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IProcessDownloadDecisions _downloadDecisionProcessor; private readonly IIndexerFactory _indexerFactory; + private readonly IDownloadClientFactory _downloadClientFactory; private readonly Logger _logger; private static readonly object PushLock = new object(); @@ -28,6 +29,7 @@ namespace Sonarr.Api.V3.Indexers public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker, IProcessDownloadDecisions downloadDecisionProcessor, IIndexerFactory indexerFactory, + IDownloadClientFactory downloadClientFactory, IQualityProfileService qualityProfileService, Logger logger) : base(qualityProfileService) @@ -35,6 +37,7 @@ namespace Sonarr.Api.V3.Indexers _downloadDecisionMaker = downloadDecisionMaker; _downloadDecisionProcessor = downloadDecisionProcessor; _indexerFactory = indexerFactory; + _downloadClientFactory = downloadClientFactory; _logger = logger; PostValidator.RuleFor(s => s.Title).NotEmpty(); @@ -57,22 +60,25 @@ namespace Sonarr.Api.V3.Indexers ResolveIndexer(info); - List<DownloadDecision> decisions; + var downloadClientId = ResolveDownloadClientId(release); + + DownloadDecision decision; lock (PushLock) { - decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info }); - _downloadDecisionProcessor.ProcessDecisions(decisions).GetAwaiter().GetResult(); + var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info }); + + decision = decisions.FirstOrDefault(); + + _downloadDecisionProcessor.ProcessDecision(decision, downloadClientId).GetAwaiter().GetResult(); } - var firstDecision = decisions.FirstOrDefault(); - - if (firstDecision?.RemoteEpisode.ParsedEpisodeInfo == null) + if (decision?.RemoteEpisode.ParsedEpisodeInfo == null) { - throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("Title", "Unable to parse", release.Title) }); + throw new ValidationException(new List<ValidationFailure> { new ("Title", "Unable to parse", release.Title) }); } - return MapDecisions(new[] { firstDecision }); + return MapDecisions(new[] { decision }); } private void ResolveIndexer(ReleaseInfo release) @@ -109,5 +115,26 @@ namespace Sonarr.Api.V3.Indexers _logger.Debug("Push Release {0} not associated with an indexer.", release.Title); } } + + private int? ResolveDownloadClientId(ReleaseResource release) + { + var downloadClientId = release.DownloadClientId.GetValueOrDefault(); + + if (downloadClientId == 0 && release.DownloadClient.IsNotNullOrWhiteSpace()) + { + var downloadClient = _downloadClientFactory.All().FirstOrDefault(v => v.Name.EqualsIgnoreCase(release.DownloadClient)); + + if (downloadClient != null) + { + _logger.Debug("Push Release {0} associated with download client {1} - {2}.", release.Title, downloadClientId, release.DownloadClient); + + return downloadClient.Id; + } + + _logger.Debug("Push Release {0} not associated with known download client {1}.", release.Title, release.DownloadClient); + } + + return release.DownloadClientId; + } } } diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index 578e652f2..a15a1b234 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -85,6 +85,9 @@ namespace Sonarr.Api.V3.Indexers [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int? DownloadClientId { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string DownloadClient { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool? ShouldOverride { get; set; } } diff --git a/src/Sonarr.Api.V3/Logs/LogController.cs b/src/Sonarr.Api.V3/Logs/LogController.cs index 3350bcce3..e838bc7e6 100644 --- a/src/Sonarr.Api.V3/Logs/LogController.cs +++ b/src/Sonarr.Api.V3/Logs/LogController.cs @@ -1,5 +1,5 @@ -using System.Linq; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Instrumentation; using Sonarr.Http; using Sonarr.Http.Extensions; @@ -18,9 +18,9 @@ namespace Sonarr.Api.V3.Logs [HttpGet] [Produces("application/json")] - public PagingResource<LogResource> GetLogs() + public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string level) { - var pagingResource = Request.ReadPagingResourceFromRequest<LogResource>(); + var pagingResource = new PagingResource<LogResource>(paging); var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(); if (pageSpec.SortKey == "time") @@ -28,11 +28,9 @@ namespace Sonarr.Api.V3.Logs pageSpec.SortKey = "id"; } - var levelFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "level"); - - if (levelFilter != null) + if (level.IsNotNullOrWhiteSpace()) { - switch (levelFilter.Value) + switch (level) { case "fatal": pageSpec.FilterExpressions.Add(h => h.Level == "Fatal"); diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs index 3db337213..5046ff2ee 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Validators; @@ -17,6 +17,8 @@ namespace Sonarr.Api.V3.Profiles.Quality ruleBuilder.SetValidator(new ItemGroupIdValidator<T>()); ruleBuilder.SetValidator(new UniqueIdValidator<T>()); ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>()); + ruleBuilder.SetValidator(new AllQualitiesValidator<T>()); + return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>()); } } @@ -151,4 +153,46 @@ namespace Sonarr.Api.V3.Profiles.Quality return true; } } + + public class AllQualitiesValidator<T> : PropertyValidator + { + protected override string GetDefaultMessageTemplate() => "Must contain all qualities"; + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items) + { + return false; + } + + var qualityIds = new HashSet<int>(); + + foreach (var item in items) + { + if (item.Id > 0) + { + foreach (var quality in item.Items) + { + qualityIds.Add(quality.Quality.Id); + } + } + else + { + qualityIds.Add(item.Quality.Id); + } + } + + var allQualityIds = NzbDrone.Core.Qualities.Quality.All; + + foreach (var quality in allQualityIds) + { + if (!qualityIds.Contains(quality.Id)) + { + return false; + } + } + + return true; + } + } } diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs index d7c24fcf9..87f39b5ad 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.Profiles.Quality SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); SharedValidator.RuleFor(c => c.Items).ValidItems(); + SharedValidator.RuleFor(c => c.FormatItems).Must(items => { var all = _formatService.All().Select(f => f.Id).ToList(); @@ -32,6 +33,7 @@ namespace Sonarr.Api.V3.Profiles.Quality return all.Except(ids).Empty(); }).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser."); + SharedValidator.RuleFor(c => c).Custom((profile, context) => { if (profile.FormatItems.Where(x => x.Score > 0).Sum(x => x.Score) < profile.MinFormatScore && diff --git a/src/Sonarr.Api.V3/ProviderControllerBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs index c5f6a5bcb..ca1082609 100644 --- a/src/Sonarr.Api.V3/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -70,11 +70,11 @@ namespace Sonarr.Api.V3 [Produces("application/json")] public ActionResult<TProviderResource> CreateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) { - var providerDefinition = GetDefinition(providerResource, true, !forceSave, false); + var providerDefinition = GetDefinition(providerResource, null, true, !forceSave, false); if (providerDefinition.Enable) { - Test(providerDefinition, false); + Test(providerDefinition, !forceSave); } providerDefinition = _providerFactory.Create(providerDefinition); @@ -87,15 +87,24 @@ namespace Sonarr.Api.V3 [Produces("application/json")] public ActionResult<TProviderResource> UpdateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) { - var providerDefinition = GetDefinition(providerResource, true, !forceSave, false); + var existingDefinition = _providerFactory.Find(providerResource.Id); + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false); - // Only test existing definitions if it is enabled and forceSave isn't set. - if (providerDefinition.Enable && !forceSave) + // Comparing via JSON string to eliminate the need for every provider implementation to implement equality checks. + // Compare settings separately because they are not serialized with the definition. + var hasDefinitionChanged = STJson.ToJson(existingDefinition) != STJson.ToJson(providerDefinition) || + STJson.ToJson(existingDefinition.Settings) != STJson.ToJson(providerDefinition.Settings); + + // Only test existing definitions if it is enabled and forceSave isn't set and the definition has changed. + if (providerDefinition.Enable && !forceSave && hasDefinitionChanged) { - Test(providerDefinition, false); + Test(providerDefinition, true); } - _providerFactory.Update(providerDefinition); + if (hasDefinitionChanged) + { + _providerFactory.Update(providerDefinition); + } return Accepted(providerResource.Id); } @@ -141,9 +150,8 @@ namespace Sonarr.Api.V3 return Accepted(_providerFactory.Update(definitionsToUpdate).Select(x => _resourceMapper.ToResource(x))); } - private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate) + private TProviderDefinition GetDefinition(TProviderResource providerResource, TProviderDefinition existingDefinition, bool validate, bool includeWarnings, bool forceValidate) { - var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; var definition = _resourceMapper.ToModel(providerResource, existingDefinition); if (validate && (definition.Enable || forceValidate)) @@ -199,7 +207,8 @@ namespace Sonarr.Api.V3 [Consumes("application/json")] public object Test([FromBody] TProviderResource providerResource) { - var providerDefinition = GetDefinition(providerResource, true, true, true); + var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, true, true); Test(providerDefinition, true); @@ -236,9 +245,10 @@ namespace Sonarr.Api.V3 [HttpPost("action/{name}")] [Consumes("application/json")] [Produces("application/json")] - public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) + public IActionResult RequestAction(string name, [FromBody] TProviderResource providerResource) { - var providerDefinition = GetDefinition(resource, false, false, false); + var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; + var providerDefinition = GetDefinition(providerResource, existingDefinition, false, false, false); var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 6259956c3..c3e4c1d52 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles.Qualities; @@ -135,15 +136,15 @@ namespace Sonarr.Api.V3.Queue [HttpGet] [Produces("application/json")] - public PagingResource<QueueResource> GetQueue(bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false) + public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, int? quality = null) { - var pagingResource = Request.ReadPagingResourceFromRequest<QueueResource>(); + var pagingResource = new PagingResource<QueueResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending); - return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } - private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, bool includeUnknownSeriesItems) + private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> seriesIds, DownloadProtocol? protocol, HashSet<int> languages, int? quality, bool includeUnknownSeriesItems) { var ascending = pagingSpec.SortDirection == SortDirection.Ascending; var orderByFunc = GetOrderByFunc(pagingSpec); @@ -151,7 +152,36 @@ namespace Sonarr.Api.V3.Queue var queue = _queueService.GetQueue(); var filteredQueue = includeUnknownSeriesItems ? queue : queue.Where(q => q.Series != null); var pending = _pendingReleaseService.GetPendingQueue(); - var fullQueue = filteredQueue.Concat(pending).ToList(); + + var hasSeriesIdFilter = seriesIds.Any(); + var hasLanguageFilter = languages.Any(); + var fullQueue = filteredQueue.Concat(pending).Where(q => + { + var include = true; + + if (hasSeriesIdFilter) + { + include &= q.Series != null && seriesIds.Contains(q.Series.Id); + } + + if (include && protocol.HasValue) + { + include &= q.Protocol == protocol.Value; + } + + if (include && hasLanguageFilter) + { + include &= q.Languages.Any(l => languages.Contains(l.Id)); + } + + if (include && quality.HasValue) + { + include &= q.Quality.Quality.Id == quality.Value; + } + + return include; + }).ToList(); + IOrderedEnumerable<NzbDrone.Core.Queue.Queue> ordered; if (pagingSpec.SortKey == "timeleft") diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 9f6c6823d..1a920a6e0 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -58,7 +58,7 @@ namespace Sonarr.Api.V3.Series SeriesExistsValidator seriesExistsValidator, SeriesAncestorValidator seriesAncestorValidator, SystemFolderValidator systemFolderValidator, - ProfileExistsValidator profileExistsValidator, + QualityProfileExistsValidator qualityProfileExistsValidator, SeriesFolderAsRootFolderValidator seriesFolderAsRootFolderValidator) : base(signalRBroadcaster) { @@ -83,7 +83,7 @@ namespace Sonarr.Api.V3.Series .SetValidator(systemFolderValidator) .When(s => !s.Path.IsNullOrWhiteSpace()); - SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator); PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath) @@ -245,6 +245,9 @@ namespace Sonarr.Api.V3.Series private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seriesStatistics) { + // Only set last aired from statistics if it's missing from the series itself + resource.LastAired ??= seriesStatistics.LastAired; + resource.PreviousAiring = seriesStatistics.PreviousAiring; resource.NextAiring = seriesStatistics.NextAiring; resource.Statistics = seriesStatistics.ToResource(resource.Seasons); diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorController.cs b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs index 99927887b..cea1220e1 100644 --- a/src/Sonarr.Api.V3/Series/SeriesEditorController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs @@ -34,6 +34,11 @@ namespace Sonarr.Api.V3.Series series.Monitored = resource.Monitored.Value; } + if (resource.MonitorNewItems.HasValue) + { + series.MonitorNewItems = resource.MonitorNewItems.Value; + } + if (resource.QualityProfileId.HasValue) { series.QualityProfileId = resource.QualityProfileId.Value; diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs b/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs index 368251a93..c1d8a53fb 100644 --- a/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs +++ b/src/Sonarr.Api.V3/Series/SeriesEditorResource.cs @@ -7,6 +7,7 @@ namespace Sonarr.Api.V3.Series { public List<int> SeriesIds { get; set; } public bool? Monitored { get; set; } + public NewItemMonitorTypes? MonitorNewItems { get; set; } public int? QualityProfileId { get; set; } public SeriesTypes? SeriesType { get; set; } public bool? SeasonFolder { get; set; } diff --git a/src/Sonarr.Api.V3/Series/SeriesResource.cs b/src/Sonarr.Api.V3/Series/SeriesResource.cs index c57e52af5..e1c87a434 100644 --- a/src/Sonarr.Api.V3/Series/SeriesResource.cs +++ b/src/Sonarr.Api.V3/Series/SeriesResource.cs @@ -44,6 +44,7 @@ namespace Sonarr.Api.V3.Series // Editing Only public bool SeasonFolder { get; set; } public bool Monitored { get; set; } + public NewItemMonitorTypes MonitorNewItems { get; set; } public bool UseSceneNumbering { get; set; } public int Runtime { get; set; } @@ -51,6 +52,7 @@ namespace Sonarr.Api.V3.Series public int TvRageId { get; set; } public int TvMazeId { get; set; } public DateTime? FirstAired { get; set; } + public DateTime? LastAired { get; set; } public SeriesTypes SeriesType { get; set; } public string CleanTitle { get; set; } public string ImdbId { get; set; } @@ -114,6 +116,7 @@ namespace Sonarr.Api.V3.Series SeasonFolder = model.SeasonFolder, Monitored = model.Monitored, + MonitorNewItems = model.MonitorNewItems, UseSceneNumbering = model.UseSceneNumbering, Runtime = model.Runtime, @@ -121,6 +124,7 @@ namespace Sonarr.Api.V3.Series TvRageId = model.TvRageId, TvMazeId = model.TvMazeId, FirstAired = model.FirstAired, + LastAired = model.LastAired, SeriesType = model.SeriesType, CleanTitle = model.CleanTitle, ImdbId = model.ImdbId, @@ -176,6 +180,7 @@ namespace Sonarr.Api.V3.Series SeasonFolder = resource.SeasonFolder, Monitored = resource.Monitored, + MonitorNewItems = resource.MonitorNewItems, UseSceneNumbering = resource.UseSceneNumbering, Runtime = resource.Runtime, diff --git a/src/Sonarr.Api.V3/Wanted/CutoffController.cs b/src/Sonarr.Api.V3/Wanted/CutoffController.cs index b906399b0..22be80366 100644 --- a/src/Sonarr.Api.V3/Wanted/CutoffController.cs +++ b/src/Sonarr.Api.V3/Wanted/CutoffController.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -29,9 +28,9 @@ namespace Sonarr.Api.V3.Wanted [HttpGet] [Produces("application/json")] - public PagingResource<EpisodeResource> GetCutoffUnmetEpisodes(bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false) + public PagingResource<EpisodeResource> GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false, bool monitored = true) { - var pagingResource = Request.ReadPagingResourceFromRequest<EpisodeResource>(); + var pagingResource = new PagingResource<EpisodeResource>(paging); var pagingSpec = new PagingSpec<Episode> { Page = pagingResource.Page, @@ -40,15 +39,13 @@ namespace Sonarr.Api.V3.Wanted SortDirection = pagingResource.SortDirection }; - var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); - - if (filter != null && filter.Value == "false") + if (monitored) { - pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } else { - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } var resource = pagingSpec.ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); diff --git a/src/Sonarr.Api.V3/Wanted/MissingController.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs index 6ed40dda7..f7444f7a3 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -25,9 +24,9 @@ namespace Sonarr.Api.V3.Wanted [HttpGet] [Produces("application/json")] - public PagingResource<EpisodeResource> GetMissingEpisodes(bool includeSeries = false, bool includeImages = false) + public PagingResource<EpisodeResource> GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeImages = false, bool monitored = true) { - var pagingResource = Request.ReadPagingResourceFromRequest<EpisodeResource>(); + var pagingResource = new PagingResource<EpisodeResource>(paging); var pagingSpec = new PagingSpec<Episode> { Page = pagingResource.Page, @@ -36,15 +35,13 @@ namespace Sonarr.Api.V3.Wanted SortDirection = pagingResource.SortDirection }; - var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); - - if (monitoredFilter != null && monitoredFilter.Value == "false") + if (monitored) { - pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); + pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } else { - pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); + pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages)); diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index ef261ba3c..4c4f2e5f5 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -59,25 +59,25 @@ "schema": { "type": "object", "properties": { - "Username": { + "username": { "type": "string" }, - "Password": { + "password": { "type": "string" }, - "RememberMe": { + "rememberMe": { "type": "string" } } }, "encoding": { - "Username": { + "username": { "style": "form" }, - "Password": { + "password": { "style": "form" }, - "RememberMe": { + "rememberMe": { "style": "form" } } @@ -381,6 +381,40 @@ "tags": [ "Blocklist" ], + "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", @@ -1050,6 +1084,38 @@ "Cutoff" ], "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" + } + }, { "name": "includeSeries", "in": "query", @@ -1073,6 +1139,14 @@ "type": "boolean", "default": false } + }, + { + "name": "monitored", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { @@ -2220,6 +2294,38 @@ "History" ], "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" + } + }, { "name": "includeSeries", "in": "query", @@ -2233,6 +2339,62 @@ "schema": { "type": "boolean" } + }, + { + "name": "eventType", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "episodeId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "downloadId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "seriesIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "languages", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "quality", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } } ], "responses": { @@ -3627,6 +3789,47 @@ "tags": [ "Log" ], + "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" + } + }, + { + "name": "level", + "in": "query", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", @@ -4142,6 +4345,38 @@ "Missing" ], "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" + } + }, { "name": "includeSeries", "in": "query", @@ -4157,6 +4392,14 @@ "type": "boolean", "default": false } + }, + { + "name": "monitored", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { @@ -4325,21 +4568,21 @@ ], "parameters": [ { - "name": "RenameEpisodes", + "name": "renameEpisodes", "in": "query", "schema": { "type": "boolean" } }, { - "name": "ReplaceIllegalCharacters", + "name": "replaceIllegalCharacters", "in": "query", "schema": { "type": "boolean" } }, { - "name": "ColonReplacementFormat", + "name": "colonReplacementFormat", "in": "query", "schema": { "type": "integer", @@ -4347,7 +4590,7 @@ } }, { - "name": "MultiEpisodeStyle", + "name": "multiEpisodeStyle", "in": "query", "schema": { "type": "integer", @@ -4355,91 +4598,91 @@ } }, { - "name": "StandardEpisodeFormat", + "name": "standardEpisodeFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "DailyEpisodeFormat", + "name": "dailyEpisodeFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "AnimeEpisodeFormat", + "name": "animeEpisodeFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "SeriesFolderFormat", + "name": "seriesFolderFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "SeasonFolderFormat", + "name": "seasonFolderFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "SpecialsFolderFormat", + "name": "specialsFolderFormat", "in": "query", "schema": { "type": "string" } }, { - "name": "IncludeSeriesTitle", + "name": "includeSeriesTitle", "in": "query", "schema": { "type": "boolean" } }, { - "name": "IncludeEpisodeTitle", + "name": "includeEpisodeTitle", "in": "query", "schema": { "type": "boolean" } }, { - "name": "IncludeQuality", + "name": "includeQuality", "in": "query", "schema": { "type": "boolean" } }, { - "name": "ReplaceSpaces", + "name": "replaceSpaces", "in": "query", "schema": { "type": "boolean" } }, { - "name": "Separator", + "name": "separator", "in": "query", "schema": { "type": "string" } }, { - "name": "NumberStyle", + "name": "numberStyle", "in": "query", "schema": { "type": "string" } }, { - "name": "Id", + "name": "id", "in": "query", "schema": { "type": "integer", @@ -4447,7 +4690,7 @@ } }, { - "name": "ResourceName", + "name": "resourceName", "in": "query", "schema": { "type": "string" @@ -5212,6 +5455,38 @@ "Queue" ], "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" + } + }, { "name": "includeUnknownSeriesItems", "in": "query", @@ -5235,6 +5510,43 @@ "type": "boolean", "default": false } + }, + { + "name": "seriesIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "protocol", + "in": "query", + "schema": { + "$ref": "#/components/schemas/DownloadProtocol" + } + }, + { + "name": "languages", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "quality", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { @@ -7185,13 +7497,6 @@ "sortDirection": { "$ref": "#/components/schemas/SortDirection" }, - "filters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PagingResourceFilter" - }, - "nullable": true - }, "totalRecords": { "type": "integer", "format": "int32" @@ -7621,6 +7926,9 @@ }, "autoRedownloadFailed": { "type": "boolean" + }, + "autoRedownloadFailedFromInteractiveSearch": { + "type": "boolean" } }, "additionalProperties": false @@ -7951,13 +8259,6 @@ "sortDirection": { "$ref": "#/components/schemas/SortDirection" }, - "filters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PagingResourceFilter" - }, - "nullable": true - }, "totalRecords": { "type": "integer", "format": "int32" @@ -8200,13 +8501,6 @@ "sortDirection": { "$ref": "#/components/schemas/SortDirection" }, - "filters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PagingResourceFilter" - }, - "nullable": true - }, "totalRecords": { "type": "integer", "format": "int32" @@ -8263,6 +8557,10 @@ "type": "string", "nullable": true }, + "passwordConfirmation": { + "type": "string", + "nullable": true + }, "logLevel": { "type": "string", "nullable": true @@ -8510,9 +8808,15 @@ "enableAutomaticAdd": { "type": "boolean" }, + "searchForMissingEpisodes": { + "type": "boolean" + }, "shouldMonitor": { "$ref": "#/components/schemas/MonitorTypes" }, + "monitorNewItems": { + "$ref": "#/components/schemas/NewItemMonitorTypes" + }, "rootFolderPath": { "type": "string", "nullable": true @@ -8891,13 +9195,6 @@ "sortDirection": { "$ref": "#/components/schemas/SortDirection" }, - "filters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PagingResourceFilter" - }, - "nullable": true - }, "totalRecords": { "type": "integer", "format": "int32" @@ -9319,8 +9616,10 @@ "missing", "existing", "firstSeason", + "lastSeason", "latestSeason", "pilot", + "recent", "monitorSpecials", "unmonitorSpecials", "none" @@ -9410,6 +9709,13 @@ }, "additionalProperties": false }, + "NewItemMonitorTypes": { + "enum": [ + "all", + "none" + ], + "type": "string" + }, "NotificationResource": { "type": "object", "properties": { @@ -9549,20 +9855,6 @@ }, "additionalProperties": false }, - "PagingResourceFilter": { - "type": "object", - "properties": { - "key": { - "type": "string", - "nullable": true - }, - "value": { - "type": "string", - "nullable": true - } - }, - "additionalProperties": false - }, "ParseResource": { "type": "object", "properties": { @@ -10096,13 +10388,6 @@ "sortDirection": { "$ref": "#/components/schemas/SortDirection" }, - "filters": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PagingResourceFilter" - }, - "nullable": true - }, "totalRecords": { "type": "integer", "format": "int32" @@ -10504,6 +10789,10 @@ "format": "int32", "nullable": true }, + "downloadClient": { + "type": "string", + "nullable": true + }, "shouldOverride": { "type": "boolean", "nullable": true @@ -10774,6 +11063,9 @@ "type": "boolean", "nullable": true }, + "monitorNewItems": { + "$ref": "#/components/schemas/NewItemMonitorTypes" + }, "qualityProfileId": { "type": "integer", "format": "int32", @@ -10907,6 +11199,9 @@ "monitored": { "type": "boolean" }, + "monitorNewItems": { + "$ref": "#/components/schemas/NewItemMonitorTypes" + }, "useSceneNumbering": { "type": "boolean" }, @@ -10931,6 +11226,11 @@ "format": "date-time", "nullable": true }, + "lastAired": { + "type": "string", + "format": "date-time", + "nullable": true + }, "seriesType": { "$ref": "#/components/schemas/SeriesTypes" }, diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs index e6e0d6cd2..21d5b7009 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -40,6 +40,7 @@ namespace Sonarr.Http.Authentication options.LoginPath = "/login"; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.SlidingExpiration = true; + options.ReturnUrlParameter = "returnUrl"; }) .AddApiKey("API", options => { diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index 8d2392f2e..cd088677e 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; +using DryIoc; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Localization; namespace Sonarr.Http.ClientSchema { @@ -15,6 +17,12 @@ namespace Sonarr.Http.ClientSchema { private const string PRIVATE_VALUE = "********"; private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>(); + private static ILocalizationService _localizationService; + + public static void Initialize(IContainer container) + { + _localizationService = container.Resolve<ILocalizationService>(); + } public static List<Field> ToSchema(object model) { @@ -107,13 +115,27 @@ namespace Sonarr.Http.ClientSchema if (propertyInfo.PropertyType.IsSimpleType()) { var fieldAttribute = property.Item2; + + var label = fieldAttribute.Label.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.Label, + GetTokens(type, fieldAttribute.Label, TokenField.Label)) + : fieldAttribute.Label; + var helpText = fieldAttribute.HelpText.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.HelpText, + GetTokens(type, fieldAttribute.Label, TokenField.HelpText)) + : fieldAttribute.HelpText; + var helpTextWarning = fieldAttribute.HelpTextWarning.IsNotNullOrWhiteSpace() + ? _localizationService.GetLocalizedString(fieldAttribute.HelpTextWarning, + GetTokens(type, fieldAttribute.Label, TokenField.HelpTextWarning)) + : fieldAttribute.HelpTextWarning; + var field = new Field { Name = prefix + GetCamelCaseName(propertyInfo.Name), - Label = fieldAttribute.Label, + Label = label, Unit = fieldAttribute.Unit, - HelpText = fieldAttribute.HelpText, - HelpTextWarning = fieldAttribute.HelpTextWarning, + HelpText = helpText, + HelpTextWarning = helpTextWarning, HelpLink = fieldAttribute.HelpLink, Order = fieldAttribute.Order, Advanced = fieldAttribute.Advanced, @@ -173,6 +195,24 @@ namespace Sonarr.Http.ClientSchema .ToArray(); } + private static Dictionary<string, object> GetTokens(Type type, string label, TokenField field) + { + var tokens = new Dictionary<string, object>(); + + foreach (var propertyInfo in type.GetProperties()) + { + foreach (var attribute in propertyInfo.GetCustomAttributes(true)) + { + if (attribute is FieldTokenAttribute fieldTokenAttribute && fieldTokenAttribute.Field == field && fieldTokenAttribute.Label == label) + { + tokens.Add(fieldTokenAttribute.Token, fieldTokenAttribute.Value); + } + } + } + + return tokens; + } + private static List<SelectOption> GetSelectOptions(Type selectOptions) { if (selectOptions.IsEnum) diff --git a/src/Sonarr.Http/Extensions/RequestExtensions.cs b/src/Sonarr.Http/Extensions/RequestExtensions.cs index 8e7ed9590..772264dfb 100644 --- a/src/Sonarr.Http/Extensions/RequestExtensions.cs +++ b/src/Sonarr.Http/Extensions/RequestExtensions.cs @@ -4,7 +4,6 @@ using System.Linq; using Microsoft.AspNetCore.Http; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Exceptions; namespace Sonarr.Http.Extensions { @@ -61,80 +60,6 @@ namespace Sonarr.Http.Extensions return defaultValue; } - public static PagingResource<TResource> ReadPagingResourceFromRequest<TResource>(this HttpRequest request) - { - if (!int.TryParse(request.Query["PageSize"].ToString(), out var pageSize)) - { - pageSize = 10; - } - - if (!int.TryParse(request.Query["Page"].ToString(), out var page)) - { - page = 1; - } - - var pagingResource = new PagingResource<TResource> - { - PageSize = pageSize, - Page = page, - Filters = new List<PagingResourceFilter>() - }; - - if (request.Query["SortKey"].Any()) - { - var sortKey = request.Query["SortKey"].ToString(); - - if (!VALID_SORT_KEYS.Contains(sortKey) && - !TableMapping.Mapper.IsValidSortKey(sortKey)) - { - throw new BadRequestException($"Invalid sort key {sortKey}"); - } - - pagingResource.SortKey = sortKey; - - if (request.Query["SortDirection"].Any()) - { - pagingResource.SortDirection = request.Query["SortDirection"].ToString() - .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) - ? SortDirection.Ascending - : SortDirection.Descending; - } - } - - // For backwards compatibility with v2 - if (request.Query["FilterKey"].Any()) - { - var filter = new PagingResourceFilter - { - Key = request.Query["FilterKey"].ToString() - }; - - if (request.Query["FilterValue"].Any()) - { - filter.Value = request.Query["FilterValue"].ToString(); - } - - pagingResource.Filters.Add(filter); - } - - // v3 uses filters in key=value format - foreach (var pair in request.Query) - { - if (EXCLUDED_KEYS.Contains(pair.Key)) - { - continue; - } - - pagingResource.Filters.Add(new PagingResourceFilter - { - Key = pair.Key, - Value = pair.Value.ToString() - }); - } - - return pagingResource; - } - public static PagingResource<TResource> ApplyToPage<TResource, TModel>(this PagingSpec<TModel> pagingSpec, Func<PagingSpec<TModel>, PagingSpec<TModel>> function, Converter<TModel, TResource> mapper) { pagingSpec = function(pagingSpec); diff --git a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 7b3021f8f..0f8ebe74d 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; namespace Sonarr.Http.Frontend.Mappers { @@ -6,6 +7,6 @@ namespace Sonarr.Http.Frontend.Mappers { string Map(string resourceUrl); bool CanHandle(string resourceUrl); - IActionResult GetResponse(string resourceUrl); + Task<IActionResult> GetResponse(string resourceUrl); } } diff --git a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs index ece8c0609..9c869c899 100644 --- a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Net; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using NzbDrone.Core.MediaCover; @@ -9,7 +10,7 @@ namespace Sonarr.Http.Frontend.Mappers { public class MediaCoverProxyMapper : IMapHttpRequestsToDisk { - private readonly Regex _regex = new Regex(@"/MediaCoverProxy/(?<hash>\w+)/(?<filename>(.+)\.(jpg|png|gif))"); + private readonly Regex _regex = new (@"/MediaCoverProxy/(?<hash>\w+)/(?<filename>(.+)\.(jpg|png|gif))"); private readonly IMediaCoverProxy _mediaCoverProxy; private readonly IContentTypeProvider _mimeTypeProvider; @@ -30,7 +31,7 @@ namespace Sonarr.Http.Frontend.Mappers return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase); } - public IActionResult GetResponse(string resourceUrl) + public async Task<IActionResult> GetResponse(string resourceUrl) { var match = _regex.Match(resourceUrl); @@ -42,7 +43,7 @@ namespace Sonarr.Http.Frontend.Mappers var hash = match.Groups["hash"].Value; var filename = match.Groups["filename"].Value; - var imageData = _mediaCoverProxy.GetImage(hash); + var imageData = await _mediaCoverProxy.GetImage(hash); if (!_mimeTypeProvider.TryGetContentType(filename, out var contentType)) { diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 277060de1..3f3982ac1 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Net.Http.Headers; @@ -30,7 +31,7 @@ namespace Sonarr.Http.Frontend.Mappers public abstract bool CanHandle(string resourceUrl); - public IActionResult GetResponse(string resourceUrl) + public Task<IActionResult> GetResponse(string resourceUrl) { var filePath = Map(resourceUrl); @@ -41,15 +42,15 @@ namespace Sonarr.Http.Frontend.Mappers contentType = "application/octet-stream"; } - return new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType) + return Task.FromResult<IActionResult>(new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType) { Encoding = contentType == "text/plain" ? Encoding.UTF8 : null - }); + })); } _logger.Warn("File {0} not found", filePath); - return null; + return Task.FromResult<IActionResult>(null); } protected virtual Stream GetContentStream(string filePath) diff --git a/src/Sonarr.Http/Frontend/StaticResourceController.cs b/src/Sonarr.Http/Frontend/StaticResourceController.cs index b70b5e597..49bc495b7 100644 --- a/src/Sonarr.Http/Frontend/StaticResourceController.cs +++ b/src/Sonarr.Http/Frontend/StaticResourceController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; @@ -25,27 +26,27 @@ namespace Sonarr.Http.Frontend [AllowAnonymous] [HttpGet("login")] - public IActionResult LoginPage() + public async Task<IActionResult> LoginPage() { - return MapResource("login"); + return await MapResource("login"); } [EnableCors("AllowGet")] [AllowAnonymous] [HttpGet("/content/{**path:regex(^(?!api/).*)}")] - public IActionResult IndexContent([FromRoute] string path) + public async Task<IActionResult> IndexContent([FromRoute] string path) { - return MapResource("Content/" + path); + return await MapResource("Content/" + path); } [HttpGet("")] [HttpGet("/{**path:regex(^(?!(api|feed)/).*)}")] - public IActionResult Index([FromRoute] string path) + public async Task<IActionResult> Index([FromRoute] string path) { - return MapResource(path); + return await MapResource(path); } - private IActionResult MapResource(string path) + private async Task<IActionResult> MapResource(string path) { path = "/" + (path ?? ""); @@ -53,7 +54,7 @@ namespace Sonarr.Http.Frontend if (mapper != null) { - var result = mapper.GetResponse(path); + var result = await mapper.GetResponse(path); if (result != null) { diff --git a/src/Sonarr.Http/PagingResource.cs b/src/Sonarr.Http/PagingResource.cs index d1c0a6eb7..6559d80ab 100644 --- a/src/Sonarr.Http/PagingResource.cs +++ b/src/Sonarr.Http/PagingResource.cs @@ -1,17 +1,39 @@ using System.Collections.Generic; +using System.ComponentModel; using NzbDrone.Core.Datastore; namespace Sonarr.Http { + public class PagingRequestResource + { + [DefaultValue(1)] + public int? Page { get; set; } + [DefaultValue(10)] + public int? PageSize { get; set; } + public string SortKey { get; set; } + public SortDirection? SortDirection { get; set; } + } + public class PagingResource<TResource> { public int Page { get; set; } public int PageSize { get; set; } public string SortKey { get; set; } public SortDirection SortDirection { get; set; } - public List<PagingResourceFilter> Filters { get; set; } public int TotalRecords { get; set; } public List<TResource> Records { get; set; } + + public PagingResource() + { + } + + public PagingResource(PagingRequestResource requestResource) + { + Page = requestResource.Page ?? 1; + PageSize = requestResource.PageSize ?? 10; + SortKey = requestResource.SortKey; + SortDirection = requestResource.SortDirection ?? SortDirection.Descending; + } } public static class PagingResourceMapper diff --git a/src/Sonarr.Http/PagingResourceFilter.cs b/src/Sonarr.Http/PagingResourceFilter.cs deleted file mode 100644 index 303097a9a..000000000 --- a/src/Sonarr.Http/PagingResourceFilter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Sonarr.Http -{ - public class PagingResourceFilter - { - public string Key { get; set; } - public string Value { get; set; } - } -} diff --git a/yarn.lock b/yarn.lock index c209a992e..3f761abd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1443,6 +1443,13 @@ dependencies: "@types/react" "*" +"@types/react-lazyload@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/react-lazyload/-/react-lazyload-3.2.0.tgz#b763f8f0c724df2c969d7e0b3a56c6aa2720fa1f" + integrity sha512-4+r+z8Cf7L/mgxA1vl5uHx5GS/8gY2jqq2p5r5WCm+nUsg9KilwQ+8uaJA3EUlLj57AOzOfGGwwRJ5LOVl8fwA== + dependencies: + "@types/react" "*" + "@types/react-redux@^7.1.16": version "7.1.26" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.26.tgz#84149f5614e40274bb70fcbe8f7cae6267d548b1"