mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
61 Commits
v4.0.2.126
...
v4.0.3.144
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac69445e4 | ||
|
|
aca10f6f4f | ||
|
|
74cdf01e49 | ||
|
|
a169ebff2a | ||
|
|
7fc3bebc91 | ||
|
|
e672996dbb | ||
|
|
238ba85f0a | ||
|
|
1562d3bae3 | ||
|
|
7776ec9955 | ||
|
|
af5a681ab7 | ||
|
|
0a7f3a12c2 | ||
|
|
2ef46e5b90 | ||
|
|
6003ca1696 | ||
|
|
0937ee6fef | ||
|
|
60ee7cc716 | ||
|
|
4e83820511 | ||
|
|
5a66b949cf | ||
|
|
f010f56290 | ||
|
|
060b789bc6 | ||
|
|
7353fe479d | ||
|
|
1ec1ce58e9 | ||
|
|
35d0e6a6f8 | ||
|
|
588372fd95 | ||
|
|
13c925b341 | ||
|
|
1335efd487 | ||
|
|
d338425951 | ||
|
|
fc6494c569 | ||
|
|
c403b2cdd5 | ||
|
|
cf3d51bab2 | ||
|
|
dec3fc6889 | ||
|
|
40bac23698 | ||
|
|
88de927435 | ||
|
|
29204c93a3 | ||
|
|
c641733781 | ||
|
|
58de0310fd | ||
|
|
172b1a82d1 | ||
|
|
e14568adef | ||
|
|
381ce61aef | ||
|
|
9f705e4161 | ||
|
|
063dba22a8 | ||
|
|
6d552f2a60 | ||
|
|
4d4d63921b | ||
|
|
6584d95331 | ||
|
|
86034beccd | ||
|
|
4aa56e3f91 | ||
|
|
2ec071a5ec | ||
|
|
d86aeb7472 | ||
|
|
48cb5d2271 | ||
|
|
a0329adeba | ||
|
|
89bef4af99 | ||
|
|
a12cdb34bc | ||
|
|
13e29bd257 | ||
|
|
61a7515041 | ||
|
|
2c25245860 | ||
|
|
18aadb544e | ||
|
|
c7dd7abf89 | ||
|
|
d0e9504af0 | ||
|
|
e81bb3b993 | ||
|
|
f211433b77 | ||
|
|
2068c5393e | ||
|
|
0183812cc5 |
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ env:
|
||||
FRAMEWORK: net6.0
|
||||
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
SONARR_MAJOR_VERSION: 4
|
||||
VERSION: 4.0.2
|
||||
VERSION: 4.0.3
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
run: yarn lint
|
||||
|
||||
- name: Stylelint
|
||||
run: yarn stylelint
|
||||
run: yarn stylelint -f github
|
||||
|
||||
- name: Build
|
||||
run: yarn build --env production
|
||||
@@ -225,3 +225,25 @@ jobs:
|
||||
branch: ${{ github.ref_name }}
|
||||
major_version: ${{ needs.backend.outputs.major_version }}
|
||||
version: ${{ needs.backend.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: Discord Notification
|
||||
needs: [backend, unit_test, unit_test_postgres, integration_test]
|
||||
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||
env:
|
||||
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Notify
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
username: 'GitHub Actions'
|
||||
avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
|
||||
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||
embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
embed-description: |
|
||||
**Branch** ${{ github.ref }}
|
||||
**Build** ${{ needs.backend.outputs.version }}
|
||||
embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface AppSectionSaveState {
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface CommandBody {
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
seriesIds?: number[];
|
||||
seasonNumber?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import monitorOptions from 'Utilities/Series/monitorOptions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectInput from './SelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function MonitorEpisodesSelectInput(props) {
|
||||
const {
|
||||
@@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) {
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) {
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
<EnhancedSelectInput
|
||||
values={values}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||
import SelectInput from './SelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function MonitorNewItemsSelectInput(props) {
|
||||
const {
|
||||
@@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
<EnhancedSelectInput
|
||||
values={values}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ function createMapStateToProps() {
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ function createMapStateToProps() {
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ interface ISeriesTypeOption {
|
||||
key: string;
|
||||
value: string;
|
||||
format?: string;
|
||||
disabled?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const seriesTypeOptions: ISeriesTypeOption[] = [
|
||||
@@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled,
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: `(${translate('Mixed')})`,
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
|
||||
.extraExtraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1600px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
@@ -90,7 +97,8 @@
|
||||
.modal.small,
|
||||
.modal.medium,
|
||||
.modal.large,
|
||||
.modal.extraLarge {
|
||||
.modal.extraLarge,
|
||||
.modal.extraExtraLarge {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
|
||||
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'extraExtraLarge': string;
|
||||
'extraLarge': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class EpisodeDetailsModal extends Component {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
||||
17
frontend/src/Episode/getReleaseTypeName.ts
Normal file
17
frontend/src/Episode/getReleaseTypeName.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export default function getReleaseTypeName(
|
||||
releaseType?: ReleaseType
|
||||
): string | null {
|
||||
switch (releaseType) {
|
||||
case 'singleEpisode':
|
||||
return translate('SingleEpisode');
|
||||
case 'multiEpisode':
|
||||
return translate('MultiEpisode');
|
||||
case 'seasonPack':
|
||||
return translate('SeasonPack');
|
||||
default:
|
||||
return translate('Unknown');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
@@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase {
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
indexerFlags: number;
|
||||
releaseType: ReleaseType;
|
||||
mediaInfo: MediaInfo;
|
||||
qualityCutoffNotMet: boolean;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ export const SMALL = 'small';
|
||||
export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||
|
||||
@@ -36,6 +36,7 @@ import InteractiveImport, {
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import Language from 'Language/Language';
|
||||
@@ -73,7 +74,8 @@ type SelectType =
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language'
|
||||
| 'indexerFlags';
|
||||
| 'indexerFlags'
|
||||
| 'releaseType';
|
||||
|
||||
type FilterExistingFiles = 'all' | 'new';
|
||||
|
||||
@@ -128,6 +130,12 @@ const COLUMNS = [
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseType',
|
||||
label: () => translate('ReleaseType'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: React.createElement(Icon, {
|
||||
@@ -369,6 +377,10 @@ function InteractiveImportModalContent(
|
||||
key: 'indexerFlags',
|
||||
value: translate('SelectIndexerFlags'),
|
||||
},
|
||||
{
|
||||
key: 'releaseType',
|
||||
value: translate('SelectReleaseType'),
|
||||
},
|
||||
];
|
||||
|
||||
if (allowSeriesChange) {
|
||||
@@ -511,6 +523,7 @@ function InteractiveImportModalContent(
|
||||
languages,
|
||||
indexerFlags,
|
||||
episodeFileId,
|
||||
releaseType,
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
@@ -560,6 +573,7 @@ function InteractiveImportModalContent(
|
||||
quality,
|
||||
languages,
|
||||
indexerFlags,
|
||||
releaseType,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -575,6 +589,7 @@ function InteractiveImportModalContent(
|
||||
quality,
|
||||
languages,
|
||||
indexerFlags,
|
||||
releaseType,
|
||||
downloadId,
|
||||
episodeFileId,
|
||||
});
|
||||
@@ -787,6 +802,22 @@ function InteractiveImportModalContent(
|
||||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const onReleaseTypeSelect = useCallback(
|
||||
(releaseType: string) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
releaseType,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
||||
if (selectedIds.includes(file.id)) {
|
||||
acc.push(file.id);
|
||||
@@ -1000,6 +1031,14 @@ function InteractiveImportModalContent(
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseTypeModal
|
||||
isOpen={selectModalOpen === 'releaseType'}
|
||||
releaseType="unknown"
|
||||
modalTitle={modalTitle}
|
||||
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
|
||||
@@ -12,6 +12,7 @@ import Episode from 'Episode/Episode';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import getReleaseTypeName from 'Episode/getReleaseTypeName';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
@@ -20,6 +21,8 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import Language from 'Language/Language';
|
||||
@@ -44,7 +47,8 @@ type SelectType =
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language'
|
||||
| 'indexerFlags';
|
||||
| 'indexerFlags'
|
||||
| 'releaseType';
|
||||
|
||||
type SelectedChangeProps = SelectStateInputProps & {
|
||||
hasEpisodeFileId: boolean;
|
||||
@@ -61,6 +65,7 @@ interface InteractiveImportRowProps {
|
||||
quality?: QualityModel;
|
||||
languages?: Language[];
|
||||
size: number;
|
||||
releaseType: ReleaseType;
|
||||
customFormats?: object[];
|
||||
customFormatScore?: number;
|
||||
indexerFlags: number;
|
||||
@@ -86,6 +91,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
languages,
|
||||
releaseGroup,
|
||||
size,
|
||||
releaseType,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
@@ -315,6 +321,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||
);
|
||||
|
||||
const onSelectReleaseTypePress = useCallback(() => {
|
||||
setSelectModalOpen('releaseType');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onReleaseTypeSelect = useCallback(
|
||||
(releaseType: ReleaseType) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItem({
|
||||
id,
|
||||
releaseType,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
selectRowAfterChange();
|
||||
},
|
||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||
);
|
||||
|
||||
const onSelectIndexerFlagsPress = useCallback(() => {
|
||||
setSelectModalOpen('indexerFlags');
|
||||
}, [setSelectModalOpen]);
|
||||
@@ -461,6 +488,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
|
||||
<TableRowCell>{formatBytes(size)}</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
title={translate('ClickToChangeReleaseType')}
|
||||
onPress={onSelectReleaseTypePress}
|
||||
>
|
||||
{getReleaseTypeName(releaseType)}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>
|
||||
{customFormats?.length ? (
|
||||
<Popover
|
||||
@@ -572,6 +606,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseTypeModal
|
||||
isOpen={selectModalOpen === 'releaseType'}
|
||||
releaseType={releaseType ?? 'unknown'}
|
||||
modalTitle={modalTitle}
|
||||
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectIndexerFlagsModal
|
||||
isOpen={selectModalOpen === 'indexerFlags'}
|
||||
indexerFlags={indexerFlags ?? 0}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import Episode from 'Episode/Episode';
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
@@ -14,6 +15,7 @@ export interface InteractiveImportCommandOptions {
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
indexerFlags: number;
|
||||
releaseType: ReleaseType;
|
||||
downloadId?: string;
|
||||
episodeFileId?: number;
|
||||
}
|
||||
@@ -33,6 +35,7 @@ interface InteractiveImport extends ModelBase {
|
||||
qualityWeight: number;
|
||||
customFormats: object[];
|
||||
indexerFlags: number;
|
||||
releaseType: ReleaseType;
|
||||
rejections: Rejection[];
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
||||
3
frontend/src/InteractiveImport/ReleaseType.ts
Normal file
3
frontend/src/InteractiveImport/ReleaseType.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type ReleaseType = 'unknown' | 'singleEpisode' | 'multiEpisode' | 'seasonPack';
|
||||
|
||||
export default ReleaseType;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import SelectReleaseTypeModalContent from './SelectReleaseTypeModalContent';
|
||||
|
||||
interface SelectQualityModalProps {
|
||||
isOpen: boolean;
|
||||
releaseType: ReleaseType;
|
||||
modalTitle: string;
|
||||
onReleaseTypeSelect(releaseType: ReleaseType): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectReleaseTypeModal(props: SelectQualityModalProps) {
|
||||
const { isOpen, releaseType, modalTitle, onReleaseTypeSelect, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<SelectReleaseTypeModalContent
|
||||
releaseType={releaseType}
|
||||
modalTitle={modalTitle}
|
||||
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectReleaseTypeModal;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const options = [
|
||||
{
|
||||
key: 'unknown',
|
||||
get value() {
|
||||
return translate('Unknown');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'singleEpisode',
|
||||
get value() {
|
||||
return translate('SingleEpisode');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'multiEpisode',
|
||||
get value() {
|
||||
return translate('MultiEpisode');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'seasonPack',
|
||||
get value() {
|
||||
return translate('SeasonPack');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface SelectReleaseTypeModalContentProps {
|
||||
releaseType: ReleaseType;
|
||||
modalTitle: string;
|
||||
onReleaseTypeSelect(releaseType: ReleaseType): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectReleaseTypeModalContent(
|
||||
props: SelectReleaseTypeModalContentProps
|
||||
) {
|
||||
const { modalTitle, onReleaseTypeSelect, onModalClose } = props;
|
||||
const [releaseType, setReleaseType] = useState(props.releaseType);
|
||||
|
||||
const handleReleaseTypeChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
setReleaseType(value as ReleaseType);
|
||||
},
|
||||
[setReleaseType]
|
||||
);
|
||||
|
||||
const handleReleaseTypeSelect = useCallback(() => {
|
||||
onReleaseTypeSelect(releaseType);
|
||||
}, [releaseType, onReleaseTypeSelect]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{modalTitle} - {translate('SelectReleaseType')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ReleaseType')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="releaseType"
|
||||
value={releaseType}
|
||||
values={options}
|
||||
onChange={handleReleaseTypeChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.SUCCESS} onPress={handleReleaseTypeSelect}>
|
||||
{translate('SelectReleaseType')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectReleaseTypeModalContent;
|
||||
@@ -36,7 +36,7 @@ const monitoredOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
@@ -58,7 +58,7 @@ const seasonFolderOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'yes',
|
||||
|
||||
@@ -15,7 +15,7 @@ function SeasonInteractiveSearchModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
||||
@@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
||||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
specifications.map((tag) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
@@ -41,11 +42,6 @@ const COLUMNS = [
|
||||
},
|
||||
];
|
||||
|
||||
interface ImportListExclusionsProps {
|
||||
useCurrentPage: number;
|
||||
totalRecords: number;
|
||||
}
|
||||
|
||||
function createImportListExlucionsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.importListExclusions,
|
||||
@@ -57,8 +53,9 @@ function createImportListExlucionsSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function ImportListExclusions(props: ImportListExclusionsProps) {
|
||||
const { useCurrentPage, totalRecords } = props;
|
||||
function ImportListExclusions() {
|
||||
const history = useHistory();
|
||||
const useCurrentPage = history.action === 'POP';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -155,6 +152,7 @@ function ImportListExclusions(props: ImportListExclusionsProps) {
|
||||
sortKey,
|
||||
error,
|
||||
sortDirection,
|
||||
totalRecords,
|
||||
...otherProps
|
||||
} = selected;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
@@ -38,6 +39,7 @@ function EditImportListModalContent(props) {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteImportListPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -288,6 +290,12 @@ function EditImportListModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -327,6 +335,7 @@ EditImportListModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteImportListPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditImportListModalContent from './EditImportListModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setImportListValue,
|
||||
setImportListFieldValue,
|
||||
saveImportList,
|
||||
testImportList
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditImportListModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditImportListModalContentConnector extends Component {
|
||||
this.props.testImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +76,7 @@ class EditImportListModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -82,6 +94,7 @@ EditImportListModalContentConnector.propTypes = {
|
||||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
saveImportList: PropTypes.func.isRequired,
|
||||
testImportList: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const autoAddOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -32,7 +32,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -80,19 +80,19 @@ const fileNameTokens = [
|
||||
];
|
||||
|
||||
const seriesTokens = [
|
||||
{ token: '{Series Title}', example: 'The Series Title\'s!' },
|
||||
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
|
||||
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
|
||||
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' },
|
||||
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' },
|
||||
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
|
||||
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
|
||||
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' },
|
||||
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
|
||||
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
|
||||
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
||||
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
||||
{ token: '{Series TitleFirstCharacter}', example: 'S' },
|
||||
{ token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 },
|
||||
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 },
|
||||
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 },
|
||||
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 },
|
||||
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
|
||||
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
|
||||
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 },
|
||||
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 },
|
||||
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||
{ token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 },
|
||||
{ token: '{Series Year}', example: '2010' }
|
||||
];
|
||||
|
||||
@@ -124,8 +124,8 @@ const absoluteTokens = [
|
||||
];
|
||||
|
||||
const episodeTitleTokens = [
|
||||
{ token: '{Episode Title}', example: 'Episode\'s Title' },
|
||||
{ token: '{Episode CleanTitle}', example: 'Episodes Title' }
|
||||
{ token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 },
|
||||
{ token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 }
|
||||
];
|
||||
|
||||
const qualityTokens = [
|
||||
@@ -149,8 +149,13 @@ const mediaInfoTokens = [
|
||||
];
|
||||
|
||||
const otherTokens = [
|
||||
{ token: '{Release Group}', example: 'Rls Grp' },
|
||||
{ token: '{Custom Formats}', example: 'iNTERNAL' }
|
||||
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 },
|
||||
{ token: '{Custom Formats}', example: 'iNTERNAL' },
|
||||
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
||||
];
|
||||
|
||||
const otherAnimeTokens = [
|
||||
{ token: '{Release Hash}', example: 'ABCDEFGH' }
|
||||
];
|
||||
|
||||
const originalTokens = [
|
||||
@@ -300,7 +305,7 @@ class NamingModal extends Component {
|
||||
<FieldSet legend={translate('Series')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
seriesTokens.map(({ token, example }) => {
|
||||
seriesTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
@@ -308,6 +313,7 @@ class NamingModal extends Component {
|
||||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
@@ -317,6 +323,11 @@ class NamingModal extends Component {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('SeriesFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('SeriesID')}>
|
||||
@@ -446,7 +457,7 @@ class NamingModal extends Component {
|
||||
<FieldSet legend={translate('EpisodeTitle')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
episodeTitleTokens.map(({ token, example }) => {
|
||||
episodeTitleTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
@@ -454,6 +465,7 @@ class NamingModal extends Component {
|
||||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
@@ -463,6 +475,10 @@ class NamingModal extends Component {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('EpisodeTitleFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Quality')}>
|
||||
@@ -518,7 +534,26 @@ class NamingModal extends Component {
|
||||
<FieldSet legend={translate('Other')}>
|
||||
<div className={styles.groups}>
|
||||
{
|
||||
otherTokens.map(({ token, example }) => {
|
||||
otherTokens.map(({ token, example, footNote }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
name={name}
|
||||
value={value}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={this.onOptionPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
anime && otherAnimeTokens.map(({ token, example }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
@@ -535,6 +570,11 @@ class NamingModal extends Component {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Original')}>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
.token {
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 6px;
|
||||
padding: 6px;
|
||||
background-color: var(--popoverTitleBackgroundColor);
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 6px;
|
||||
padding: 6px;
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
|
||||
.footNote {
|
||||
|
||||
@@ -4,7 +4,6 @@ import translate from 'Utilities/String/translate';
|
||||
import styles from './TheTvdb.css';
|
||||
|
||||
function TheTvdb(props) {
|
||||
debugger;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<img
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationEventItems from './NotificationEventItems';
|
||||
import styles from './EditNotificationModalContent.css';
|
||||
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
|
||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
saveNotification,
|
||||
testNotification
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
||||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
import createServerSideCollectionHandlers from '../Creators/createServerSideCollectionHandlers';
|
||||
import createSetTableOptionReducer from '../Creators/Reducers/createSetTableOptionReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
@@ -163,6 +163,7 @@ export const actionHandlers = handleThunks({
|
||||
languages: item.languages,
|
||||
releaseGroup: item.releaseGroup,
|
||||
indexerFlags: item.indexerFlags,
|
||||
releaseType: item.releaseType,
|
||||
downloadId: item.downloadId
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import indexerFlags from 'Store/Actions/Settings/indexerFlags';
|
||||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import autoTaggings from './Settings/autoTaggings';
|
||||
@@ -13,6 +12,7 @@ import general from './Settings/general';
|
||||
import importListExclusions from './Settings/importListExclusions';
|
||||
import importListOptions from './Settings/importListOptions';
|
||||
import importLists from './Settings/importLists';
|
||||
import indexerFlags from './Settings/indexerFlags';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
import languages from './Settings/languages';
|
||||
@@ -91,7 +91,8 @@ export const defaultState = {
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'settings.advancedSettings'
|
||||
'settings.advancedSettings',
|
||||
'settings.importListExclusions.pageSize'
|
||||
];
|
||||
|
||||
//
|
||||
|
||||
23
frontend/src/Store/Selectors/createMultiSeriesSelector.ts
Normal file
23
frontend/src/Store/Selectors/createMultiSeriesSelector.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Series from 'Series/Series';
|
||||
|
||||
function createMultiSeriesSelector(seriesIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.series.itemMap,
|
||||
(state: AppState) => state.series.items,
|
||||
(itemMap, allSeries) => {
|
||||
return seriesIds.reduce((acc: Series[], seriesId) => {
|
||||
const series = allSeries[itemMap[seriesId]];
|
||||
|
||||
if (series) {
|
||||
acc.push(series);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMultiSeriesSelector;
|
||||
@@ -10,15 +10,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.queued,
|
||||
.started,
|
||||
.ended {
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'commandName': string;
|
||||
'duration': string;
|
||||
'ended': string;
|
||||
'queued': string;
|
||||
'started': string;
|
||||
'trigger': string;
|
||||
'triggerContent': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status, message) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(props) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
} = props;
|
||||
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
||||
};
|
||||
}
|
||||
|
||||
class QueuedTaskRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
...getFormattedDates(props),
|
||||
isCancelConfirmModalOpen: false
|
||||
};
|
||||
|
||||
this._updateTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setUpdateTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
queued !== prevProps.queued ||
|
||||
started !== prevProps.started ||
|
||||
ended !== prevProps.ended
|
||||
) {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._updateTimeoutId) {
|
||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setUpdateTimer() {
|
||||
this._updateTimeoutId = setTimeout(() => {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
this.setUpdateTimer();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onAbortCancel = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
clientUserAgent,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onCancelPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
queuedAt,
|
||||
startedAt,
|
||||
endedAt,
|
||||
isCancelConfirmModalOpen
|
||||
} = this.state;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon
|
||||
name={triggerIcon}
|
||||
title={titleCase(trigger)}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
{...getStatusIconProps(status, message)}
|
||||
/>
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
||||
{translate('From')}: {clientUserAgent}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
status === 'queued' &&
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onCancelPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={onCancelPress}
|
||||
onCancel={this.onAbortCancel}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTaskRow.propTypes = {
|
||||
trigger: PropTypes.string.isRequired,
|
||||
commandName: PropTypes.string.isRequired,
|
||||
queued: PropTypes.string.isRequired,
|
||||
started: PropTypes.string,
|
||||
ended: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
duration: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
clientUserAgent: PropTypes.string,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTaskRow;
|
||||
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status: string, message: string | undefined) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(
|
||||
queued: string,
|
||||
started: string | undefined,
|
||||
ended: string | undefined,
|
||||
showRelativeDates: boolean,
|
||||
shortDateFormat: string
|
||||
) {
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||
};
|
||||
}
|
||||
|
||||
interface QueuedTimes {
|
||||
queuedAt: string;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowProps {
|
||||
id: number;
|
||||
trigger: string;
|
||||
commandName: string;
|
||||
queued: string;
|
||||
started?: string;
|
||||
ended?: string;
|
||||
status: string;
|
||||
duration?: string;
|
||||
message?: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||
const {
|
||||
id,
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
body,
|
||||
clientUserAgent,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const [times, setTimes] = useState<QueuedTimes>(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
|
||||
const [
|
||||
isCancelConfirmModalOpen,
|
||||
openCancelConfirmModal,
|
||||
closeCancelConfirmModal,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
dispatch(cancelCommand({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimeTimeoutId.current = setTimeout(() => {
|
||||
setTimes(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (updateTimeTimeoutId.current) {
|
||||
clearTimeout(updateTimeTimeoutId.current);
|
||||
}
|
||||
};
|
||||
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||
|
||||
const { queuedAt, startedAt, endedAt } = times;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||
|
||||
<Icon {...getStatusIconProps(status, message)} />
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<QueuedTaskRowNameCell
|
||||
commandName={commandName}
|
||||
body={body}
|
||||
clientUserAgent={clientUserAgent}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{status === 'queued' && (
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={openCancelConfirmModal}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={handleCancelPress}
|
||||
onCancel={closeCancelConfirmModal}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onCancelPress() {
|
||||
dispatch(cancelCommand({
|
||||
id: props.id
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
||||
@@ -0,0 +1,8 @@
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'commandName': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
57
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
57
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRowNameCell.css';
|
||||
|
||||
export interface QueuedTaskRowNameCellProps {
|
||||
commandName: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRowNameCell(
|
||||
props: QueuedTaskRowNameCellProps
|
||||
) {
|
||||
const { commandName, body, clientUserAgent } = props;
|
||||
const seriesIds = [...(body.seriesIds ?? [])];
|
||||
|
||||
if (body.seriesId) {
|
||||
seriesIds.push(body.seriesId);
|
||||
}
|
||||
|
||||
const series = useSelector(createMultiSeriesSelector(seriesIds));
|
||||
const sortedSeries = series.sort((a, b) =>
|
||||
a.sortTitle.localeCompare(b.sortTitle)
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
{sortedSeries.length ? (
|
||||
<span> - {sortedSeries.map((s) => s.title).join(', ')}</span>
|
||||
) : null}
|
||||
{body.seasonNumber ? (
|
||||
<span>
|
||||
{' '}
|
||||
{translate('SeasonNumberToken', {
|
||||
seasonNumber: body.seasonNumber,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
{clientUserAgent ? (
|
||||
<span
|
||||
className={styles.userAgent}
|
||||
title={translate('TaskUserAgentTooltip')}
|
||||
>
|
||||
{translate('From')}: {clientUserAgent}
|
||||
</span>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function QueuedTasks(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueuedTaskRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
QueuedTasks.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTasks;
|
||||
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function QueuedTasks() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.commands
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCommands());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return <QueuedTaskRow key={item.id} {...item} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import QueuedTasks from './QueuedTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(commands) => {
|
||||
return commands;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCommands: fetchCommands
|
||||
};
|
||||
|
||||
class QueuedTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueuedTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTasksConnector.propTypes = {
|
||||
dispatchFetchCommands: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||
import QueuedTasks from './Queued/QueuedTasks';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
|
||||
function Tasks() {
|
||||
@@ -10,7 +10,7 @@ function Tasks() {
|
||||
<PageContent title={translate('Tasks')}>
|
||||
<PageContentBody>
|
||||
<ScheduledTasksConnector />
|
||||
<QueuedTasksConnector />
|
||||
<QueuedTasks />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Sonarr" />
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Chrome, Opera, and Firefox OS -->
|
||||
<meta name="theme-color" content="#3a3f51" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
<!-- Android/Apple Phone -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<meta name="description" content="Sonarr" />
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
||||
"stylelint": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
|
||||
},
|
||||
"repository": "https://github.com/Sonarr/Sonarr",
|
||||
"author": "Team Sonarr",
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
|
||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
@@ -30,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||
private readonly ICached<CredentialCache> _credentialCache;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||
ICreateManagedWebProxy createManagedWebProxy,
|
||||
ICertificateValidationService certificateValidationService,
|
||||
IUserAgentBuilder userAgentBuilder,
|
||||
ICacheManager cacheManager)
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_createManagedWebProxy = createManagedWebProxy;
|
||||
@@ -43,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||
@@ -249,19 +255,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||
}
|
||||
|
||||
private static bool HasRoutableIPv4Address()
|
||||
private bool HasRoutableIPv4Address()
|
||||
{
|
||||
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
try
|
||||
{
|
||||
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
return networkInterfaces.Any(ni =>
|
||||
ni.OperationalStatus == OperationalStatus.Up &&
|
||||
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||
!IPAddress.IsLoopback(ip.Address)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||
@@ -285,7 +299,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
catch
|
||||
{
|
||||
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||
useIPv6 = !HasRoutableIPv4Address();
|
||||
var routableIPv4 = HasRoutableIPv4Address();
|
||||
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||
useIPv6 = !routableIPv4;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.8" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
|
||||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
|
||||
}
|
||||
|
||||
@@ -120,6 +120,17 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
private void WithImdbId()
|
||||
{
|
||||
_list1Series.First().ImdbId = "tt0496424";
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Setup(s => s.SearchForNewSeriesByImdbId(_list1Series.First().ImdbId))
|
||||
.Returns(
|
||||
Builder<Series>
|
||||
.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(s => s.Title = "Breaking Bad")
|
||||
.With(s => s.TvdbId = 81189)
|
||||
.Build()
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private void WithExistingSeries()
|
||||
@@ -342,6 +353,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
public void should_add_new_series_from_single_list_to_library()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithTvdbId();
|
||||
WithList(1, true);
|
||||
WithCleanLevel(ListSyncLevelType.Disabled);
|
||||
|
||||
@@ -358,6 +370,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithTvdbId();
|
||||
WithList(1, true);
|
||||
WithList(2, true);
|
||||
|
||||
@@ -376,6 +389,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
_importListFetch.Series.AddRange(_list2Series);
|
||||
|
||||
WithTvdbId();
|
||||
WithList(1, true);
|
||||
WithList(2, false);
|
||||
|
||||
@@ -422,12 +436,17 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
public void should_search_by_imdb_if_series_title_and_series_imdb()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
|
||||
WithList(1, true);
|
||||
WithImdbId();
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<ISearchForNewSeries>()
|
||||
.Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny<string>()), Times.Once());
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1), It.IsAny<bool>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -498,5 +517,18 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
Mocker.GetMock<IImportListExclusionService>()
|
||||
.Verify(v => v.All(), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_add_if_tvdbid_is_0()
|
||||
{
|
||||
_importListFetch.Series.ForEach(m => m.ImportListId = 1);
|
||||
WithList(1, true);
|
||||
WithExcludedSeries();
|
||||
|
||||
Subject.Execute(_commandAll);
|
||||
|
||||
Mocker.GetMock<IAddSeriesService>()
|
||||
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0), It.IsAny<bool>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
{
|
||||
[TestFixture]
|
||||
public class AggregateReleaseHashFixture : CoreTest<AggregateReleaseHash>
|
||||
{
|
||||
private Series _series;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>.CreateNew().Build();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_prefer_file()
|
||||
{
|
||||
var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCDEFGH]");
|
||||
var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]");
|
||||
var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]");
|
||||
var localEpisode = new LocalEpisode
|
||||
{
|
||||
FileEpisodeInfo = fileEpisodeInfo,
|
||||
FolderEpisodeInfo = folderEpisodeInfo,
|
||||
DownloadClientEpisodeInfo = downloadClientEpisodeInfo,
|
||||
Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(),
|
||||
Series = _series
|
||||
};
|
||||
|
||||
Subject.Aggregate(localEpisode, null);
|
||||
|
||||
localEpisode.ReleaseHash.Should().Be("ABCDEFGH");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_fallback_to_downloadclient()
|
||||
{
|
||||
var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)");
|
||||
var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]");
|
||||
var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]");
|
||||
var localEpisode = new LocalEpisode
|
||||
{
|
||||
FileEpisodeInfo = fileEpisodeInfo,
|
||||
FolderEpisodeInfo = folderEpisodeInfo,
|
||||
DownloadClientEpisodeInfo = downloadClientEpisodeInfo,
|
||||
Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(),
|
||||
Series = _series
|
||||
};
|
||||
|
||||
Subject.Aggregate(localEpisode, null);
|
||||
|
||||
localEpisode.ReleaseHash.Should().Be("ABCD1234");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_fallback_to_folder()
|
||||
{
|
||||
var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)");
|
||||
var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)");
|
||||
var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]");
|
||||
var localEpisode = new LocalEpisode
|
||||
{
|
||||
FileEpisodeInfo = fileEpisodeInfo,
|
||||
FolderEpisodeInfo = folderEpisodeInfo,
|
||||
DownloadClientEpisodeInfo = downloadClientEpisodeInfo,
|
||||
Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(),
|
||||
Series = _series
|
||||
};
|
||||
|
||||
Subject.Aggregate(localEpisode, null);
|
||||
|
||||
localEpisode.ReleaseHash.Should().Be("12345678");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,14 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
[TestFixture]
|
||||
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
|
||||
{
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
|
||||
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
|
||||
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 5.1].mkv", "", "Name (2020) - S01E20 - [FLAC 2.0].fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [FLAC 2.0].mkv")]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
|
||||
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
|
||||
public void should_do_basic_parse(string relativePath, string originalFilePath, string path, string fileNameBeforeRename)
|
||||
{
|
||||
var episodeFile = new EpisodeFile
|
||||
{
|
||||
@@ -23,7 +24,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
OriginalFilePath = originalFilePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, fileNameBeforeRename);
|
||||
|
||||
subtitleTitleInfo.Title.Should().BeNull();
|
||||
subtitleTitleInfo.Copy.Should().Be(0);
|
||||
@@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
||||
RelativePath = relativePath
|
||||
};
|
||||
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
|
||||
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, null);
|
||||
|
||||
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class CustomFormatsFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Series _series;
|
||||
private Episode _episode1;
|
||||
private EpisodeFile _episodeFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
private List<CustomFormat> _customFormats;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "South Park")
|
||||
.Build();
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.RenameEpisodes = true;
|
||||
|
||||
Mocker.GetMock<INamingConfigService>()
|
||||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
_episode1 = Builder<Episode>.CreateNew()
|
||||
.With(e => e.Title = "City Sushi")
|
||||
.With(e => e.SeasonNumber = 15)
|
||||
.With(e => e.EpisodeNumber = 6)
|
||||
.With(e => e.AbsoluteEpisodeNumber = 100)
|
||||
.Build();
|
||||
|
||||
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
|
||||
|
||||
_customFormats = new List<CustomFormat>()
|
||||
{
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "INTERNAL",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "AMZN",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "NAME WITH SPACES",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat()
|
||||
{
|
||||
Name = "NotIncludedFormat",
|
||||
IncludeCustomFormatWhenRenaming = false
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats}", "INTERNAL AMZN NAME WITH SPACES")]
|
||||
public void should_replace_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats}", "")]
|
||||
public void should_replace_custom_formats_with_no_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>())
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats:-INTERNAL}", "AMZN NAME WITH SPACES")]
|
||||
[TestCase("{Custom Formats:-NAME WITH SPACES}", "INTERNAL AMZN")]
|
||||
[TestCase("{Custom Formats:-INTERNAL,NAME WITH SPACES}", "AMZN")]
|
||||
[TestCase("{Custom Formats:INTERNAL}", "INTERNAL")]
|
||||
[TestCase("{Custom Formats:NAME WITH SPACES}", "NAME WITH SPACES")]
|
||||
[TestCase("{Custom Formats:INTERNAL,NAME WITH SPACES}", "INTERNAL NAME WITH SPACES")]
|
||||
public void should_replace_custom_formats_with_filtered_names(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Formats:-}", "{Custom Formats:-}")]
|
||||
[TestCase("{Custom Formats:}", "{Custom Formats:}")]
|
||||
public void should_not_replace_custom_formats_due_to_invalid_token(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Format}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL}", "INTERNAL")]
|
||||
[TestCase("{Custom Format:AMZN}", "AMZN")]
|
||||
[TestCase("{Custom Format:NAME WITH SPACES}", "NAME WITH SPACES")]
|
||||
[TestCase("{Custom Format:DOESNOTEXIST}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL} - {Custom Format:AMZN}", "INTERNAL - AMZN")]
|
||||
[TestCase("{Custom Format:AMZN} - {Custom Format:INTERNAL}", "AMZN - INTERNAL")]
|
||||
public void should_replace_custom_format(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: _customFormats)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{Custom Format}", "")]
|
||||
[TestCase("{Custom Format:INTERNAL}", "")]
|
||||
[TestCase("{Custom Format:AMZN}", "")]
|
||||
public void should_replace_custom_format_with_no_custom_formats(string format, string expected)
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = format;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile, customFormats: new List<CustomFormat>())
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -991,6 +991,28 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
result.Should().EndWith("HDR");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_replace_release_hash_with_stored_hash()
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = "{Release Hash}";
|
||||
|
||||
_episodeFile.ReleaseHash = "ABCDEFGH";
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||
.Should().Be("ABCDEFGH");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_replace_null_release_hash_with_empty_string()
|
||||
{
|
||||
_namingConfig.StandardEpisodeFormat = "{Release Hash}";
|
||||
|
||||
_episodeFile.ReleaseHash = null;
|
||||
|
||||
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||
.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
private void GivenMediaInfoModel(string videoCodec = "h264",
|
||||
string audioCodec = "dts",
|
||||
int audioChannels = 6,
|
||||
|
||||
@@ -132,6 +132,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)]
|
||||
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
|
||||
[TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)]
|
||||
[TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)]
|
||||
|
||||
// [TestCase("", "", 0, 0, 0)]
|
||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||
|
||||
@@ -22,12 +22,42 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")]
|
||||
[TestCase("[ACX]Series Title 01 Episode Name [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")]
|
||||
[TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")]
|
||||
public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash)
|
||||
|
||||
// These tests are dupes of the above, except with parenthesized hashes instead of square bracket
|
||||
[TestCase("[SubDESU]_Show_Title_DxD_07_(1280x720_x264-AAC)_(6B7FD717)", "SubDESU", "6B7FD717")]
|
||||
[TestCase("[Chihiro]_Show_Title!!_-_06_[848x480_H.264_AAC](859EEAFA)", "Chihiro", "859EEAFA")]
|
||||
[TestCase("[Underwater]_Show_Title_-_12_(720p)_(5C7BC4F9)", "Underwater", "5C7BC4F9")]
|
||||
[TestCase("[HorribleSubs]_Show_Title_-_33_[720p]", "HorribleSubs", "")]
|
||||
[TestCase("[HorribleSubs] Show-Title - 13 [1080p].mkv", "HorribleSubs", "")]
|
||||
[TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].(C65D4B1F).mkv", "Doremi", "C65D4B1F")]
|
||||
[TestCase("[Doremi].Show.Title.5.Go.Go!.31[1280x720].(C65D4B1F)", "Doremi", "C65D4B1F")]
|
||||
[TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")]
|
||||
[TestCase("[K-F] Series Title 214", "K-F", "")]
|
||||
[TestCase("[K-F] Series Title S10E14 214", "K-F", "")]
|
||||
[TestCase("[K-F] Series Title 10x14 214", "K-F", "")]
|
||||
[TestCase("[K-F] Series Title 214 10x14", "K-F", "")]
|
||||
[TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")]
|
||||
[TestCase("[ACX]Series Title 01 Episode Name [Kosaka] (9C57891E).mkv", "ACX", "9C57891E")]
|
||||
[TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) (59B3F2EA).mkv", "S-T-D", "59B3F2EA")]
|
||||
public void should_parse_releasegroup_and_hash(string postTitle, string subGroup, string hash)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseGroup.Should().Be(subGroup);
|
||||
result.ReleaseHash.Should().Be(hash);
|
||||
}
|
||||
|
||||
[TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [8B00F2EA].mkv", "8B00F2EA")]
|
||||
[TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [10BBF2EA].mkv", "10BBF2EA")]
|
||||
[TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [008BF28B].mkv", "008BF28B")]
|
||||
[TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [000BF10B].mkv", "000BF10B")]
|
||||
[TestCase("[DHD] Series Title! - 08 (1280x720 8bit AAC) [8B8BF2EA].mkv", "8B8BF2EA")]
|
||||
[TestCase("[DHD] Series Title! - 10 (1280x720 8bit AAC) [10B10BEA].mkv", "10B10BEA")]
|
||||
public void should_parse_release_hashes_with_10b_or_8b(string postTitle, string hash)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseHash.Should().Be(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +444,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle 1.en.ass", new string[0], "mytitle 1", "English")]
|
||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")]
|
||||
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series Title (S15E06-08) City Sushi", "Series Title", 15, new[] { 6, 7, 8 })]
|
||||
[TestCase("Босх: Спадок (S2E1-4) / Series: Legacy (S2E1-4) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
|
||||
[TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })]
|
||||
[TestCase("Series Title - S26E96-97-98-99-100 - Episode 5931 + Episode 5932 + Episode 5933 + Episode 5934 + Episode 5935", "Series Title", 26, new[] { 96, 97, 98, 99, 100 })]
|
||||
|
||||
// [TestCase("", "", , new [] { })]
|
||||
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
||||
|
||||
@@ -29,6 +29,10 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase(@"C:\Test\Series\Season 01\1 Pilot (1080p HD).mkv", 1, 1)]
|
||||
[TestCase(@"C:\Test\Series\Season 1\02 Honor Thy Father (1080p HD).m4v", 1, 2)]
|
||||
[TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)]
|
||||
[TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
|
||||
[TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
|
||||
[TestCase(@"C:\Test\Series\Season 1\02.04.24 - S01E01 - The Rabbit Hole", 1, 1)]
|
||||
[TestCase(@"C:\Test\Series\Season 1\8 Series Rules - S01E01 - Pilot", 1, 1)]
|
||||
|
||||
// [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0
|
||||
public void should_parse_from_path(string path, int season, int episode)
|
||||
@@ -45,6 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
}
|
||||
|
||||
[TestCase("01-03\\The Series Title (2010) - 1x01-02-03 - Episode Title HDTV-720p Proper", "The Series Title (2010)", 1, new[] { 1, 2, 3 })]
|
||||
[TestCase("Season 2\\E05-06 - Episode Title HDTV-720p Proper", "", 2, new[] { 5, 6 })]
|
||||
public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes)
|
||||
{
|
||||
var result = Parser.Parser.ParsePath(path.AsOsAgnostic());
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[Erai-raws] Series - 0955 ~ 1005 [1080p]", "Erai-raws")]
|
||||
[TestCase("[Exiled-Destiny] Series Title", "Exiled-Destiny")]
|
||||
[TestCase("Series.Title.S01E09.1080p.DSNP.WEB-DL.DDP2.0.H.264-VARYG", "VARYG")]
|
||||
[TestCase("Stargate SG-1 (1997) - S01E01-02 - Children of the Gods (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")]
|
||||
|
||||
// [TestCase("", "")]
|
||||
public void should_parse_release_group(string title, string expected)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(204)]
|
||||
public class add_add_release_hash : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("EpisodeFiles").AddColumn("ReleaseHash").AsString().Nullable();
|
||||
|
||||
Execute.WithConnection(UpdateEpisodeFiles);
|
||||
}
|
||||
|
||||
private void UpdateEpisodeFiles(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updates = new List<object>();
|
||||
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT \"Id\", \"SceneName\", \"RelativePath\", \"OriginalFilePath\" FROM \"EpisodeFiles\"";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var sceneName = reader[1] as string;
|
||||
var relativePath = reader[2] as string;
|
||||
var originalFilePath = reader[3] as string;
|
||||
|
||||
ParsedEpisodeInfo parsedEpisodeInfo = null;
|
||||
|
||||
var originalTitle = sceneName;
|
||||
|
||||
if (originalTitle.IsNullOrWhiteSpace() && originalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
originalTitle = Path.GetFileNameWithoutExtension(originalFilePath);
|
||||
}
|
||||
|
||||
if (originalTitle.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parsedEpisodeInfo = Parser.Parser.ParseTitle(originalTitle);
|
||||
}
|
||||
|
||||
if (parsedEpisodeInfo == null || parsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
|
||||
{
|
||||
parsedEpisodeInfo = Parser.Parser.ParseTitle(Path.GetFileNameWithoutExtension(relativePath));
|
||||
}
|
||||
|
||||
if (parsedEpisodeInfo != null && parsedEpisodeInfo.ReleaseHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
updates.Add(new
|
||||
{
|
||||
Id = id,
|
||||
ReleaseHash = parsedEpisodeInfo.ReleaseHash
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.Count > 0)
|
||||
{
|
||||
var updateEpisodeFilesSql = "UPDATE \"EpisodeFiles\" SET \"ReleaseHash\" = @ReleaseHash WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateEpisodeFilesSql, updates, transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,16 +388,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
}
|
||||
}
|
||||
|
||||
var minimumRetention = 60 * 24 * 14;
|
||||
|
||||
return new DownloadClientInfo
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
||||
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) },
|
||||
RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)
|
||||
RemovesCompletedDownloads = RemovesCompletedDownloads(config)
|
||||
};
|
||||
}
|
||||
|
||||
private bool RemovesCompletedDownloads(QBittorrentPreferences config)
|
||||
{
|
||||
var minimumRetention = 60 * 24 * 14; // 14 days in minutes
|
||||
return (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles);
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
@@ -448,7 +452,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
|
||||
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles))
|
||||
if (RemovesCompletedDownloads(config))
|
||||
{
|
||||
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimit"))
|
||||
{
|
||||
|
||||
@@ -139,12 +139,14 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
// Ignore torrents with an empty path
|
||||
if (torrent.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Warn("Torrent '{0}' has an empty download path and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (torrent.Path.StartsWith("."))
|
||||
{
|
||||
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
|
||||
_logger.Warn("Torrent '{0}' has a download path starting with '.' and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Extras
|
||||
{
|
||||
public interface IExistingExtraFiles
|
||||
{
|
||||
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles);
|
||||
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename);
|
||||
}
|
||||
|
||||
public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent>
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles)
|
||||
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename)
|
||||
{
|
||||
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Extras
|
||||
|
||||
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
|
||||
{
|
||||
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles);
|
||||
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles, fileNameBeforeRename);
|
||||
|
||||
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Extras
|
||||
{
|
||||
var series = message.Series;
|
||||
var possibleExtraFiles = message.PossibleExtraFiles;
|
||||
var importedFiles = ImportExtraFiles(series, possibleExtraFiles);
|
||||
var importedFiles = ImportExtraFiles(series, possibleExtraFiles, null);
|
||||
|
||||
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras
|
||||
public interface IImportExistingExtraFiles
|
||||
{
|
||||
int Order { get; }
|
||||
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
|
||||
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,21 @@ namespace NzbDrone.Core.Extras
|
||||
}
|
||||
|
||||
public abstract int Order { get; }
|
||||
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
|
||||
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
|
||||
|
||||
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries)
|
||||
{
|
||||
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id);
|
||||
|
||||
if (keepExistingEntries)
|
||||
{
|
||||
var incompleteImports = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance).Select(f => f.Id);
|
||||
|
||||
_extraFileService.DeleteMany(incompleteImports);
|
||||
|
||||
return Filter(series, filesOnDisk, importedFiles, new List<TExtraFile>());
|
||||
}
|
||||
|
||||
Clean(series, filesOnDisk, importedFiles, seriesFiles);
|
||||
|
||||
return Filter(series, filesOnDisk, importedFiles, seriesFiles);
|
||||
|
||||
@@ -33,12 +33,12 @@ namespace NzbDrone.Core.Extras.Metadata
|
||||
|
||||
public override int Order => 0;
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
|
||||
{
|
||||
_logger.Debug("Looking for existing metadata in {0}", series.Path);
|
||||
|
||||
var metadataFiles = new List<MetadataFile>();
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
|
||||
|
||||
foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
|
||||
{
|
||||
|
||||
@@ -28,12 +28,12 @@ namespace NzbDrone.Core.Extras.Others
|
||||
|
||||
public override int Order => 2;
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
|
||||
{
|
||||
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
||||
|
||||
var extraFiles = new List<OtherExtraFile>();
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
|
||||
|
||||
foreach (var possibleExtraFile in filterResult.FilesOnDisk)
|
||||
{
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
|
||||
public override int Order => 1;
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
|
||||
{
|
||||
_logger.Debug("Looking for existing subtitle files in {0}", series.Path);
|
||||
|
||||
var subtitleFiles = new List<SubtitleFile>();
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
|
||||
|
||||
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
|
||||
{
|
||||
@@ -46,7 +46,8 @@ namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile),
|
||||
Series = series,
|
||||
Path = possibleSubtitleFile
|
||||
Path = possibleSubtitleFile,
|
||||
FileNameBeforeRename = fileNameBeforeRename
|
||||
};
|
||||
|
||||
try
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
_localizationService.GetLocalizedString("DownloadClientRootFolderHealthCheckMessage", new Dictionary<string, object>
|
||||
{
|
||||
{ "downloadClientName", client.Definition.Name },
|
||||
{ "path", folder.FullPath }
|
||||
{ "rootFolderPath", folder.FullPath }
|
||||
}),
|
||||
"#downloads-in-root-folder");
|
||||
}
|
||||
|
||||
@@ -190,6 +190,29 @@ namespace NzbDrone.Core.ImportLists
|
||||
item.Title = mappedSeries.Title;
|
||||
}
|
||||
|
||||
// Map by MyAniList ID if we have it
|
||||
if (item.TvdbId <= 0 && item.MalId > 0)
|
||||
{
|
||||
var mappedSeries = _seriesSearchService.SearchForNewSeriesByMyAnimeListId(item.MalId)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (mappedSeries == null)
|
||||
{
|
||||
_logger.Debug("Rejected, unable to find matching TVDB ID for MAL ID: {0} [{1}]", item.MalId, item.Title);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
item.TvdbId = mappedSeries.TvdbId;
|
||||
item.Title = mappedSeries.Title;
|
||||
}
|
||||
|
||||
if (item.TvdbId == 0)
|
||||
{
|
||||
_logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if series excluded
|
||||
var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault();
|
||||
|
||||
@@ -202,7 +225,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
// Break if Series Exists in DB
|
||||
if (existingTvdbIds.Any(x => x == item.TvdbId))
|
||||
{
|
||||
_logger.Debug("{0} [{1}] Rejected, Series Exists in DB", item.TvdbId, item.Title);
|
||||
_logger.Debug("{0} [{1}] Rejected, series exists in database", item.TvdbId, item.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
121
src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs
Normal file
121
src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cloud;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public class MyAnimeListImport : HttpImportListBase<MyAnimeListSettings>
|
||||
{
|
||||
public const string OAuthPath = "oauth/myanimelist/authorize";
|
||||
public const string RedirectUriPath = "oauth/myanimelist/auth";
|
||||
public const string RenewUriPath = "oauth/myanimelist/renew";
|
||||
|
||||
public override string Name => "MyAnimeList";
|
||||
public override ImportListType ListType => ImportListType.Other;
|
||||
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6);
|
||||
|
||||
private readonly IImportListRepository _importListRepository;
|
||||
private readonly IHttpRequestBuilderFactory _requestBuilder;
|
||||
|
||||
// This constructor the first thing that is called when sonarr creates a button
|
||||
public MyAnimeListImport(IImportListRepository netImportRepository, IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, localizationService, logger)
|
||||
{
|
||||
_importListRepository = netImportRepository;
|
||||
_requestBuilder = requestBuilder.Services;
|
||||
}
|
||||
|
||||
public override ImportListFetchResult Fetch()
|
||||
{
|
||||
if (Settings.Expires < DateTime.UtcNow.AddMinutes(5))
|
||||
{
|
||||
RefreshToken();
|
||||
}
|
||||
|
||||
return FetchItems(g => g.GetListItems());
|
||||
}
|
||||
|
||||
// MAL OAuth info: https://myanimelist.net/blog.php?eid=835707
|
||||
// The whole process is handled through Sonarr's services.
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "startOAuth")
|
||||
{
|
||||
var request = _requestBuilder.Create()
|
||||
.Resource(OAuthPath)
|
||||
.AddQueryParam("state", query["callbackUrl"])
|
||||
.AddQueryParam("redirect_uri", _requestBuilder.Create().Resource(RedirectUriPath).Build().Url.ToString())
|
||||
.Build();
|
||||
|
||||
return new
|
||||
{
|
||||
OauthUrl = request.Url.ToString()
|
||||
};
|
||||
}
|
||||
else if (action == "getOAuthToken")
|
||||
{
|
||||
return new
|
||||
{
|
||||
accessToken = query["access_token"],
|
||||
expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])),
|
||||
refreshToken = query["refresh_token"]
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new MyAnimeListParser();
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new MyAnimeListRequestGenerator()
|
||||
{
|
||||
Settings = Settings,
|
||||
};
|
||||
}
|
||||
|
||||
private void RefreshToken()
|
||||
{
|
||||
_logger.Trace("Refreshing Token");
|
||||
|
||||
Settings.Validate().Filter("RefreshToken").ThrowOnError();
|
||||
|
||||
var httpReq = _requestBuilder.Create()
|
||||
.Resource(RenewUriPath)
|
||||
.AddQueryParam("refresh_token", Settings.RefreshToken)
|
||||
.Build();
|
||||
try
|
||||
{
|
||||
var httpResp = _httpClient.Get<MyAnimeListAuthToken>(httpReq);
|
||||
|
||||
if (httpResp?.Resource != null)
|
||||
{
|
||||
var token = httpResp.Resource;
|
||||
Settings.AccessToken = token.AccessToken;
|
||||
Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn);
|
||||
Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken;
|
||||
|
||||
if (Definition.Id > 0)
|
||||
{
|
||||
_importListRepository.UpdateSettings((ImportListDefinition)Definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
_logger.Error("Error trying to refresh MAL access token.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public class MyAnimeListParser : IParseImportListResponse
|
||||
{
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MyAnimeListParser));
|
||||
|
||||
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
|
||||
{
|
||||
var jsonResponse = Json.Deserialize<MyAnimeListResponse>(importListResponse.Content);
|
||||
var series = new List<ImportListItemInfo>();
|
||||
|
||||
foreach (var show in jsonResponse.Animes)
|
||||
{
|
||||
series.AddIfNotNull(new ImportListItemInfo
|
||||
{
|
||||
Title = show.AnimeListInfo.Title,
|
||||
MalId = show.AnimeListInfo.Id
|
||||
});
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public class MyAnimeListRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
public MyAnimeListSettings Settings { get; set; }
|
||||
|
||||
private static readonly Dictionary<MyAnimeListStatus, string> StatusMapping = new Dictionary<MyAnimeListStatus, string>
|
||||
{
|
||||
{ MyAnimeListStatus.Watching, "watching" },
|
||||
{ MyAnimeListStatus.Completed, "completed" },
|
||||
{ MyAnimeListStatus.OnHold, "on_hold" },
|
||||
{ MyAnimeListStatus.Dropped, "dropped" },
|
||||
{ MyAnimeListStatus.PlanToWatch, "plan_to_watch" },
|
||||
};
|
||||
|
||||
public virtual ImportListPageableRequestChain GetListItems()
|
||||
{
|
||||
var pageableReq = new ImportListPageableRequestChain();
|
||||
|
||||
pageableReq.Add(GetSeriesRequest());
|
||||
|
||||
return pageableReq;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetSeriesRequest()
|
||||
{
|
||||
var status = (MyAnimeListStatus)Settings.ListStatus;
|
||||
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim());
|
||||
|
||||
requestBuilder.Resource("users/@me/animelist");
|
||||
requestBuilder.AddQueryParam("fields", "list_status");
|
||||
requestBuilder.AddQueryParam("limit", "1000");
|
||||
requestBuilder.Accept(HttpAccept.Json);
|
||||
|
||||
if (status != MyAnimeListStatus.All && StatusMapping.TryGetValue(status, out var statusName))
|
||||
{
|
||||
requestBuilder.AddQueryParam("status", statusName);
|
||||
}
|
||||
|
||||
var httpReq = new ImportListRequest(requestBuilder.Build());
|
||||
|
||||
httpReq.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.AccessToken}");
|
||||
|
||||
yield return httpReq;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public class MyAnimeListResponse
|
||||
{
|
||||
[JsonProperty("data")]
|
||||
public List<MyAnimeListItem> Animes { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnimeListItem
|
||||
{
|
||||
[JsonProperty("node")]
|
||||
public MyAnimeListItemInfo AnimeListInfo { get; set; }
|
||||
|
||||
[JsonProperty("list_status")]
|
||||
public MyAnimeListStatusResult ListStatus { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnimeListStatusResult
|
||||
{
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnimeListItemInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnimeListIds
|
||||
{
|
||||
[JsonProperty("mal_id")]
|
||||
public int MalId { get; set; }
|
||||
|
||||
[JsonProperty("thetvdb_id")]
|
||||
public int TvdbId { get; set; }
|
||||
}
|
||||
|
||||
public class MyAnimeListAuthToken
|
||||
{
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public class MalSettingsValidator : AbstractValidator<MyAnimeListSettings>
|
||||
{
|
||||
public MalSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.AccessToken).NotEmpty()
|
||||
.OverridePropertyName("SignIn")
|
||||
.WithMessage("Must authenticate with MyAnimeList");
|
||||
|
||||
RuleFor(c => c.ListStatus).Custom((status, context) =>
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(MyAnimeListStatus), status))
|
||||
{
|
||||
context.AddFailure($"Invalid status: {status}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class MyAnimeListSettings : IImportListSettings
|
||||
{
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
protected AbstractValidator<MyAnimeListSettings> Validator => new MalSettingsValidator();
|
||||
|
||||
public MyAnimeListSettings()
|
||||
{
|
||||
BaseUrl = "https://api.myanimelist.net/v2";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")]
|
||||
public int ListStatus { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public DateTime Expires { get; set; }
|
||||
|
||||
[FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)]
|
||||
public string SignIn { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||
{
|
||||
public enum MyAnimeListStatus
|
||||
{
|
||||
[FieldOption(label: "All")]
|
||||
All = 0,
|
||||
|
||||
[FieldOption(label: "Watching")]
|
||||
Watching = 1,
|
||||
|
||||
[FieldOption(label: "Completed")]
|
||||
Completed = 2,
|
||||
|
||||
[FieldOption(label: "On Hold")]
|
||||
OnHold = 3,
|
||||
|
||||
[FieldOption(label: "Dropped")]
|
||||
Dropped = 4,
|
||||
|
||||
[FieldOption(label: "Plan to Watch")]
|
||||
PlanToWatch = 5
|
||||
}
|
||||
}
|
||||
@@ -522,7 +522,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
var reports = batch.SelectMany(x => x).ToList();
|
||||
|
||||
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||
_logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||
|
||||
// Update the last search time for all episodes if at least 1 indexer was searched.
|
||||
if (indexers.Any())
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
|
||||
public class RssSyncCommand : Command
|
||||
{
|
||||
public override bool SendUpdatesToClient => true;
|
||||
|
||||
public override bool IsLongRunning => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"AirsTbaOn": "Pendent el seu anunci a {networkLabel}",
|
||||
"AllFiles": "Tots els fitxers",
|
||||
"AllSeriesAreHiddenByTheAppliedFilter": "Tots els resultats estan ocults pel filtre aplicat",
|
||||
"AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {0}",
|
||||
"AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {path}",
|
||||
"AlreadyInYourLibrary": "Ja a la vostra biblioteca",
|
||||
"AlternateTitles": "Títols alternatius",
|
||||
"AnalyseVideoFilesHelpText": "Extraieu informació de vídeo com ara la resolució, el temps d'execució i la informació del còdec dels fitxers. Això requereix que {appName} llegeixi parts del fitxer que poden provocar una activitat elevada al disc o a la xarxa durant les exploracions.",
|
||||
@@ -183,7 +183,7 @@
|
||||
"DeleteRootFolder": "Suprimeix la carpeta arrel",
|
||||
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es pot comunicar amb {downloadClientName}. {errorMessage}",
|
||||
"DownloadClientStatusSingleClientHealthCheckMessage": "Clients de baixada no disponibles a causa d'errors: {downloadClientNames}",
|
||||
"DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {path}. No s'hauria de baixar a una carpeta arrel.",
|
||||
"DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {rootFolderPath}. No s'hauria de baixar a una carpeta arrel.",
|
||||
"DownloadClientSortingHealthCheckMessage": "El client de baixada {downloadClientName} té l'ordenació {sortingMode} activada per a la categoria de {appName}. Hauríeu de desactivar l'ordenació al vostre client de descàrrega per a evitar problemes d'importació.",
|
||||
"HiddenClickToShow": "Amagat, feu clic per a mostrar",
|
||||
"ImportUsingScript": "Importa amb script",
|
||||
@@ -213,7 +213,7 @@
|
||||
"FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions",
|
||||
"False": "Fals",
|
||||
"Implementation": "Implementació",
|
||||
"ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFoldersInfo}",
|
||||
"ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFolderInfo}",
|
||||
"ImportListRootFolderMissingRootHealthCheckMessage": "Falta la carpeta arrel per a les llistes d'importació: {rootFolderInfo}",
|
||||
"IndexerRssNoIndexersEnabledHealthCheckMessage": "No hi ha indexadors disponibles amb la sincronització RSS activada, {appName} no capturarà els nous llançaments automàticament",
|
||||
"ImportListStatusAllUnavailableHealthCheckMessage": "Totes les llistes no estan disponibles a causa d'errors",
|
||||
@@ -261,7 +261,7 @@
|
||||
"AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}",
|
||||
"AutoRedownloadFailedHelpText": "Cerca i intenta baixar automàticament una versió diferent",
|
||||
"AutoTaggingNegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
|
||||
"AutoTaggingRequiredHelpText": "Aquesta condició {implementationName} ha de coincidir perquè s'apliqui la regla d'etiquetatge automàtic. En cas contrari, una única coincidència {implementationName} és suficient.",
|
||||
"AutoTaggingRequiredHelpText": "La condició {implementationName} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {implementationName}.",
|
||||
"BlocklistLoadError": "No es pot carregar la llista de bloqueig",
|
||||
"BlocklistRelease": "Publicació de la llista de bloqueig",
|
||||
"BranchUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern",
|
||||
@@ -301,7 +301,7 @@
|
||||
"Clone": "Clona",
|
||||
"CloneProfile": "Clona el perfil",
|
||||
"CompletedDownloadHandling": "Gestió de descàrregues completades",
|
||||
"CountSeriesSelected": "{selectedCount} sèries seleccionades",
|
||||
"CountSeriesSelected": "{count} sèries seleccionades",
|
||||
"InteractiveImportLoadError": "No es poden carregar els elements d'importació manual",
|
||||
"ChownGroupHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega utilitza el mateix grup que {appName}.",
|
||||
"ChmodFolderHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega estableixi correctament els permisos.",
|
||||
@@ -451,7 +451,7 @@
|
||||
"DeleteEpisodesFilesHelpText": "Suprimeix els fitxers de l'episodi i la carpeta de la sèrie",
|
||||
"DeleteRemotePathMapping": "Editeu el mapa de camins remots",
|
||||
"DefaultNotFoundMessage": "Deu estar perdut, no hi ha res a veure aquí.",
|
||||
"DelayMinutes": "{delay} minuts",
|
||||
"DelayMinutes": "{delay} Minuts",
|
||||
"DelayProfile": "Perfil de retard",
|
||||
"DeleteImportListExclusionMessageText": "Esteu segur que voleu suprimir aquesta exclusió de la llista d'importació?",
|
||||
"DeleteReleaseProfile": "Suprimeix el perfil de llançament",
|
||||
@@ -665,5 +665,51 @@
|
||||
"NotificationsPushoverSettingsRetry": "Torna-ho a provar",
|
||||
"NotificationsSettingsWebhookMethod": "Mètode",
|
||||
"Other": "Altres",
|
||||
"Monitor": "Monitora"
|
||||
"Monitor": "Monitora",
|
||||
"AutoTaggingSpecificationOriginalLanguage": "Llenguatge",
|
||||
"AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat",
|
||||
"AutoTaggingSpecificationRootFolder": "Carpeta arrel",
|
||||
"AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar",
|
||||
"AutoTaggingSpecificationSeriesType": "Tipus de Sèries",
|
||||
"AutoTaggingSpecificationStatus": "Estat",
|
||||
"BlocklistAndSearch": "Llista de bloqueig i cerca",
|
||||
"BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2",
|
||||
"Directory": "Directori",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}",
|
||||
"Destination": "Destinació",
|
||||
"Umask": "UMask",
|
||||
"ConnectionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}",
|
||||
"DoNotBlocklist": "No afegiu a la llista de bloqueig",
|
||||
"DoNotBlocklistHint": "Elimina sense afegir a la llista de bloqueig",
|
||||
"Donate": "Dona",
|
||||
"DownloadClientDelugeTorrentStateError": "Deluge està informant d'un error",
|
||||
"NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.",
|
||||
"TorrentDelayTime": "Retard del torrent: {torrentDelay}",
|
||||
"CustomFormatsSpecificationRegularExpression": "Expressió regular",
|
||||
"RemoveFromDownloadClient": "Elimina del client de baixada",
|
||||
"StartupDirectory": "Directori d'inici",
|
||||
"ClickToChangeIndexerFlags": "Feu clic per canviar els indicadors de l'indexador",
|
||||
"ImportListsSettingsSummary": "Importa des d'una altra instància {appName} o llistes de Trakt i gestiona les exclusions de llistes",
|
||||
"DeleteSpecificationHelpText": "Esteu segur que voleu suprimir l'especificació '{name}'?",
|
||||
"DeleteSpecification": "Esborra especificació",
|
||||
"UsenetDelayTime": "Retard d'Usenet: {usenetDelay}",
|
||||
"DownloadClientDelugeSettingsDirectory": "Directori de baixada",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"RetryingDownloadOn": "S'està retardant la baixada fins al {date} a les {time}",
|
||||
"ListWillRefreshEveryInterval": "La llista s'actualitzarà cada {refreshInterval}",
|
||||
"BlocklistAndSearchMultipleHint": "Comença una cerca per reemplaçar després d'haver bloquejat",
|
||||
"BlocklistMultipleOnlyHint": "Afegeix a la llista de bloqueig sense cercar substituts",
|
||||
"BlocklistOnly": "Sols afegir a la llista de bloqueig",
|
||||
"BlocklistOnlyHint": "Afegir a la llista de bloqueig sense cercar substituts",
|
||||
"ChangeCategory": "Canvia categoria",
|
||||
"ChangeCategoryHint": "Canvia la baixada a la \"Categoria post-importació\" des del client de descàrrega",
|
||||
"ChangeCategoryMultipleHint": "Canvia les baixades a la \"Categoria post-importació\" des del client de descàrrega",
|
||||
"BlocklistReleaseHelpText": "Impedeix que {appName} baixi aquesta versió mitjançant RSS o cerca automàtica",
|
||||
"MinutesSixty": "60 minuts: {sixty}",
|
||||
"CustomFilter": "Filtres personalitzats",
|
||||
"CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules",
|
||||
"CustomFormatsSpecificationFlag": "Bandera"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"CancelProcessing": "Zrušit zpracování",
|
||||
"CheckDownloadClientForDetails": "zkontrolujte klienta pro stahování pro více informací",
|
||||
"ChmodFolderHelpText": "Octal, aplikováno během importu / přejmenování na mediální složky a soubory (bez provádění bitů)",
|
||||
"ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.",
|
||||
"ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.",
|
||||
"ChooseAnotherFolder": "Vyberte jinou složku",
|
||||
"ChownGroup": "Skupina chown",
|
||||
"ConnectSettings": "Nastavení připojení",
|
||||
@@ -67,7 +67,7 @@
|
||||
"CalendarLoadError": "Nelze načíst kalendář",
|
||||
"CertificateValidationHelpText": "Změňte přísnost ověřování certifikátů HTTPS. Neměňte, pokud nerozumíte rizikům.",
|
||||
"ChownGroupHelpText": "Název skupiny nebo gid. Použijte gid pro vzdálené systémy souborů.",
|
||||
"ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.",
|
||||
"ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient stahování používal stejnou skupinu jako {appName}.",
|
||||
"ClientPriority": "Priorita klienta",
|
||||
"Clone": "Klonovat",
|
||||
"CloneIndexer": "Klonovat indexátor",
|
||||
@@ -319,5 +319,5 @@
|
||||
"EditSelectedImportLists": "Upravit vybrané seznamy k importu",
|
||||
"FormatDateTime": "{formattedDate} {formattedTime}",
|
||||
"AddRootFolderError": "Nepodařilo se přidat kořenový adresář",
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {1}."
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}."
|
||||
}
|
||||
|
||||
@@ -17,5 +17,22 @@
|
||||
"AddANewPath": "Tilføj en ny sti",
|
||||
"AddConditionImplementation": "Tilføj betingelse - {implementationName}",
|
||||
"AddConnectionImplementation": "Tilføj forbindelse - {implementationName}",
|
||||
"AddCustomFilter": "Tilføj tilpasset filter"
|
||||
"AddCustomFilter": "Tilføj tilpasset filter",
|
||||
"ApplyChanges": "Anvend ændringer",
|
||||
"Test": "Afprøv",
|
||||
"AddImportList": "Tilføj importliste",
|
||||
"AddExclusion": "Tilføj undtagelse",
|
||||
"TestAll": "Afprøv alle",
|
||||
"TestAllClients": "Afprøv alle klienter",
|
||||
"TestAllLists": "Afprøv alle lister",
|
||||
"Unknown": "Ukendt",
|
||||
"AllTitles": "All titler",
|
||||
"TablePageSize": "Sidestørrelse",
|
||||
"TestAllIndexers": "Afprøv alle indeks",
|
||||
"AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
|
||||
"AddIndexerError": "Kunne ikke tilføje en ny indekser. Prøv igen.",
|
||||
"AddImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||
"AddRootFolderError": "Kunne ikke tilføje rodmappe",
|
||||
"Table": "Tabel",
|
||||
"AddIndexer": "Tilføj indekser"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"AutomaticAdd": "Automatisch hinzufügen",
|
||||
"CountSeasons": "{count} Staffeln",
|
||||
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Es ist kein Download-Client verfügbar",
|
||||
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich.",
|
||||
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich. {errorMessage}",
|
||||
"DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {downloadClientName} legt Downloads im Stammordner {rootFolderPath} ab. Sie sollten nicht in einen Stammordner herunterladen.",
|
||||
"DownloadClientSortingHealthCheckMessage": "Im Download-Client {downloadClientName} ist die Sortierung {sortingMode} für die Kategorie von {appName} aktiviert. Sie sollten die Sortierung in Ihrem Download-Client deaktivieren, um Importprobleme zu vermeiden.",
|
||||
"DownloadClientStatusSingleClientHealthCheckMessage": "Download-Clients sind aufgrund von Fehlern nicht verfügbar: {downloadClientNames}",
|
||||
@@ -41,7 +41,7 @@
|
||||
"SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen",
|
||||
"AbsoluteEpisodeNumber": "Exakte Folgennummer",
|
||||
"AddConnection": "Verbindung hinzufügen",
|
||||
"AddAutoTagError": "Der neue automatische Tag konnte nicht hinzugefügt werden, bitte versuche es erneut.",
|
||||
"AddAutoTagError": "Auto-Tag konnte nicht hinzugefügt werden. Bitte erneut versuchen.",
|
||||
"AddConditionError": "Neue Bedingung konnte nicht hinzugefügt werden, bitte erneut versuchen.",
|
||||
"AddCustomFormat": "Eigenes Format hinzufügen",
|
||||
"AddCustomFormatError": "Neues eigenes Format kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.",
|
||||
@@ -86,7 +86,7 @@
|
||||
"QuickSearch": "Schnelle Suche",
|
||||
"ReadTheWikiForMoreInformation": "Lesen Sie das Wiki für weitere Informationen",
|
||||
"Real": "Real",
|
||||
"RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {Pfad}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.",
|
||||
"RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {path}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.",
|
||||
"RecyclingBin": "Papierkorb",
|
||||
"RecyclingBinCleanup": "Papierkorb leeren",
|
||||
"RefreshSeries": "Serie aktualisieren",
|
||||
@@ -146,7 +146,7 @@
|
||||
"AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.",
|
||||
"AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien extrahieren. Dies erfordert, dass {appName} Teile der Datei liest, was bei Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.",
|
||||
"AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.",
|
||||
"AutoTaggingNegateHelpText": "Wenn diese Option aktiviert ist, wird die automatische Tagging-Regel nicht angewendet, wenn diese {implementationName}-Bedingung zutrifft.",
|
||||
"AutoTaggingNegateHelpText": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
|
||||
"CopyUsingHardlinksSeriesHelpText": "Mithilfe von Hardlinks kann {appName} Seeding-Torrents in den Serienordner importieren, ohne zusätzlichen Speicherplatz zu beanspruchen oder den gesamten Inhalt der Datei zu kopieren. Hardlinks funktionieren nur, wenn sich Quelle und Ziel auf demselben Volume befinden",
|
||||
"DailyEpisodeTypeFormat": "Datum ({format})",
|
||||
"DefaultDelayProfileSeries": "Dies ist das Standardprofil. Es gilt für alle Serien, die kein explizites Profil haben.",
|
||||
@@ -171,7 +171,7 @@
|
||||
"BackupIntervalHelpText": "Intervall zwischen automatischen Sicherungen",
|
||||
"BuiltIn": "Eingebaut",
|
||||
"ChangeFileDate": "Ändern Sie das Dateidatum",
|
||||
"CustomFormatsLoadError": "Benutzerdefinierte Formate können nicht geladen werden",
|
||||
"CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden",
|
||||
"DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?",
|
||||
"DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren",
|
||||
"DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen",
|
||||
@@ -202,10 +202,10 @@
|
||||
"AuthBasic": "Basis (Browser-Popup)",
|
||||
"AuthForm": "Formulare (Anmeldeseite)",
|
||||
"Authentication": "Authentifizierung",
|
||||
"AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich.",
|
||||
"AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich",
|
||||
"Automatic": "Automatisch",
|
||||
"AutomaticSearch": "Automatische Suche",
|
||||
"AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss zutreffen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht eine einzelne {implementationName}-Übereinstimmung aus.",
|
||||
"AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.",
|
||||
"BackupRetentionHelpText": "Automatische Backups, die älter als der Aufbewahrungszeitraum sind, werden automatisch bereinigt",
|
||||
"BindAddressHelpText": "Gültige IP-Adresse, localhost oder „*“ für alle Schnittstellen",
|
||||
"BackupsLoadError": "Sicherrungen können nicht geladen werden",
|
||||
@@ -280,23 +280,23 @@
|
||||
"Custom": "Benutzerdefiniert",
|
||||
"CustomFilters": "Benutzerdefinierte Filter",
|
||||
"CustomFormat": "Benutzerdefiniertes Format",
|
||||
"CustomFormats": "Benutzerdefinierte Formate",
|
||||
"CustomFormatsSettingsSummary": "Benutzerdefinierte Formate und Einstellungen",
|
||||
"CustomFormats": "Eigene Formate",
|
||||
"CustomFormatsSettingsSummary": "Eigene Formate und Einstellungen",
|
||||
"DailyEpisodeFormat": "Tägliches Episodenformat",
|
||||
"Database": "Datenbank",
|
||||
"Dates": "Termine",
|
||||
"Day": "Tag",
|
||||
"Default": "Standard",
|
||||
"DefaultCase": "Standardfall",
|
||||
"DefaultNameCopiedProfile": "{Name} – Kopieren",
|
||||
"DefaultNameCopiedSpecification": "{Name} – Kopieren",
|
||||
"DefaultNameCopiedProfile": "{name} – Kopieren",
|
||||
"DefaultNameCopiedSpecification": "{name} – Kopieren",
|
||||
"DefaultNotFoundMessage": "Sie müssen verloren sein, hier gibt es nichts zu sehen.",
|
||||
"DelayMinutes": "{delay} Minuten",
|
||||
"DelayProfile": "Verzögerungsprofil",
|
||||
"DelayProfileProtocol": "Protokoll: {preferredProtocol}",
|
||||
"DelayProfiles": "Verzögerungsprofile",
|
||||
"DelayProfilesLoadError": "Verzögerungsprofile können nicht geladen werden",
|
||||
"DelayingDownloadUntil": "Download wird bis zum {Datum} um {Uhrzeit} verzögert",
|
||||
"DelayingDownloadUntil": "Download wird bis zum {date} um {time} verzögert",
|
||||
"DeleteAutoTag": "Auto-Tag löschen",
|
||||
"DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?",
|
||||
"DeleteBackup": "Sicherung löschen",
|
||||
@@ -350,7 +350,7 @@
|
||||
"DownloadClientDownloadStationValidationFolderMissing": "Ordner existiert nicht",
|
||||
"DownloadClientDownloadStationValidationFolderMissingDetail": "Der Ordner „{downloadDir}“ existiert nicht, er muss manuell im freigegebenen Ordner „{sharedFolder}“ erstellt werden.",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestination": "Kein Standardziel",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {Benutzername} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {username} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissing": "Der freigegebene Ordner existiert nicht",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Die Diskstation verfügt nicht über einen freigegebenen Ordner mit dem Namen „{sharedFolder}“. Sind Sie sicher, dass Sie ihn richtig angegeben haben?",
|
||||
"DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags",
|
||||
@@ -372,7 +372,7 @@
|
||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „app_token“)",
|
||||
"DownloadClientFreeboxSettingsHostHelpText": "Hostname oder Host-IP-Adresse der Freebox, standardmäßig „{url}“ (funktioniert nur im selben Netzwerk)",
|
||||
"DownloadClientFreeboxSettingsPortHelpText": "Port, der für den Zugriff auf die Freebox-Schnittstelle verwendet wird, standardmäßig ist „{port}“",
|
||||
"DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {ExceptionMessage})",
|
||||
"DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {exceptionMessage})",
|
||||
"DownloadClientFreeboxUnableToReachFreeboxApi": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellung „API-URL“ für Basis-URL und Version.",
|
||||
"DownloadClientNzbVortexMultipleFilesMessage": "Der Download enthält mehrere Dateien und befindet sich nicht in einem Jobordner: {outputPath}",
|
||||
"DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0",
|
||||
@@ -446,7 +446,7 @@
|
||||
"Restore": "Wiederherstellen",
|
||||
"RestartRequiredWindowsService": "Je nachdem, welcher Benutzer den {appName}-Dienst ausführt, müssen Sie {appName} möglicherweise einmal als Administrator neu starten, bevor der Dienst automatisch gestartet wird.",
|
||||
"RestartSonarr": "{appName} neu starten",
|
||||
"RetryingDownloadOn": "Erneuter Downloadversuch am {Datum} um {Uhrzeit}",
|
||||
"RetryingDownloadOn": "Erneuter Downloadversuch am {date} um {time}",
|
||||
"SceneInfo": "Szeneninfo",
|
||||
"Scene": "Szene",
|
||||
"SaveSettings": "Einstellungen speichern",
|
||||
@@ -525,7 +525,7 @@
|
||||
"UsenetDelayTime": "Usenet-Verzögerung: {usenetDelay}",
|
||||
"UsenetDelayHelpText": "Verzögerung in Minuten, bevor Sie eine Veröffentlichung aus dem Usenet erhalten",
|
||||
"VideoCodec": "Video-Codec",
|
||||
"VersionNumber": "Version {Version}",
|
||||
"VersionNumber": "Version {version}",
|
||||
"Version": "Version",
|
||||
"WantMoreControlAddACustomFormat": "Möchten Sie mehr Kontrolle darüber haben, welche Downloads bevorzugt werden? Fügen Sie ein [benutzerdefiniertes Format] hinzu (/settings/customformats)",
|
||||
"WaitingToProcess": "Warten auf Bearbeitung",
|
||||
@@ -540,7 +540,7 @@
|
||||
"ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu",
|
||||
"ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen",
|
||||
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).",
|
||||
"Wanted": "› Gesucht",
|
||||
"Wanted": "Gesucht",
|
||||
"ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.",
|
||||
"Continuing": "Fortsetzung",
|
||||
"CopyUsingHardlinksHelpTextWarning": "Gelegentlich können Dateisperren das Umbenennen von Dateien verhindern, die geseedet werden. Sie können das Seeding vorübergehend deaktivieren und als Workaround die Umbenennungsfunktion von {appName} verwenden.",
|
||||
@@ -549,9 +549,8 @@
|
||||
"CountImportListsSelected": "{count} Importliste(n) ausgewählt",
|
||||
"CountIndexersSelected": "{count} Indexer ausgewählt",
|
||||
"CountSelectedFiles": "{selectedCount} ausgewählte Dateien",
|
||||
"CustomFormatUnknownCondition": "Unknown Custom Format condition '{implementation}'",
|
||||
"CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“",
|
||||
"CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen",
|
||||
"CustomFormatsSettings": "Einstellungen für eigene Formate",
|
||||
"Daily": "Täglich",
|
||||
"Dash": "Bindestrich",
|
||||
"Debug": "Debuggen",
|
||||
@@ -710,7 +709,7 @@
|
||||
"ClickToChangeSeason": "Klicken Sie hier, um die Staffel zu ändern",
|
||||
"BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert",
|
||||
"BlackholeWatchFolder": "Überwachter Ordner",
|
||||
"BlackholeWatchFolderHelpText": "Ordner, aus dem {appName} abgeschlossene Downloads importieren soll",
|
||||
"BlackholeWatchFolderHelpText": "Der Ordner, aus dem {appName} fertige Downloads importieren soll",
|
||||
"BrowserReloadRequired": "Neuladen des Browsers erforderlich",
|
||||
"CalendarOptions": "Kalenderoptionen",
|
||||
"CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?",
|
||||
@@ -777,5 +776,16 @@
|
||||
"Airs": "Wird ausgestrahlt",
|
||||
"AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden",
|
||||
"IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht",
|
||||
"DownloadClientSettingsAddPaused": "Pausiert hinzufügen"
|
||||
"DownloadClientSettingsAddPaused": "Pausiert hinzufügen",
|
||||
"ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern",
|
||||
"BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten",
|
||||
"BlocklistAndSearch": "Sperrliste und Suche",
|
||||
"AddDelayProfileError": "Verzögerungsprofil konnte nicht hinzugefügt werden. Bitte erneut versuchen.",
|
||||
"BlocklistAndSearchHint": "Starte Suche nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
|
||||
"BlocklistAndSearchMultipleHint": "Starte Suchen nach einer Alternative, falls es der Sperrliste hinzugefügt wurde",
|
||||
"BlocklistMultipleOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternativen zu suchen",
|
||||
"BlocklistOnly": "Nur der Sperrliste hinzufügen",
|
||||
"BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen",
|
||||
"BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren",
|
||||
"ChangeCategory": "Kategorie wechseln"
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {selectedCount} αντικείμενα από την ουρά;",
|
||||
"CloneCondition": "Κλωνοποίηση συνθήκης",
|
||||
"RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;",
|
||||
"AddConditionImplementation": "Προσθήκη",
|
||||
"AddConditionImplementation": "Προσθήκη - {implementationName}",
|
||||
"AppUpdated": "{appName} Ενημερώθηκε",
|
||||
"AutoAdd": "Προσθήκη",
|
||||
"AddConnectionImplementation": "Προσθήκη"
|
||||
"AddConnectionImplementation": "Προσθήκη - {implementationName}"
|
||||
}
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"ClickToChangeLanguage": "Click to change language",
|
||||
"ClickToChangeQuality": "Click to change quality",
|
||||
"ClickToChangeReleaseGroup": "Click to change release group",
|
||||
"ClickToChangeReleaseType": "Click to change release type",
|
||||
"ClickToChangeSeason": "Click to change season",
|
||||
"ClickToChangeSeries": "Click to change series",
|
||||
"ClientPriority": "Client Priority",
|
||||
@@ -278,6 +279,7 @@
|
||||
"CustomFormats": "Custom Formats",
|
||||
"CustomFormatsLoadError": "Unable to load Custom Formats",
|
||||
"CustomFormatsSettings": "Custom Formats Settings",
|
||||
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
|
||||
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
|
||||
"CustomFormatsSpecificationFlag": "Flag",
|
||||
"CustomFormatsSpecificationLanguage": "Language",
|
||||
@@ -378,7 +380,7 @@
|
||||
"DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?",
|
||||
"Deleted": "Deleted",
|
||||
"DeletedReasonEpisodeMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the episode in the database",
|
||||
"DeletedReasonManual": "File was deleted by via UI",
|
||||
"DeletedReasonManual": "File was deleted using {appName}, either manually or by another tool through the API",
|
||||
"DeletedReasonUpgrade": "File was deleted to import an upgrade",
|
||||
"DeletedSeriesDescription": "Series was deleted from TheTVDB",
|
||||
"Destination": "Destination",
|
||||
@@ -635,6 +637,7 @@
|
||||
"EpisodeRequested": "Episode Requested",
|
||||
"EpisodeSearchResultsLoadError": "Unable to load results for this episode search. Try again later",
|
||||
"EpisodeTitle": "Episode Title",
|
||||
"EpisodeTitleFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Episode Title:30}`) or the beginning (e.g. `{Episode Title:-30}`) are both supported. Episode titles will be automatically truncated to file system limitations if necessary.",
|
||||
"EpisodeTitleRequired": "Episode Title Required",
|
||||
"EpisodeTitleRequiredHelpText": "Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA",
|
||||
"Episodes": "Episodes",
|
||||
@@ -838,6 +841,9 @@
|
||||
"ImportListsImdbSettingsListId": "List ID",
|
||||
"ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)",
|
||||
"ImportListsLoadError": "Unable to load Import Lists",
|
||||
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authenticate with MyAnimeList",
|
||||
"ImportListsMyAnimeListSettingsListStatus": "List Status",
|
||||
"ImportListsMyAnimeListSettingsListStatusHelpText": "Type of list you want to import from, set to 'All' for all lists",
|
||||
"ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv",
|
||||
"ImportListsPlexSettingsWatchlistName": "Plex Watchlist",
|
||||
"ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS",
|
||||
@@ -1586,11 +1592,12 @@
|
||||
"RelativePath": "Relative Path",
|
||||
"Release": "Release",
|
||||
"ReleaseGroup": "Release Group",
|
||||
"ReleaseGroupFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Release Group:30}`) or the beginning (e.g. `{Release Group:-30}`) are both supported.`).",
|
||||
"ReleaseGroups": "Release Groups",
|
||||
"ReleaseHash": "Release Hash",
|
||||
"ReleaseProfile": "Release Profile",
|
||||
"ReleaseProfileIndexerHelpText": "Specify what indexer the profile applies to",
|
||||
"ReleaseProfileIndexerHelpTextWarning": "Using a specific indexer with release profiles can lead to duplicate releases being grabbed",
|
||||
"ReleaseProfileIndexerHelpTextWarning": "Setting a specific indexer on a release profile will cause this profile to only apply to releases from that indexer.",
|
||||
"ReleaseProfileTagSeriesHelpText": "Release profiles will apply to series with at least one matching tag. Leave blank to apply to all series",
|
||||
"ReleaseProfiles": "Release Profiles",
|
||||
"ReleaseProfilesLoadError": "Unable to load Release Profiles",
|
||||
@@ -1771,11 +1778,13 @@
|
||||
"SelectLanguages": "Select Languages",
|
||||
"SelectQuality": "Select Quality",
|
||||
"SelectReleaseGroup": "Select Release Group",
|
||||
"SelectReleaseType": "Select Release Type",
|
||||
"SelectSeason": "Select Season",
|
||||
"SelectSeasonModalTitle": "{modalTitle} - Select Season",
|
||||
"SelectSeries": "Select Series",
|
||||
"SendAnonymousUsageData": "Send Anonymous Usage Data",
|
||||
"Series": "Series",
|
||||
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
|
||||
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .",
|
||||
"SeriesCannotBeFound": "Sorry, that series cannot be found.",
|
||||
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files",
|
||||
|
||||
@@ -167,8 +167,8 @@
|
||||
"AllResultsAreHiddenByTheAppliedFilter": "Todos los resultados están ocultos por el filtro aplicado",
|
||||
"AnalyseVideoFilesHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.",
|
||||
"AnimeEpisodeTypeDescription": "Episodios lanzados usando un número de episodio absoluto",
|
||||
"ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración",
|
||||
"AppDataLocationHealthCheckMessage": "No será posible actualizar para prevenir la eliminación de AppData al Actualizar",
|
||||
"ApiKeyValidationHealthCheckMessage": "Por favor actualiza tu clave API para que tenga de longitud al menos {length} caracteres. Puedes hacerlo en los ajustes o en el archivo de configuración",
|
||||
"AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar",
|
||||
"Scheduled": "Programado",
|
||||
"Season": "Temporada",
|
||||
"Clone": "Clonar",
|
||||
@@ -308,7 +308,7 @@
|
||||
"CountSeasons": "{count} Temporadas",
|
||||
"BranchUpdate": "Rama a usar para actualizar {appName}",
|
||||
"ChmodFolder": "Carpeta chmod",
|
||||
"CheckDownloadClientForDetails": "Revisar cliente de descarpa para mas detalles",
|
||||
"CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles",
|
||||
"ChooseAnotherFolder": "Elige otra Carpeta",
|
||||
"ClientPriority": "Prioridad del Cliente",
|
||||
"CloneIndexer": "Clonar Indexer",
|
||||
@@ -324,11 +324,11 @@
|
||||
"ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados",
|
||||
"ConnectSettings": "Conectar Ajustes",
|
||||
"CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'",
|
||||
"XmlRpcPath": "Ruta XML RPC",
|
||||
"AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no aplicará si la condición {implementationName} coincide.",
|
||||
"XmlRpcPath": "Ruta RPC de XML",
|
||||
"AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no se aplicará si esta condición {implementationName} coincide.",
|
||||
"CloneCustomFormat": "Clonar formato personalizado",
|
||||
"Close": "Cerrar",
|
||||
"AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {0} será suficiente.",
|
||||
"AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {implementationName} será suficiente.",
|
||||
"WeekColumnHeaderHelpText": "Mostrado sobre cada columna cuando la vista activa es semana",
|
||||
"WhyCantIFindMyShow": "Por que no puedo encontrar mi serie?",
|
||||
"WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?",
|
||||
@@ -357,7 +357,7 @@
|
||||
"ChangeFileDate": "Cambiar fecha de archivo",
|
||||
"CertificateValidationHelpText": "Cambiar como es la validacion de la certificacion estricta de HTTPS. No cambiar a menos que entiendas las consecuencias.",
|
||||
"AddListExclusion": "Agregar Lista de Exclusión",
|
||||
"AddedDate": "Agregado: {fecha}",
|
||||
"AddedDate": "Agregado: {date}",
|
||||
"AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado",
|
||||
"AlternateTitles": "Titulos alternativos",
|
||||
"ChmodFolderHelpText": "Octal, aplicado durante la importación / cambio de nombre a carpetas y archivos multimedia (sin bits de ejecución)",
|
||||
@@ -369,7 +369,7 @@
|
||||
"AirsTbaOn": "A anunciar en {networkLabel}",
|
||||
"AllFiles": "Todos los archivos",
|
||||
"Any": "Cualquiera",
|
||||
"AirsTomorrowOn": "Mañana a las {hora} en {networkLabel}",
|
||||
"AirsTomorrowOn": "Mañana a las {time} en {networkLabel}",
|
||||
"AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes, necesitará recargar {appName} ",
|
||||
"AddListExclusionSeriesHelpText": "Evitar que las series sean agregadas a {appName} por las listas",
|
||||
"CalendarLegendEpisodeDownloadedTooltip": "El episodio fue descargado y ordenado",
|
||||
@@ -530,20 +530,20 @@
|
||||
"DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series",
|
||||
"DoNotPrefer": "No preferir",
|
||||
"DoNotUpgradeAutomatically": "No actualizar automáticamente",
|
||||
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, en blanco usa el defecto por el cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
|
||||
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
|
||||
"Download": "Descargar",
|
||||
"Donate": "Donar",
|
||||
"DownloadClientDelugeValidationLabelPluginFailure": "Falló la configuración de la etiqueta",
|
||||
"DownloadClientDelugeTorrentStateError": "Deluge está informando de un error",
|
||||
"DownloadClientDownloadStationValidationFolderMissing": "No existe la carpeta",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en los ajustes de DownloadStation en BT/HTTP/FTP/NZB -> Ubicación.",
|
||||
"DownloadClientFreeboxSettingsAppIdHelpText": "ID de la aplicación cuando se crea acceso a la API de Freebox (i.e. 'app_id')",
|
||||
"DownloadClientFreeboxSettingsAppToken": "Token de la aplicación",
|
||||
"DownloadClientFreeboxSettingsAppIdHelpText": "ID de la app dada cuando se crea acceso a la API de Freebox (esto es 'app_id')",
|
||||
"DownloadClientFreeboxSettingsAppToken": "Token de la app",
|
||||
"DownloadClientFreeboxUnableToReachFreebox": "No es posible acceder a la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})",
|
||||
"DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene varios archivos y no está en una carpeta de trabajo: {outputPath}",
|
||||
"DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0",
|
||||
"DownloadClientOptionsLoadError": "No es posible cargar las opciones del cliente de descarga",
|
||||
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta tendrá que ser accesible desde XBMC",
|
||||
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC",
|
||||
"DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb",
|
||||
"Docker": "Docker",
|
||||
"DockerUpdater": "Actualiza el contenedor docker para recibir la actualización",
|
||||
@@ -570,17 +570,17 @@
|
||||
"DotNetVersion": ".NET",
|
||||
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Ningún cliente de descarga disponible",
|
||||
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es posible comunicarse con {downloadClientName}. {errorMessage}",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo a la url json de deluge, ver {url}",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo al url del json de deluge, vea {url}",
|
||||
"DownloadClientDownloadStationValidationApiVersion": "Versión de la API de la Estación de Descarga no soportada, debería ser al menos {requiredVersion}. Soporte desde {minVersion} hasta {maxVersion}",
|
||||
"DownloadClientDownloadStationValidationFolderMissingDetail": "No existe la carpeta '{downloadDir}', debe ser creada manualmente dentro de la Carpeta Compartida '{sharedFolder}'.",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestination": "Sin destino predeterminado",
|
||||
"DownloadClientFreeboxNotLoggedIn": "No ha iniciado sesión",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "URL de la API",
|
||||
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API de Freebox con la versión de la API, p.ej. '{url}', por defecto es '{defaultApiUrl}'",
|
||||
"DownloadClientFreeboxSettingsAppId": "ID de la aplicación",
|
||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "App token recuperado cuando se crea el acceso a la API de Freebox (i.e. 'app_token')",
|
||||
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, por defecto es '{port}'",
|
||||
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "URL de API",
|
||||
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'",
|
||||
"DownloadClientFreeboxSettingsAppId": "ID de la app",
|
||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')",
|
||||
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'",
|
||||
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)",
|
||||
"DownloadClientFreeboxUnableToReachFreeboxApi": "No es posible acceder a la API de Freebox. Verifica la configuración 'URL de la API' para la URL base y la versión.",
|
||||
"DownloadClientNzbgetValidationKeepHistoryOverMax": "La opción KeepHistory de NZBGet debería ser menor de 25000",
|
||||
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.",
|
||||
@@ -605,7 +605,7 @@
|
||||
"EnableHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato",
|
||||
"EnableMediaInfoHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.",
|
||||
"TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)",
|
||||
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, por defecto es '.magnet'",
|
||||
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, predeterminado a '.magnet'",
|
||||
"DownloadIgnoredEpisodeTooltip": "Descarga de episodio ignorada",
|
||||
"EditDelayProfile": "Editar perfil de retraso",
|
||||
"DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}",
|
||||
@@ -613,13 +613,13 @@
|
||||
"EditReleaseProfile": "Editar perfil de lanzamiento",
|
||||
"DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm",
|
||||
"DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría",
|
||||
"DownloadClientRTorrentSettingsUrlPath": "Ruta de la url",
|
||||
"DownloadClientRTorrentSettingsUrlPath": "Ruta de url",
|
||||
"DownloadClientSabnzbdValidationDevelopVersion": "Versión de desarrollo de Sabnzbd, asumiendo versión 3.0.0 o superior.",
|
||||
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} de rastrear nuevas descargas. Sabnzbd también recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que resulta más efectivo.",
|
||||
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName] puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.",
|
||||
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.",
|
||||
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debe deshabilitar la ordenación por fechas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
|
||||
"DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url de {clientName}, como {url}",
|
||||
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}",
|
||||
"DownloadClientStatusAllClientHealthCheckMessage": "Ningún cliente de descarga está disponible debido a fallos",
|
||||
"DownloadClientValidationGroupMissing": "El grupo no existe",
|
||||
"DownloadClientValidationSslConnectFailure": "No es posible conectarse a través de SSL",
|
||||
@@ -641,7 +641,7 @@
|
||||
"EnableInteractiveSearchHelpText": "Se usará cuando se utilice la búsqueda interactiva",
|
||||
"DoneEditingGroups": "Terminado de editar grupos",
|
||||
"DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales",
|
||||
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades multimedia como etiquetas. Sugerencias a modo de ejemplo.",
|
||||
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades de medios como etiquetas. Los consejos son ejemplos.",
|
||||
"DownloadClientFloodSettingsPostImportTagsHelpText": "Añade etiquetas después de que se importe una descarga.",
|
||||
"DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basada en el criterio de sembrado actual en Ajustes -> Indexadores",
|
||||
"DownloadClientFloodSettingsPostImportTags": "Etiquetas tras importación",
|
||||
@@ -649,7 +649,7 @@
|
||||
"DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir",
|
||||
"DownloadClientFreeboxApiError": "La API de Freebox devolvió el error: {errorDescription}",
|
||||
"DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. El motivo: {errorDescription}",
|
||||
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Se importarán los archivos .strm en esta carpeta por drone",
|
||||
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone",
|
||||
"DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial",
|
||||
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado",
|
||||
@@ -658,26 +658,26 @@
|
||||
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar descargas completadas sin una categoría.",
|
||||
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Poner en cola el torrent no está habilitado en los ajustes de su qBittorrent. Habilítelo en qBittorrent o seleccione 'Último' como prioridad.",
|
||||
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} no podrá efectuar el Manejo de Descargas Completadas según lo configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'",
|
||||
"DownloadClientRTorrentSettingsAddStopped": "Añadir parados",
|
||||
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitarlo añadirá los torrents y magnets a rTorrent en un estado parado. Esto puede romper los archivos magnet.",
|
||||
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de rTorrent",
|
||||
"DownloadClientRTorrentSettingsAddStopped": "Añadir detenido",
|
||||
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.",
|
||||
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent",
|
||||
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El cliente de descarga {downloadClientName} se establece para eliminar las descargas completadas. Esto puede resultar en descargas siendo eliminadas de tu cliente antes de que {appName} pueda importarlas.",
|
||||
"DownloadClientRootFolderHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en la carpeta raíz {rootFolderPath}. No debería descargar a una carpeta raíz.",
|
||||
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar la ordenación por fechas",
|
||||
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debe deshabilitar la ordenación de películas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
|
||||
"DownloadClientSettingsCategorySubFolderHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.",
|
||||
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, déjelo en blanco para usar el predeterminado",
|
||||
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado",
|
||||
"DownloadClientSettingsInitialState": "Estado inicial",
|
||||
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a {clientName}",
|
||||
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}",
|
||||
"DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión hace más de 14 días",
|
||||
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está descargando metadatos",
|
||||
"DownloadClientSettingsPostImportCategoryHelpText": "Categoría para {appName} que se establece después de que se haya importado la descarga. {appName} no eliminará los torrents en esa categoría incluso si finalizó la siembra. Déjelo en blanco para mantener la misma categoría.",
|
||||
"DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión dentro de los últimos 14 días",
|
||||
"DownloadClientSettingsUseSslHelpText": "Usa conexión segura cuando haya una conexión a {clientName}",
|
||||
"DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}",
|
||||
"DownloadClientSortingHealthCheckMessage": "El cliente de descarga {downloadClientName} tiene habilitada la ordenación {sortingMode} para la categoría de {appName}. Debería deshabilitar la ordenación en su cliente de descarga para evitar problemas al importar.",
|
||||
"DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de descarga no disponibles debido a fallos: {downloadClientNames}",
|
||||
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de Transmission",
|
||||
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p.ej. {url}, por defecto es '{defaultUrl}'",
|
||||
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission",
|
||||
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'",
|
||||
"DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error",
|
||||
"DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta",
|
||||
"DownloadClientValidationApiKeyRequired": "Clave API requerida",
|
||||
@@ -706,11 +706,11 @@
|
||||
"DownloadClientSabnzbdValidationUnknownVersion": "Versión desconocida: {rawVersion}",
|
||||
"DownloadClientSettingsAddPaused": "Añadir pausado",
|
||||
"DownloadClientSeriesTagHelpText": "Solo use este cliente de descarga para series con al menos una etiqueta coincidente. Déjelo en blanco para usarlo con todas las series.",
|
||||
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Consulte Opciones -> Interfaz Web -> 'Usar HTTPS en lugar de HTTP' en qBittorrent.",
|
||||
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.",
|
||||
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} no pudo añadir la etiqueta a qBittorrent.",
|
||||
"DownloadClientQbittorrentValidationCategoryUnsupported": "La categoría no está soportada",
|
||||
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Poner en cola no está habilitado",
|
||||
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, vea {url}. Esto es usualmente RPC2 o [ruta a rTorrent]{url2} cuando se usa rTorrent.",
|
||||
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.",
|
||||
"DownloadClientQbittorrentTorrentStatePathError": "No es posible importar. La ruta coincide con el directorio de descarga base del cliente, ¿es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Diseño de contenido de torrent' NO se haya establecido en 'Original' o 'Crear subcarpeta'?",
|
||||
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar la ordenación de películas",
|
||||
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenación de TV",
|
||||
@@ -733,8 +733,8 @@
|
||||
"DownloadClientValidationSslConnectFailureDetail": "{appName} no se puede conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor, intente configurar tanto {appName} como {clientName} para no usar SSL.",
|
||||
"DownloadFailedEpisodeTooltip": "La descarga del episodio falló",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primero las primeras y últimas",
|
||||
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Tenga en cuenta que Torrents forzados no se atiene a las restricciones de sembrado",
|
||||
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero",
|
||||
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla",
|
||||
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una Carpeta Compartida con el nombre '{sharedFolder}', ¿estás seguro que lo has especificado correctamente?",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas",
|
||||
@@ -825,7 +825,7 @@
|
||||
"Existing": "Existentes",
|
||||
"ExportCustomFormat": "Exportar formato personalizado",
|
||||
"EpisodeFilesLoadError": "No se puede cargar los archivos de episodios",
|
||||
"EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadCliente}",
|
||||
"EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadClient}",
|
||||
"EpisodeInfo": "Información del episodio",
|
||||
"EpisodeMissingAbsoluteNumber": "El episodio no tiene un número de episodio absoluto",
|
||||
"EpisodeTitleRequired": "Título del episodio requerido",
|
||||
@@ -873,7 +873,7 @@
|
||||
"FilterNotInLast": "no en el último",
|
||||
"Group": "Grupo",
|
||||
"ImportListSearchForMissingEpisodes": "Buscar episodios faltantes",
|
||||
"EnableProfileHelpText": "Señalar para habilitar el perfil de lanzamiento",
|
||||
"EnableProfileHelpText": "Marcar para habilitar el perfil de lanzamiento",
|
||||
"EnableRssHelpText": "Se usará cuando {appName} busque periódicamente lanzamientos vía Sincronización RSS",
|
||||
"EndedSeriesDescription": "No se esperan episodios o temporadas adicionales",
|
||||
"EpisodeFileDeleted": "Archivo de episodio eliminado",
|
||||
@@ -1005,8 +1005,8 @@
|
||||
"Forecast": "Previsión",
|
||||
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
|
||||
"IndexerHDBitsSettingsCodecs": "Códecs",
|
||||
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.",
|
||||
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.",
|
||||
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usarán todas las opciones.",
|
||||
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usarán todas las opciones.",
|
||||
"IndexerPriority": "Prioridad del indexador",
|
||||
"IconForFinales": "Icono para Finales",
|
||||
"IgnoreDownload": "Ignorar descarga",
|
||||
@@ -1059,7 +1059,7 @@
|
||||
"ICalTagsSeriesHelpText": "El feed solo contendrá series con al menos una etiqueta coincidente",
|
||||
"IconForCutoffUnmet": "Icono para Umbrales no alcanzados",
|
||||
"IconForCutoffUnmetHelpText": "Mostrar icono para archivos cuando el umbral no haya sido alcanzado",
|
||||
"EpisodeCount": "Número de episodios",
|
||||
"EpisodeCount": "Recuento de episodios",
|
||||
"IndexerSettings": "Ajustes de Indexador",
|
||||
"AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, inténtelo de nuevo.",
|
||||
"IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexers capaces de RSS están temporalmente desactivados debido a errores recientes con el indexer",
|
||||
@@ -1068,23 +1068,23 @@
|
||||
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores",
|
||||
"IndexerSearchNoInteractiveHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Interactiva activada, {appName} no proporcionará ningún resultado de búsquedas interactivas",
|
||||
"PasswordConfirmation": "Confirmación de Contraseña",
|
||||
"IndexerSettingsAdditionalParameters": "Parámetros Adicionales",
|
||||
"IndexerSettingsAdditionalParameters": "Parámetros adicionales",
|
||||
"IndexerSettingsAllowZeroSizeHelpText": "Activar esta opción le permitirá utilizar fuentes que no especifiquen el tamaño del lanzamiento, pero tenga cuidado, no se realizarán comprobaciones relacionadas con el tamaño.",
|
||||
"IndexerSettingsAllowZeroSize": "Permitir Tamaño Cero",
|
||||
"StopSelecting": "Detener la Selección",
|
||||
"IndexerSettingsCookie": "Cookie",
|
||||
"IndexerSettingsCategories": "Categorías",
|
||||
"IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semillas necesario.",
|
||||
"IndexerSettingsSeedRatio": "Proporción de Semillado",
|
||||
"IndexerSettingsSeedRatio": "Ratio de sembrado",
|
||||
"StartupDirectory": "Directorio de Arranque",
|
||||
"IndexerSettingsAdditionalParametersNyaa": "Parámetros Adicionales",
|
||||
"IndexerSettingsPasskey": "Clave de acceso",
|
||||
"IndexerSettingsSeasonPackSeedTime": "Tiempo de Semillado de los Pack de Temporada",
|
||||
"IndexerSettingsAnimeStandardFormatSearch": "Formato Estándar de Búsqueda de Anime",
|
||||
"IndexerSettingsAnimeStandardFormatSearchHelpText": "Buscar también anime utilizando la numeración estándar",
|
||||
"IndexerSettingsApiPathHelpText": "Ruta a la api, normalmente {url}",
|
||||
"IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}",
|
||||
"IndexerSettingsSeasonPackSeedTimeHelpText": "La cantidad de tiempo que un torrent de pack de temporada debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
|
||||
"IndexerSettingsSeedTime": "Tiempo de Semillado",
|
||||
"IndexerSettingsSeedTime": "Tiempo de sembrado",
|
||||
"IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores",
|
||||
"IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.",
|
||||
"NotificationsDiscordSettingsAuthor": "Autor",
|
||||
@@ -1097,12 +1097,12 @@
|
||||
"IndexerSettingsMinimumSeeders": "Semillas mínimas",
|
||||
"IndexerSettingsRssUrl": "URL de RSS",
|
||||
"IndexerSettingsAnimeCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar anime",
|
||||
"IndexerSettingsApiPath": "Ruta de la API",
|
||||
"IndexerSettingsApiPath": "Ruta de API",
|
||||
"IndexerSettingsCookieHelpText": "Si su sitio requiere una cookie de inicio de sesión para acceder al RSS, tendrá que conseguirla a través de un navegador.",
|
||||
"IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}",
|
||||
"IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}",
|
||||
"IndexerHDBitsSettingsMediums": "Medios",
|
||||
"IndexerSettingsSeedTimeHelpText": "La cantidad de tiempo que un torrent debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
|
||||
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vació usa el predeterminado del cliente de descarga",
|
||||
"IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.",
|
||||
"NotificationsEmailSettingsUseEncryption": "Usar Cifrado",
|
||||
"LastDuration": "Última Duración",
|
||||
@@ -1177,7 +1177,7 @@
|
||||
"LogFiles": "Archivos de Registro",
|
||||
"LogLevel": "Nivel de Registro",
|
||||
"LogLevelTraceHelpTextWarning": "El registro de seguimiento sólo debe activarse temporalmente",
|
||||
"LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. ej. 'episodio.s02e15.bluray.mkv'.",
|
||||
"LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. P. ej. `episodio.s02e15.bluray.mkv`",
|
||||
"ListSyncLevelHelpText": "Las series de la biblioteca se gestionarán en función de su selección si se caen o no aparecen en su(s) lista(s)",
|
||||
"LogOnly": "Sólo Registro",
|
||||
"LongDateFormat": "Formato de Fecha Larga",
|
||||
@@ -1335,7 +1335,7 @@
|
||||
"Monitor": "Monitorizar",
|
||||
"MonitorAllEpisodes": "Todos los episodios",
|
||||
"MonitorAllSeasons": "Todas las temporadas",
|
||||
"NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeSet}, asegúrate de que tu script maneja esto correctamente",
|
||||
"NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeTest}, asegúrate de que tu script maneja esto correctamente",
|
||||
"NotificationsDiscordSettingsAvatar": "Avatar",
|
||||
"NotificationsDiscordSettingsAvatarHelpText": "Cambia el avatar que es usado para mensajes desde esta integración",
|
||||
"NotificationsAppriseSettingsNotificationType": "Tipo de notificación de Apprise",
|
||||
@@ -1351,7 +1351,7 @@
|
||||
"NotificationsDiscordSettingsAuthorHelpText": "Sobrescribe el autor incrustado que se muestra para esta notificación. En blanco es el nombre de la instancia",
|
||||
"MonitorNewItems": "Monitorizar nuevos elementos",
|
||||
"MonitoredEpisodesHelpText": "Descargar episodios monitorizados en estas series",
|
||||
"NegateHelpText": "Si se elige, el formato personalizado no se aplica si coincide la condición {implementationName}.",
|
||||
"NegateHelpText": "Si se marca, el formato personalizado no se aplica si coincide la condición {implementationName}.",
|
||||
"NotificationsCustomScriptSettingsName": "Script personalizado",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar la monitorización de temporada",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincroniza la monitorización de temporada de la instancia de {appName}, si se habilita 'Monitorizar' será ignorado",
|
||||
@@ -1361,5 +1361,704 @@
|
||||
"MoreDetails": "Más detalles",
|
||||
"MoreInfo": "Más información",
|
||||
"NoEpisodesInThisSeason": "No hay episodios en esta temporada",
|
||||
"NoLinks": "No hay enlaces"
|
||||
"NoLinks": "No hay enlaces",
|
||||
"OrganizeSelectedSeriesModalAlert": "Consejo: Para previsualizar un renombrado, selecciona \"Cancelar\", entonces selecciona cualquier título de serie y usa este icono:",
|
||||
"OrganizeSelectedSeriesModalConfirmation": "¿Estás seguro que quieres organizar todos los archivos en las {count} series seleccionadas?",
|
||||
"Password": "Contraseña",
|
||||
"Permissions": "Permisos",
|
||||
"Port": "Puerto",
|
||||
"RecyclingBinCleanup": "Limpieza de la papelera de reciclaje",
|
||||
"ReleaseSceneIndicatorSourceMessage": "Los lanzamientos {message} existen con numeración ambigua, no se pudo identificar de forma fiable el episodio.",
|
||||
"SeriesTitle": "Título de serie",
|
||||
"ShowEpisodes": "Mostrar episodios",
|
||||
"ShowBanners": "Mostrar banners",
|
||||
"ShowSeriesTitleHelpText": "Muestra el título de serie bajo el póster",
|
||||
"SkipFreeSpaceCheck": "Saltar comprobación de espacio libre",
|
||||
"OneSeason": "1 temporada",
|
||||
"OnlyTorrent": "Solo torrent",
|
||||
"OpenBrowserOnStart": "Abrir navegador al inicio",
|
||||
"OnlyUsenet": "Solo Usenet",
|
||||
"OverrideAndAddToDownloadQueue": "Sobrescribe y añade a la cola de descarga",
|
||||
"Table": "Tabla",
|
||||
"TagsLoadError": "No se pudo cargar Etiquetas",
|
||||
"OverviewOptions": "Opciones de vista general",
|
||||
"Umask775Description": "{octal} - Usuario y grupo escriben, Otros leen",
|
||||
"PendingChangesStayReview": "Quedarse y revisar cambios",
|
||||
"PendingDownloadClientUnavailable": "Pendiente - El cliente de descarga no está disponible",
|
||||
"PostImportCategory": "Categoría de post-importación",
|
||||
"PreferUsenet": "Preferir usenet",
|
||||
"PreviousAiringDate": "Emisiones anteriores: {date}",
|
||||
"Profiles": "Perfiles",
|
||||
"PrioritySettings": "Prioridad: {priority}",
|
||||
"Ok": "Ok",
|
||||
"PrefixedRange": "Rango prefijado",
|
||||
"Qualities": "Calidades",
|
||||
"PublishedDate": "Fecha de publicación",
|
||||
"QualitySettings": "Opciones de calidad",
|
||||
"QualitySettingsSummary": "Tamaños de calidad y nombrado",
|
||||
"RecentChanges": "Cambios recientes",
|
||||
"MountSeriesHealthCheckMessage": "El montaje que contiene una ruta de series se monta en solo lectura: ",
|
||||
"NotificationsEmailSettingsBccAddress": "Dirección(es) BCC",
|
||||
"NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc",
|
||||
"NotificationsEmailSettingsName": "E-mail",
|
||||
"NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario",
|
||||
"NotificationsEmbySettingsSendNotificationsHelpText": "Hacer que MediaBrowser envíe notificaciones a los proveedores configurados",
|
||||
"NotificationsGotifySettingsAppToken": "Token de app",
|
||||
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje",
|
||||
"NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo",
|
||||
"NotificationsJoinSettingsDeviceNamesHelpText": "Lista separada por coma de nombres de dispositivo completos o parciales a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.",
|
||||
"NotificationsJoinSettingsNotificationPriority": "Prioridad de notificación",
|
||||
"NotificationsNtfySettingsClickUrlHelpText": "Enlace opcional cuando el usuario hace clic en la notificación",
|
||||
"NotificationsNtfySettingsPasswordHelpText": "Contraseña opcional",
|
||||
"NotificationsNtfySettingsTagsEmojis": "Etiquetas y emojis de Ntfy",
|
||||
"NotificationsNtfySettingsServerUrlHelpText": "Deja en blanco para usar el servidor público ({url})",
|
||||
"NotificationsNtfySettingsTopicsHelpText": "Lista de temas a la que enviar notificaciones",
|
||||
"NotificationsPushBulletSettingSenderIdHelpText": "La ID del dispositivo desde la que enviar notificaciones, usa device_iden en la URL del dispositivo en pushbullet.com (deja en blanco para enviarla por ti mismo)",
|
||||
"NotificationsPushBulletSettingsChannelTagsHelpText": "Lista de etiquetas de canal a las que enviar notificaciones",
|
||||
"NotificationsSettingsUpdateMapPathsFromHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
|
||||
"NotificationsSettingsUpdateMapPathsFrom": "Mapear rutas desde",
|
||||
"NotificationsTagsSeriesHelpText": "Envía notificaciones solo para series con al menos una etiqueta coincidente",
|
||||
"NotificationsTraktSettingsRefreshToken": "Refrescar token",
|
||||
"OnEpisodeFileDelete": "Al borrar un archivo de episodio",
|
||||
"OnGrab": "Al capturar",
|
||||
"OneMinute": "1 minuto",
|
||||
"Or": "o",
|
||||
"OrganizeSelectedSeriesModalHeader": "Organizar series seleccionadas",
|
||||
"Original": "Original",
|
||||
"OriginalLanguage": "Idioma original",
|
||||
"OverrideGrabNoLanguage": "Al menos un idioma debe ser seleccionado",
|
||||
"ParseModalHelpTextDetails": "{appName} intentará analizar el título y te mostrará detalles sobre ello",
|
||||
"Parse": "Analizar",
|
||||
"Path": "Ruta",
|
||||
"PortNumber": "Número de puerto",
|
||||
"PosterSize": "Tamaño de póster",
|
||||
"Posters": "Pósteres",
|
||||
"PreviewRename": "Previsualizar renombrado",
|
||||
"PreferredProtocol": "Protocolo preferido",
|
||||
"ProcessingFolders": "Procesando carpetas",
|
||||
"Proper": "Proper",
|
||||
"ProxyFailedToTestHealthCheckMessage": "Fallo al probar el proxy: {url}",
|
||||
"ProxyBadRequestHealthCheckMessage": "Fallo al probar el proxy. Código de estado: {statusCode}",
|
||||
"ProxyType": "Tipo de proxy",
|
||||
"QualityLimitsSeriesRuntimeHelpText": "Los límites son automáticamente ajustados para las series en tiempo de ejecución y el número de episodios en el archivo.",
|
||||
"Range": "Rango",
|
||||
"RecycleBinUnableToWriteHealthCheckMessage": "No se pudo escribir en la carpeta configurada de la papelera de reciclaje: {path}. Asegúrate de que la ruta existe y es modificable por el usuario que ejecuta {appName}",
|
||||
"RecyclingBinHelpText": "Los archivos irán aquí cuando se borren en lugar de ser borrados permanentemente",
|
||||
"RelativePath": "Ruta relativa",
|
||||
"RegularExpressionsCanBeTested": "Las expresiones regulares pueden ser probadas [aquí]({url}).",
|
||||
"ReleaseGroup": "Grupo de lanzamiento",
|
||||
"ReleaseGroups": "Grupos de lanzamiento",
|
||||
"ReleaseProfilesLoadError": "No se pudo cargar los perfiles de lanzamiento",
|
||||
"RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} falló al importar (un) episodio(s). Comprueba tus registros para más detalles.",
|
||||
"RemotePathMappingRemoteDownloadClientHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero este directorio no parece existir. Posiblemente mapeo de ruta remota perdido.",
|
||||
"RemoveQueueItem": "Eliminar - {sourceTitle}",
|
||||
"RemoveFailed": "Fallo al eliminar",
|
||||
"ResetQualityDefinitions": "Restablecer definiciones de calidad",
|
||||
"Scene": "Escena",
|
||||
"RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)",
|
||||
"SceneNumberNotVerified": "El número de escena no ha sido verificado aún",
|
||||
"SearchForAllMissingEpisodes": "Buscar todos los episodios perdidos",
|
||||
"SeasonInformation": "Información de temporada",
|
||||
"SeasonNumber": "Número de temporada",
|
||||
"SeasonCount": "Recuento de temporada",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga",
|
||||
"SelectEpisodes": "Seleccionar episodio(s)",
|
||||
"SeriesDetailsGoTo": "Ir a {title}",
|
||||
"SeriesTypes": "Tipos de serie",
|
||||
"SeriesTypesHelpText": "El tipo de serie es usado para renombrar, analizar y buscar",
|
||||
"SingleEpisodeInvalidFormat": "Episodio individual: Formato inválido",
|
||||
"SslCertPasswordHelpText": "Contraseña para el archivo pfx",
|
||||
"SslPort": "Puerto SSL",
|
||||
"StandardEpisodeFormat": "Formato de episodio estándar",
|
||||
"StartProcessing": "Iniciar procesamiento",
|
||||
"SupportedListsMoreInfo": "Para más información en las listas individuales, haz clic en los botones de más información.",
|
||||
"TagDetails": "Detalles de etiqueta - {label}",
|
||||
"Total": "Total",
|
||||
"True": "Verdadero",
|
||||
"Umask770Description": "{octal} - Usuario y grupo escriben",
|
||||
"UsenetBlackholeNzbFolder": "Carpeta Nzb",
|
||||
"UsenetDelay": "Retraso de usenet",
|
||||
"UsenetBlackhole": "Blackhole de usenet",
|
||||
"RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} reportó archivos en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.",
|
||||
"RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.",
|
||||
"RemotePathMappingFilesWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones de tu cliente de descarga.",
|
||||
"RemoveFilter": "Eliminar filtro",
|
||||
"RemoveQueueItemConfirmation": "¿Estás seguro que quieres eliminar '{sourceTitle}' de la cola?",
|
||||
"RemoveRootFolder": "Eliminar la carpeta raíz",
|
||||
"RemoveSelectedItem": "Eliminar elemento seleccionado",
|
||||
"RemoveTagsAutomaticallyHelpText": "Eliminar etiquetas automáticamente si las condiciones no se cumplen",
|
||||
"RemovedFromTaskQueue": "Eliminar de la cola de tareas",
|
||||
"RemovedSeriesMultipleRemovedHealthCheckMessage": "Las series {series} fueron eliminadas de TheTVDB",
|
||||
"RenameFiles": "Renombrar archivos",
|
||||
"ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?",
|
||||
"ResetDefinitions": "Restablecer definiciones",
|
||||
"ResetDefinitionTitlesHelpText": "Restablecer títulos de definición también como valores",
|
||||
"ResetQualityDefinitionsMessageText": "¿Estás seguro que quieres restablecer las definiciones de calidad?",
|
||||
"RestartNow": "Reiniciar ahora",
|
||||
"RestartRequiredToApplyChanges": "{appName} requiere reiniciar para aplicar cambios. ¿Quieres reiniciar ahora?",
|
||||
"RestartSonarr": "Reiniciar {appName}",
|
||||
"RestoreBackup": "Restaurar copia de seguridad",
|
||||
"Result": "Resultado",
|
||||
"RetryingDownloadOn": "Reintentar descarga en {date} a las {time}",
|
||||
"Rss": "RSS",
|
||||
"SaveChanges": "Guardar cambios",
|
||||
"SceneNumbering": "Numeración de escena",
|
||||
"Script": "Script",
|
||||
"Search": "Buscar",
|
||||
"SearchForMonitoredEpisodesSeason": "Buscar episodios monitorizados en esta temporada",
|
||||
"SearchForQuery": "Buscar {query}",
|
||||
"SeasonFolder": "Carpeta de temporada",
|
||||
"SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} episodios descargados",
|
||||
"SelectDropdown": "Seleccionar...",
|
||||
"SelectLanguageModalTitle": "{modalTitle} - Seleccionar idioma",
|
||||
"SelectLanguages": "Seleccionar idiomas",
|
||||
"SelectReleaseGroup": "Seleccionar grupo de lanzamiento",
|
||||
"SeriesDetailsNoEpisodeFiles": "Sin archivos de episodio",
|
||||
"SeriesFolderImportedTooltip": "Episodio importado de la carpeta de serie",
|
||||
"SeriesIsMonitored": "La serie está monitorizada",
|
||||
"SeriesLoadError": "No se pudo cargar la serie",
|
||||
"SeriesIsUnmonitored": "La serie no está monitorizada",
|
||||
"SetPermissionsLinuxHelpTextWarning": "Si no estás seguro qué configuraciones hacer, no las cambies.",
|
||||
"SetPermissionsLinuxHelpText": "¿Debería ejecutarse chmod cuando los archivos son importados/renombrados?",
|
||||
"SetReleaseGroup": "Establecer grupo de lanzamiento",
|
||||
"ShowEpisodeInformationHelpText": "Muestra el título y número de episodio",
|
||||
"ShowMonitoredHelpText": "Muestra el estado monitorizado bajo el póster",
|
||||
"ShowQualityProfile": "Mostrar perfil de calidad",
|
||||
"ShowQualityProfileHelpText": "Muestra el perfil de calidad bajo el póster",
|
||||
"ShowRelativeDates": "Mostrar fechas relativas",
|
||||
"OnImport": "Al importar",
|
||||
"Other": "Otro",
|
||||
"ShowRelativeDatesHelpText": "Muestra fechas absolutas o relativas (Hoy/Ayer/etc)",
|
||||
"Proxy": "Proxy",
|
||||
"ShowSearch": "Mostrar búsqueda",
|
||||
"ShowSearchHelpText": "Muestra el botón de búsqueda al pasar por encima",
|
||||
"ShowSeasonCount": "Muestra el recuento de temporada",
|
||||
"ShowAdvanced": "Mostrar avanzado",
|
||||
"Socks4": "Socks4",
|
||||
"Socks5": "Socks5 (Soporta TOR)",
|
||||
"ShowTitle": "Mostrar título",
|
||||
"Unknown": "Desconocido",
|
||||
"Sort": "Ordenar",
|
||||
"SourcePath": "Ruta de la fuente",
|
||||
"SourceRelativePath": "Ruta relativa de la fuente",
|
||||
"Special": "Especial",
|
||||
"SourceTitle": "Título de la fuente",
|
||||
"SpecialEpisode": "Episodio especial",
|
||||
"Specials": "Especiales",
|
||||
"SpecialsFolderFormat": "Formato de carpeta de los especiales",
|
||||
"SslCertPassword": "Contraseña de certificado SSL",
|
||||
"SupportedCustomConditions": "{appName} soporta condiciones personalizadas para las siguientes propiedades de lanzamiento.",
|
||||
"SupportedDownloadClients": "{appName} soporta muchos torrent populares y clientes de descarga de usenet.",
|
||||
"SupportedIndexers": "{appName} soporta cualquier indexador que use el estándar Newznab, así como otros indexadores listados a continuación.",
|
||||
"OnSeriesDelete": "Al borrar series",
|
||||
"OnRename": "Al renombrar",
|
||||
"OutputPath": "Ruta de salida",
|
||||
"PreferAndUpgrade": "Preferir y actualizar",
|
||||
"Presets": "Preajustes",
|
||||
"ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.",
|
||||
"QueueLoadError": "Fallo al cargar la cola",
|
||||
"ReadTheWikiForMoreInformation": "Lee la Wiki para más información",
|
||||
"RecyclingBinCleanupHelpText": "Establece a 0 para deshabilitar la limpieza automática",
|
||||
"RegularExpressionsTutorialLink": "Más detalles de las expresiones regulares pueden ser encontradas [aquí]({url}).",
|
||||
"RejectionCount": "Recuento de rechazos",
|
||||
"RemotePathMappingFileRemovedHealthCheckMessage": "El fichero {path} ha sido eliminado durante el proceso.",
|
||||
"RemotePathMappings": "Mapeos de ruta remota",
|
||||
"RemovedSeriesSingleRemovedHealthCheckMessage": "La serie {series} fue eliminada de TheTVDB",
|
||||
"ReplaceWithDash": "Reemplazar con guion",
|
||||
"ReplaceWithSpaceDash": "Reemplazar por barra espaciadora",
|
||||
"ReplaceWithSpaceDashSpace": "Reemplazar por espacio en la barra espaciadora",
|
||||
"ScriptPath": "Ruta del script",
|
||||
"SeasonDetails": "Detalles de temporada",
|
||||
"SecretToken": "Token secreto",
|
||||
"SelectQuality": "Seleccionar calidad",
|
||||
"SelectLanguage": "Seleccionar idioma",
|
||||
"SelectSeason": "Seleccionar temporada",
|
||||
"SeriesIndexFooterMissingUnmonitored": "Episodios perdidos (Serie no monitorizada)",
|
||||
"ShowEpisodeInformation": "Mostrar información de episodio",
|
||||
"ShowPath": "Mostrar ruta",
|
||||
"ShowNetwork": "Mostrar red",
|
||||
"TvdbIdExcludeHelpText": "La ID de TVDB de la serie a excluir",
|
||||
"UpdateSonarrDirectlyLoadError": "No se pudo actualizar {appName} directamente,",
|
||||
"UpgradesAllowedHelpText": "Si se deshabilita las calidades no serán actualizadas",
|
||||
"WithFiles": "Con archivos",
|
||||
"SystemTimeHealthCheckMessage": "La hora del sistema está desfasada más de 1 día. Las tareas programadas pueden no ejecutarse correctamente hasta que la hora sea corregida",
|
||||
"TableColumns": "Columnas",
|
||||
"TableColumnsHelpText": "Elige qué columnas son visibles en qué orden aparecen",
|
||||
"TablePageSize": "Tamaño de página",
|
||||
"TablePageSizeHelpText": "Número de elementos a mostrar en cada página",
|
||||
"TablePageSizeMinimum": "El tamaño de página debe ser al menos {minimumValue}",
|
||||
"TablePageSizeMaximum": "El tamaño de página no debe exceder {maximumValue}",
|
||||
"TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada",
|
||||
"TagsSettingsSummary": "Vea todas las etiquetas y cómo se usan. Las etiquetas sin usar pueden ser eliminadas",
|
||||
"TaskUserAgentTooltip": "User-Agent proporcionado por la aplicación que llamó a la API",
|
||||
"Test": "Prueba",
|
||||
"TestAllIndexers": "Probar todos los indexadores",
|
||||
"TestAllLists": "Probar todas las listas",
|
||||
"TestParsing": "Probar análisis",
|
||||
"ThemeHelpText": "Cambiar el tema de la interfaz de la aplicación, el tema 'Auto' usará el tema de tu sistema para establecer el modo luminoso u oscuro. Inspirado por Theme.Park",
|
||||
"TimeLeft": "Tiempo restante",
|
||||
"ToggleMonitoredSeriesUnmonitored ": "No se puede conmutar el estado monitorizado cuando la serie no está monitorizada",
|
||||
"Tomorrow": "Mañana",
|
||||
"TorrentBlackhole": "Blackhole de torrent",
|
||||
"TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet",
|
||||
"TorrentBlackholeSaveMagnetFilesExtension": "Guardar extensión de archivos magnet",
|
||||
"TorrentBlackholeSaveMagnetFilesReadOnly": "Solo lectura",
|
||||
"TorrentBlackholeTorrentFolder": "Carpeta de torrent",
|
||||
"TorrentDelayHelpText": "Retraso en minutos a esperar antes de capturar un torrent",
|
||||
"TorrentDelayTime": "Retraso torrent: {torrentDelay}",
|
||||
"Umask755Description": "{octal} - Usuario escribe, Todos los demás leen",
|
||||
"Umask777Description": "{octal} - Todos escriben",
|
||||
"UnableToLoadAutoTagging": "No se pudo cargar el etiquetado automático",
|
||||
"UnableToLoadBackups": "No se pudo cargar las copias de seguridad",
|
||||
"Ungroup": "Sin agrupar",
|
||||
"UnknownDownloadState": "Estado de descarga desconocido: {state}",
|
||||
"Unlimited": "Ilimitado",
|
||||
"UnmappedFilesOnly": "Solo archivos sin mapear",
|
||||
"UnmonitorDeletedEpisodes": "Dejar de monitorizar episodios borrados",
|
||||
"UnmonitoredOnly": "Solo sin monitorizar",
|
||||
"UnsavedChanges": "Cambios sin guardar",
|
||||
"UnselectAll": "Desmarcar todo",
|
||||
"Upcoming": "Próximamente",
|
||||
"UpcomingSeriesDescription": "Series que han sido anunciadas pero aún no hay fecha de emisión exacta",
|
||||
"ReleaseSceneIndicatorUnknownSeries": "Episodio o serie desconocido.",
|
||||
"RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.",
|
||||
"RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto",
|
||||
"SelectFolder": "Seleccionar carpeta",
|
||||
"TestAllClients": "Probar todos los clientes",
|
||||
"UpdateFiltered": "Actualizar filtrados",
|
||||
"SeriesEditor": "Editor de serie",
|
||||
"Updates": "Actualizaciones",
|
||||
"NotificationsKodiSettingsDisplayTimeHelpText": "Durante cuánto tiempo serán mostradas las notificaciones (en segundos)",
|
||||
"NotificationsNtfySettingsUsernameHelpText": "Usuario opcional",
|
||||
"NotificationsSimplepushSettingsEvent": "Evento",
|
||||
"NotificationsSimplepushSettingsEventHelpText": "Personaliza el comportamiento de las notificaciones push",
|
||||
"NotificationsTwitterSettingsConsumerSecret": "Secreto de consumidor",
|
||||
"NotificationsTelegramSettingsSendSilently": "Enviar de forma silenciosa",
|
||||
"NotificationsValidationInvalidHttpCredentials": "Credenciales de autenticación HTTP inválidas: {exceptionMessage}",
|
||||
"OnEpisodeFileDeleteForUpgrade": "Al borrar un archivo de episodio para actualización",
|
||||
"OnHealthIssue": "Al haber un problema de salud",
|
||||
"Organize": "Organizar",
|
||||
"OrganizeRenamingDisabled": "El renombrado está deshabilitado, nada que renombrar",
|
||||
"OrganizeNothingToRename": "¡Éxito! Mi trabajo está hecho, no hay archivos que renombrar.",
|
||||
"OrganizeRelativePaths": "Todas las rutas son relativas a: `{path}`",
|
||||
"Pending": "Pendiente",
|
||||
"QualityDefinitions": "Definiciones de calidad",
|
||||
"RecyclingBin": "Papelera de reciclaje",
|
||||
"ReleaseTitle": "Título de lanzamiento",
|
||||
"RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente",
|
||||
"Remove": "Eliminar",
|
||||
"RetentionHelpText": "Solo usenet: Establece a cero para establecer una retención ilimitada",
|
||||
"SelectIndexerFlags": "Seleccionar banderas del indexador",
|
||||
"SelectSeasonModalTitle": "{modalTitle} - Seleccionar temporada",
|
||||
"SeriesFinale": "Final de serie",
|
||||
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}).",
|
||||
"SetIndexerFlags": "Establecer banderas del indexador",
|
||||
"SkipRedownload": "Saltar redescarga",
|
||||
"ShowMonitored": "Mostrar monitorizado",
|
||||
"Space": "Espacio",
|
||||
"TimeFormat": "Formato de hora",
|
||||
"UiSettings": "Opciones de interfaz",
|
||||
"Umask": "UMask",
|
||||
"UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' no es modificable por el usuario '{userName}'.",
|
||||
"UsenetDelayHelpText": "Retraso en minutos a esperar antes de capturar un lanzamiento desde usenet",
|
||||
"PartialSeason": "Temporada parcial",
|
||||
"RemoveSelectedItemQueueMessageText": "¿Estás seguro que quieres eliminar 1 elemento de la cola?",
|
||||
"SceneInformation": "Información de escena",
|
||||
"UpgradeUntilThisQualityIsMetOrExceeded": "Actualizar hasta que esta calidad sea alcanzada o excedida",
|
||||
"Uppercase": "Mayúsculas",
|
||||
"SeriesDetailsRuntime": "{runtime} minutos",
|
||||
"ShowBannersHelpText": "Muestra banners en lugar de títulos",
|
||||
"SslCertPathHelpText": "Ruta al archivo pfx",
|
||||
"Umask750Description": "{octal} - Usuario escribe, Grupo lee",
|
||||
"UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío",
|
||||
"UpdateAll": "Actualizar todo",
|
||||
"ConnectionSettingsUrlBaseHelpText": "Añade un prefijo a la url {connectionName}, como {url}",
|
||||
"UsenetDelayTime": "Retraso de usenet: {usenetDelay}",
|
||||
"UsenetDisabled": "Usenet deshabilitado",
|
||||
"Username": "Usuario",
|
||||
"UtcAirDate": "Fecha de emisión UTC",
|
||||
"Version": "Versión",
|
||||
"WaitingToImport": "Esperar para importar",
|
||||
"NotificationsDiscordSettingsOnGrabFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al capturar'",
|
||||
"NotificationsNtfyValidationAuthorizationRequired": "Se requiere autorización",
|
||||
"NotificationsNtfySettingsClickUrl": "URL al hacer clic",
|
||||
"NotificationsNotifiarrSettingsApiKeyHelpText": "Tu clave API de tu perfil",
|
||||
"NotificationsPlexSettingsAuthenticateWithPlexTv": "Autenticar con Plex.tv",
|
||||
"NotificationsPushcutSettingsNotificationNameHelpText": "Nombre de notificación de la pestaña Notificaciones de la aplicación Pushcut",
|
||||
"NotificationsPlexValidationNoTvLibraryFound": "Al menos se requiere una biblioteca de TV",
|
||||
"NotificationsPushBulletSettingSenderId": "ID del remitente",
|
||||
"NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID de grupo / Número de teléfono del receptor",
|
||||
"NotificationsSettingsWebhookMethod": "Método",
|
||||
"NotificationsSettingsUseSslHelpText": "Conectar a {serviceName} sobre HTTPS en vez de HTTP",
|
||||
"NotificationsTwitterSettingsConsumerSecretHelpText": "Secreto de consumidor de una aplicación de Twitter",
|
||||
"NotificationsValidationInvalidUsernamePassword": "Usuario o contraseña inválido",
|
||||
"NotificationsSlackSettingsWebhookUrlHelpText": "URL de canal webhook de Slack",
|
||||
"PackageVersion": "Versión del paquete",
|
||||
"NotificationsValidationUnableToConnectToApi": "No se pudo conectar a la API de {service}. La conexión al servidor falló: ({responseCode}) {exceptionMessage}",
|
||||
"PosterOptions": "Opciones de póster",
|
||||
"PreferTorrent": "Preferir torrent",
|
||||
"PreviewRenameSeason": "Previsualizar renombrado para esta temporada",
|
||||
"PreviousAiring": "Emisiones anteriores",
|
||||
"RemoveFromDownloadClient": "Eliminar del cliente de descarga",
|
||||
"RemovingTag": "Eliminando etiqueta",
|
||||
"Required": "Solicitado",
|
||||
"Reorder": "Reordenar",
|
||||
"SceneInfo": "Información de escena",
|
||||
"RootFolderMissingHealthCheckMessage": "Carpeta raíz perdida: {rootFolderPath}",
|
||||
"SearchAll": "Buscar todo",
|
||||
"SelectAll": "Seleccionar todo",
|
||||
"SeriesIndexFooterEnded": "FInalizado (Todos los episodios descargados)",
|
||||
"ShowDateAdded": "Mostrar fecha de adición",
|
||||
"UnmonitorDeletedEpisodesHelpText": "Los episodios borrados del disco son dejados de monitorizar automáticamente en {appName}",
|
||||
"UnmonitorSelected": "Dejar de monitorizar seleccionados",
|
||||
"UpdateSelected": "Actualizar seleccionados",
|
||||
"UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz '{uiFolder}' no es modificable por el usuario '{userName}'.",
|
||||
"UpgradeUntil": "Actualizar hasta",
|
||||
"UpdaterLogFiles": "Actualizador de archivos de registro",
|
||||
"UseSeasonFolder": "Usar carpeta de temporada",
|
||||
"UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar",
|
||||
"View": "Vista",
|
||||
"VisitTheWikiForMoreDetails": "Visita la wiki para más detalles: ",
|
||||
"WaitingToProcess": "Esperar al proceso",
|
||||
"Week": "Semana",
|
||||
"WeekColumnHeader": "Cabecera de columna de semana",
|
||||
"Release": "Lanzamiento",
|
||||
"RemoveSelectedItems": "Eliminar elementos seleccionados",
|
||||
"RemoveSelectedItemsQueueMessageText": "¿Estás seguro que quieres eliminar {selectedCount} elementos de la cola?",
|
||||
"RootFolderSelectFreeSpace": "{freeSpace} libres",
|
||||
"RootFolderPath": "Ruta de carpeta raíz",
|
||||
"RssSyncInterval": "Intervalo de sincronización RSS",
|
||||
"SingleEpisode": "Episodio individual",
|
||||
"ShowUnknownSeriesItems": "Mostrar elementos de serie desconocidos",
|
||||
"NotificationsGotifySettingIncludeSeriesPoster": "Incluir poster de serie",
|
||||
"NotificationsKodiSettingsCleanLibraryHelpText": "Limpia la biblioteca después de actualizar",
|
||||
"NotificationsKodiSettingsCleanLibrary": "Limpiar biblioteca",
|
||||
"NotificationsKodiSettingsGuiNotification": "Notificación de interfaz gráfica",
|
||||
"NotificationsKodiSettingsUpdateLibraryHelpText": "¿Actualiza la biblioteca durante Importar y renombrar?",
|
||||
"NotificationsMailgunSettingsUseEuEndpointHelpText": "Habilitar el uso del endpoint de UE de MailGun",
|
||||
"NotificationsMailgunSettingsUseEuEndpoint": "Usar el endpoint de la UE",
|
||||
"NotificationsNtfySettingsAccessToken": "Token de acceso",
|
||||
"NotificationsNtfySettingsAccessTokenHelpText": "Autorización opcional basada en token. Tiene prioridad sobre usuario/contraseña",
|
||||
"NotificationsNtfySettingsTagsEmojisHelpText": "Lista opcional de etiquetas o emojis para usar",
|
||||
"NotificationsNtfySettingsTopics": "Temas",
|
||||
"NotificationsPushBulletSettingsDeviceIds": "IDs de dispositivo",
|
||||
"NotificationsPushBulletSettingsAccessToken": "Token de acceso",
|
||||
"NotificationsPushBulletSettingsChannelTags": "Etiquetas de canal",
|
||||
"NotificationsPushcutSettingsTimeSensitive": "Sensible al tiempo",
|
||||
"NotificationsPushcutSettingsTimeSensitiveHelpText": "Habilitar para marcas la notificación como \"Sensible al tiempo\"",
|
||||
"NotificationsPushoverSettingsDevices": "Dispositivos",
|
||||
"NotificationsPushoverSettingsDevicesHelpText": "Lista de nombres de dispositivo (deja en blanco para enviar a todos los dispositivos)",
|
||||
"NotificationsPushoverSettingsExpireHelpText": "Tiempo máximo para reintentar las alertas de emergencia, máximo 86400 segundos",
|
||||
"NotificationsPushoverSettingsRetry": "Reintentar",
|
||||
"NotificationsPushoverSettingsSound": "Sonido",
|
||||
"NotificationsPushoverSettingsUserKey": "Clave de usuario",
|
||||
"NotificationsPushoverSettingsSoundHelpText": "Sonido de notificación, deja en blanco para usar el predeterminado",
|
||||
"NotificationsSettingsWebhookMethodHelpText": "Qué método HTTP utilizar para enviar al servicio web",
|
||||
"NotificationsSettingsWebhookUrl": "URL del webhook",
|
||||
"NotificationsSignalSettingsGroupIdPhoneNumber": "ID de grupo / Número de teléfono",
|
||||
"NotificationsSignalSettingsPasswordHelpText": "Contraseña usada para autenticar solicitudes hacia signal-api",
|
||||
"NotificationsSignalSettingsSenderNumber": "Número del emisor",
|
||||
"NotificationsSignalSettingsUsernameHelpText": "Usuario usado para autenticar solicitudes hacia signal-api",
|
||||
"NotificationsSignalValidationSslRequired": "Se requiere SSL",
|
||||
"NotificationsSimplepushSettingsKey": "Clave",
|
||||
"NotificationsSlackSettingsChannel": "Canal",
|
||||
"NotificationsSlackSettingsIconHelpText": "Cambia el icono usado para mensajes publicados a Slack (emoji o URL)",
|
||||
"NotificationsSlackSettingsUsernameHelpText": "Usuario para publicar a Slack",
|
||||
"NotificationsTelegramSettingsSendSilentlyHelpText": "Envía el mensaje de forma silenciosa. Los usuarios recibirán una notificación sin sonido",
|
||||
"NotificationsTelegramSettingsTopicId": "ID de tema",
|
||||
"NotificationsTraktSettingsAuthenticateWithTrakt": "Autenticar con Trakt",
|
||||
"NotificationsTraktSettingsExpires": "Caduca",
|
||||
"NotificationsValidationUnableToConnectToService": "No se pudo conectar a {serviceName}",
|
||||
"NotificationsValidationUnableToSendTestMessage": "No se pudo enviar un mensaje de prueba: {exceptionMessage}",
|
||||
"NzbgetHistoryItemMessage": "Estado de PAR: {parStatus} - Estado de desempaquetado: {unpackStatus} - Estado de movido: {moveStatus} - Estado de script: {scriptStatus} - Estado de borrado: {deleteStatus} - Estado de marcado: {markStatus}",
|
||||
"OpenSeries": "Abrir serie",
|
||||
"OrganizeLoadError": "Error cargando vistas previas",
|
||||
"OrganizeNamingPattern": "Patrón de nombrado: `{episodeFormat}`",
|
||||
"OverrideGrabNoSeries": "La serie debe ser seleccionada",
|
||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||
"PendingChangesDiscardChanges": "Descartar cambios y salir",
|
||||
"Period": "Periodo",
|
||||
"PendingChangesMessage": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir de esta página?",
|
||||
"PreviouslyInstalled": "Previamente instalado",
|
||||
"ProtocolHelpText": "Elige qué protocolo(s) usar y cuál se prefiere cuando se elige entre lanzamientos equivalentes",
|
||||
"ProgressBarProgress": "Barra de progreso al {progress}%",
|
||||
"ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios",
|
||||
"ProxyResolveIpHealthCheckMessage": "Fallo al resolver la dirección IP para el host proxy configurado {proxyHostName}",
|
||||
"ProxyUsernameHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.",
|
||||
"QualityProfile": "Perfil de calidad",
|
||||
"QualityDefinitionsLoadError": "No se pudo cargar las definiciones de calidad",
|
||||
"QualityProfiles": "Perfiles de calidad",
|
||||
"QualityProfilesLoadError": "No se pudo cargar los perfiles de calidad",
|
||||
"QueueFilterHasNoItems": "Seleccionado filtro de cola que no tiene elementos",
|
||||
"QuickSearch": "Búsqueda rápida",
|
||||
"Real": "Real",
|
||||
"Reason": "Razón",
|
||||
"RegularExpression": "Expresión regular",
|
||||
"ReleaseHash": "Hash de lanzamiento",
|
||||
"Rejections": "Rechazos",
|
||||
"RecyclingBinCleanupHelpTextWarning": "Los archivos en la papelera de reciclaje anteriores al número de días seleccionado serán limpiados automáticamente",
|
||||
"ReleaseProfiles": "Perfiles de lanzamiento",
|
||||
"ReleaseRejected": "Lanzamiento rechazado",
|
||||
"ReleaseSceneIndicatorAssumingScene": "Asumiendo numeración de escena.",
|
||||
"ReleaseSceneIndicatorAssumingTvdb": "Asumiendo numeración de TVDB.",
|
||||
"ReleaseSceneIndicatorUnknownMessage": "La numeración varía para este episodio y el lanzamiento no coincide con ningún mapeo conocido.",
|
||||
"RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} puede ver pero no acceder al episodio descargado {path}. Probablemente error de permisos.",
|
||||
"RemotePathMappingRemotePathHelpText": "Ruta raíz al directorio al que accede el cliente de descarga",
|
||||
"RemoveFailedDownloads": "Eliminar descargas fallidas",
|
||||
"RemoveSelected": "Eliminar seleccionado",
|
||||
"RenameEpisodesHelpText": "{appName} usará el nombre de archivo existente si el renombrado está deshabilitado",
|
||||
"RenameEpisodes": "Renombrar episodios",
|
||||
"RestrictionsLoadError": "No se pudo cargar Restricciones",
|
||||
"SearchForMissing": "Buscar perdidos",
|
||||
"SeasonFinale": "Final de temporada",
|
||||
"SearchSelected": "Buscar seleccionados",
|
||||
"SeasonFolderFormat": "Formato de carpeta de temporada",
|
||||
"SendAnonymousUsageData": "Enviar datos de uso anónimos",
|
||||
"SeriesDetailsOneEpisodeFile": "1 archivo de episodio",
|
||||
"SeriesFolderFormatHelpText": "Usado cuando se añade una nueva serie o se mueve la serie a través del editor de serie",
|
||||
"SeriesID": "ID de serie",
|
||||
"SetPermissions": "Establecer permisos",
|
||||
"SetReleaseGroupModalTitle": "{modalTitle} - Establecer grupo de lanzamiento",
|
||||
"SetTags": "Establecer etiquetas",
|
||||
"ShowPreviousAiring": "Mostrar emisión anterior",
|
||||
"ShowSizeOnDisk": "Mostrar tamaño en disco",
|
||||
"SizeOnDisk": "Tamaño en disco",
|
||||
"SizeLimit": "Límite de tamaño",
|
||||
"SkipRedownloadHelpText": "Evita que {appName} intente descargar un lanzamiento alternativo para este elemento",
|
||||
"Small": "Pequeño",
|
||||
"SomeResultsAreHiddenByTheAppliedFilter": "Algunos resultados están ocultos por el filtro aplicado",
|
||||
"SonarrTags": "Etiquetas de {appName}",
|
||||
"Standard": "Estándar",
|
||||
"StandardEpisodeTypeFormat": "Temporada y número de episodios ({format})",
|
||||
"StandardEpisodeTypeDescription": "Episodios lanzados con patrón SxxEyy",
|
||||
"SubtitleLanguages": "Idiomas de subtítulo",
|
||||
"SupportedAutoTaggingProperties": "{appName} soporta las siguientes propiedades para reglas de etiquetado automáticas",
|
||||
"SupportedIndexersMoreInfo": "Para más información en los indexadores individuales, haz clic en los botones de más información.",
|
||||
"SupportedListsSeries": "{appName} soporta múltiples listas para importar series en la base de datos.",
|
||||
"TableOptions": "Opciones de tabla",
|
||||
"TableOptionsButton": "Botón de opciones de tabla",
|
||||
"Today": "Hoy",
|
||||
"Titles": "Títulos",
|
||||
"ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar",
|
||||
"TotalFileSize": "Tamaño total de archivo",
|
||||
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización",
|
||||
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
|
||||
"UrlBase": "URL base",
|
||||
"UseSsl": "Usar SSL",
|
||||
"Usenet": "Usenet",
|
||||
"VersionNumber": "Versión {version}",
|
||||
"OnManualInteractionRequired": "Cuando se requiera interacción manual",
|
||||
"OnLatestVersion": "La última versión de {appName} ya está instalada",
|
||||
"OnUpgrade": "Al actualizar",
|
||||
"RootFolders": "Carpetas raíz",
|
||||
"SeasonPremiere": "Estreno de temporada",
|
||||
"UnableToUpdateSonarrDirectly": "No se pudo actualizar {appName} directamente,",
|
||||
"UnmappedFolders": "Carpetas sin mapear",
|
||||
"QualitiesLoadError": "No se pudo cargar las calidades",
|
||||
"SeasonNumberToken": "Temporada {seasonNumber}",
|
||||
"PreferredSize": "Tamaño preferido",
|
||||
"TypeOfList": "Lista {typeOfList}",
|
||||
"UiSettingsLoadError": "No se pudo cargar las opciones de interfaz",
|
||||
"UpdateMonitoring": "Actualizar monitorizando",
|
||||
"ReleaseType": "Tipo de lanzamiento",
|
||||
"RemotePathMappingLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.",
|
||||
"RemotePathMappingWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones del cliente de descarga.",
|
||||
"RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas desde el historial del cliente de descarga",
|
||||
"RemoveFromQueue": "Eliminar de la cola",
|
||||
"RemoveMultipleFromDownloadClientHint": "Elimina descargas y archivos del cliente de descarga",
|
||||
"RemoveQueueItemsRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará las descargas y los archivos del cliente de descarga.",
|
||||
"RemoveTagsAutomatically": "Eliminar etiquetas automáticamente",
|
||||
"ReplaceIllegalCharactersHelpText": "Reemplaza los caracteres ilegales. Si no está marcado, {appName} los eliminará en su lugar",
|
||||
"ResetAPIKey": "Restablecer clave API",
|
||||
"RootFolder": "Carpeta raíz",
|
||||
"RootFolderMultipleMissingHealthCheckMessage": "Múltiples carpetas raíz están perdidas: {rootFolderPaths}",
|
||||
"RestartReloadNote": "Nota: {appName} se reiniciará automáticamente y recargará la interfaz durante el proceso de restauración.",
|
||||
"RestartRequiredWindowsService": "Dependiendo de qué usuario esté ejecutando el servicio {appName}, puede ser necesario reiniciar {appName} como administrador antes de que el servicio se inicie automáticamente.",
|
||||
"SeasonPremieresOnly": "Solo estrenos de temporada",
|
||||
"SeasonPassTruncated": "Solo se muestran las últimas 25 temporadas, ve a detalles para ver todas las temporadas",
|
||||
"SelectFolderModalTitle": "{modalTitle} - Seleccionar carpeta",
|
||||
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio",
|
||||
"SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)",
|
||||
"SetIndexerFlagsModalTitle": "{modalTitle} - Establecer banderas del indexador",
|
||||
"ShortDateFormat": "Formato de fecha breve",
|
||||
"ShowUnknownSeriesItemsHelpText": "Muestra elementos sin una serie en la cola, esto incluiría series eliminadas, películas o cualquier cosa más en la categoría de {appName}",
|
||||
"ShownClickToHide": "Mostrado, haz clic para ocultar",
|
||||
"SkipFreeSpaceCheckWhenImportingHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz durante la importación de archivo",
|
||||
"SmartReplace": "Reemplazo inteligente",
|
||||
"SupportedDownloadClientsMoreInfo": "Para más información en los clientes de descarga individuales, haz clic en los botones de más información.",
|
||||
"SupportedImportListsMoreInfo": "Para más información de los listas de importación individuales, haz clic en los botones de más información.",
|
||||
"TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "En lugar de mover archivos esto indicará a {appName} que copie o enlace (dependiendo de los ajustes/configuración del sistema)",
|
||||
"TorrentDelay": "Retraso de torrent",
|
||||
"ToggleMonitoredToUnmonitored": "Monitorizado, haz clic para dejar de monitorizar",
|
||||
"TorrentBlackholeSaveMagnetFilesHelpText": "Guarda el enlace magnet si no hay ningún archivo .torrent disponible (útil solo si el cliente de descarga soporta magnets guardados en un archivo)",
|
||||
"UiLanguage": "Idioma de interfaz",
|
||||
"UiLanguageHelpText": "Idioma que {appName} usará en la interfaz",
|
||||
"UiSettingsSummary": "Opciones de calendario, fecha y color alterado",
|
||||
"UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones",
|
||||
"TotalRecords": "Total de registros: {totalRecords}",
|
||||
"WantMoreControlAddACustomFormat": "¿Quieres más control sobre qué descargas son preferidas? Añade un [formato personalizado](/opciones/formatospersonalizados)",
|
||||
"OrganizeModalHeader": "Organizar y renombrar",
|
||||
"RemoveCompleted": "Eliminar completado",
|
||||
"OpenBrowserOnStartHelpText": " Abre un navegador web y navega a la página de inicio de {appName} al iniciar la aplicación.",
|
||||
"SslCertPath": "Ruta del certificado SSL",
|
||||
"StartImport": "Iniciar importación",
|
||||
"OptionalName": "Nombre opcional",
|
||||
"RemotePath": "Ruta remota",
|
||||
"SeriesPremiere": "Estreno de serie",
|
||||
"SeriesMatchType": "Tipo de emparejamiento de series",
|
||||
"SeriesMonitoring": "Monitorización de serie",
|
||||
"Tba": "TBA",
|
||||
"TorrentsDisabled": "Torrents deshabilitados",
|
||||
"RemotePathMappingGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.",
|
||||
"ReplaceIllegalCharacters": "Reemplazar caracteres ilegales",
|
||||
"ResetTitles": "Restablecer títulos",
|
||||
"SmartReplaceHint": "Raya o barra espaciadora según el nombre",
|
||||
"SelectEpisodesModalTitle": "{modalTitle} - Seleccionar episodio(s)",
|
||||
"DownloadClientDelugeSettingsDirectory": "Directorio de descarga",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Deluge",
|
||||
"UnmonitorSpecialsEpisodesDescription": "Dejar de monitorizar todos los episodios especiales sin cambiar el estado monitorizado de otros episodios",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicación opcional a la que mover las descargas completadas, dejar en blanco para usar la ubicación predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directorio al que mover cuando se complete",
|
||||
"NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord",
|
||||
"NotificationsEmailSettingsCcAddress": "Dirección(es) CC",
|
||||
"NotificationsEmbySettingsSendNotifications": "Enviar notificaciones",
|
||||
"NotificationsEmbySettingsUpdateLibraryHelpText": "¿Actualiza biblioteca en importar, renombrar o borrar?",
|
||||
"NotificationsJoinSettingsDeviceIdsHelpText": "En desuso, usar Nombres de dispositivo en su lugar. Lista separada por coma de los IDs de dispositivo a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.",
|
||||
"NotificationsPushoverSettingsExpire": "Caduca",
|
||||
"NotificationsMailgunSettingsSenderDomain": "Dominio del remitente",
|
||||
"NotificationsNtfySettingsServerUrl": "URL del servidor",
|
||||
"PreferProtocol": "Preferir {preferredProtocol}",
|
||||
"ProfilesSettingsSummary": "Perfiles de calidad, de retraso de idioma y de lanzamiento",
|
||||
"QualitiesHelpText": "Calidades superiores en la lista son más preferibles. Calidades dentro del mismo grupo son iguales. Comprobar solo calidades que se busquen",
|
||||
"RssIsNotSupportedWithThisIndexer": "RSS no está soportado con este indexador",
|
||||
"Repack": "Reempaquetar",
|
||||
"NotificationsGotifySettingsPriorityHelpText": "Prioridad de la notificación",
|
||||
"NotificationsGotifySettingsServer": "Servidor Gotify",
|
||||
"NotificationsPlexSettingsAuthToken": "Token de autenticación",
|
||||
"NotificationsSynologySettingsUpdateLibraryHelpText": "Llamada synoindex en localhost para actualizar un archivo de biblioteca",
|
||||
"Overview": "Vista general",
|
||||
"UseSeasonFolderHelpText": "Ordenar episodios en carpetas de temporada",
|
||||
"RemotePathMappingDockerFolderMissingHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir dentro del contenedor. Revisa tus mapeos de ruta remotos y opciones de volumen del contenedor.",
|
||||
"Retention": "Retención",
|
||||
"NotificationsDiscordSettingsOnManualInteractionFields": "Campos durante la interacción manual",
|
||||
"NotificationsDiscordSettingsOnGrabFields": "Campos al capturar",
|
||||
"NotificationsDiscordSettingsOnImportFields": "Campos al importar",
|
||||
"NotificationsDiscordSettingsOnImportFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al importar'",
|
||||
"NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'durante la interacción manual'",
|
||||
"NotificationsEmailSettingsCcAddressHelpText": "Lista separada por coma de destinatarios de e-mail cc",
|
||||
"NotificationsEmailSettingsFromAddress": "De dirección",
|
||||
"NotificationsKodiSettingAlwaysUpdateHelpText": "¿Actualiza la biblioteca incluso cuando un video se esté reproduciendo?",
|
||||
"NotificationsKodiSettingsDisplayTime": "Tiempo de visualización",
|
||||
"NotificationsLoadError": "No se pudo cargar las notificaciones",
|
||||
"NotificationsMailgunSettingsApiKeyHelpText": "La clave API generada desde MailGun",
|
||||
"NotificationsSendGridSettingsApiKeyHelpText": "La clave API generada por SendGrid",
|
||||
"NotificationsTwitterSettingsConsumerKeyHelpText": "Clave de consumidor de una aplicación de Twitter",
|
||||
"NotificationsTwitterSettingsDirectMessage": "Mensaje directo",
|
||||
"NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público",
|
||||
"OnApplicationUpdate": "Al actualizar la aplicación",
|
||||
"OnSeriesAdd": "Al añadir series",
|
||||
"OnlyForBulkSeasonReleases": "Solo para lanzamientos de temporada a granel",
|
||||
"OrganizeModalHeaderSeason": "Organizar y renombrar - {season}",
|
||||
"OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado",
|
||||
"OverrideGrabNoQuality": "La calidad debe ser seleccionada",
|
||||
"NotificationsValidationInvalidAuthenticationToken": "Token de autenticación inválido",
|
||||
"NotificationsValidationUnableToConnect": "No se pudo conectar: {exceptionMessage}",
|
||||
"NotificationsValidationUnableToSendTestMessageApiResponse": "No se pudo enviar un mensaje de prueba. Respuesta de la API: {error}",
|
||||
"OverrideGrabModalTitle": "Sobrescribe y captura - {title}",
|
||||
"ReleaseProfileTagSeriesHelpText": "Los perfiles de lanzamientos se aplicarán a series con al menos una etiqueta coincidente. Deja en blanco para aplicar a todas las series",
|
||||
"ReleaseSceneIndicatorMappedNotRequested": "El episodio mapeado no fue solicitado en esta búsqueda.",
|
||||
"RemotePathMappingBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.",
|
||||
"RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} puede ver pero no acceder al directorio de descarga {downloadPath}. Probablemente error de permisos.",
|
||||
"RemotePathMappingHostHelpText": "El mismo host que especificaste para el cliente de descarga remoto",
|
||||
"ParseModalUnableToParse": "No se pudo analizar el título proporcionado, por favor inténtalo de nuevo.",
|
||||
"Preferred": "Preferido",
|
||||
"Priority": "Prioridad",
|
||||
"QualityProfileInUseSeriesListCollection": "No se puede borrar un perfil de calidad que está asignado a una serie, lista o colección",
|
||||
"ReleaseProfile": "Perfil de lanzamiento",
|
||||
"ReleaseProfileIndexerHelpText": "Especifica a qué indexador se aplica el perfil",
|
||||
"RequiredHelpText": "Esta condición {implementationName} debe coincidir para el formato personalizado para aplicar. De otro modo una coincidencia sencilla {implementationName} es suficiente.",
|
||||
"RemotePathMappingsLoadError": "No se pudo cargar los mapeos de ruta remota",
|
||||
"RestartLater": "Reiniciaré más tarde",
|
||||
"RootFoldersLoadError": "No se pudo cargar las carpetas raíz",
|
||||
"RssSync": "Sincronización RSS",
|
||||
"RssSyncIntervalHelpTextWarning": "Esto se aplicará a todos los indexadores, por favor sigue las reglas establecidas por ellos",
|
||||
"Score": "Puntuación",
|
||||
"SearchFailedError": "La búsqueda falló, por favor inténtalo de nuevo más tarde.",
|
||||
"SearchForMonitoredEpisodes": "Buscar episodios monitorizados",
|
||||
"SearchIsNotSupportedWithThisIndexer": "La búsqueda no está soportada con este indexador",
|
||||
"SearchMonitored": "Buscar monitorizados",
|
||||
"SeasonPack": "Pack de temporada",
|
||||
"SeriesCannotBeFound": "Lo siento, esta serie no puede ser encontrada.",
|
||||
"SeriesEditRootFolderHelpText": "Mover series a la misma carpeta raíz se puede usar para renombrar carpetas de series para coincidir el título actualizado o el formato de nombrado",
|
||||
"SeriesFolderFormat": "Formato de carpeta de serie",
|
||||
"SeriesIndexFooterDownloading": "Descargando (Uno o más episodios)",
|
||||
"SeriesIndexFooterMissingMonitored": "Episodios perdidos (Serie monitorizada)",
|
||||
"SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Total: {totalEpisodeCount}, Descargando: {downloadingCount})",
|
||||
"UpgradesAllowed": "Actualizaciones permitidas",
|
||||
"VideoCodec": "Códec de vídeo",
|
||||
"SeriesTitleToExcludeHelpText": "El nombre de la serie a excluir",
|
||||
"Shutdown": "Apagar",
|
||||
"TestAll": "Probar todo",
|
||||
"UseProxy": "Usar proxy",
|
||||
"Repeat": "Repetir",
|
||||
"Replace": "Reemplazar",
|
||||
"RemoveCompletedDownloadsHelpText": "Elimina las descargas importadas desde el historial del cliente de descarga",
|
||||
"RemoveQueueItemRemovalMethod": "Método de eliminación",
|
||||
"RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.",
|
||||
"RemoveCompletedDownloads": "Eliminar descargas completadas",
|
||||
"RemoveFromDownloadClientHint": "Elimina la descarga y archivo(s) del cliente de descarga",
|
||||
"EpisodeRequested": "Episodio requerido",
|
||||
"NotificationsEmailSettingsServer": "Servidor",
|
||||
"NotificationsEmailSettingsServerHelpText": "Nombre de host o IP del servidor de e-mail",
|
||||
"NotificationsGotifySettingsAppTokenHelpText": "El token de aplicación generado por Gotify",
|
||||
"NotificationsGotifySettingsServerHelpText": "URL de servidor de Gotify, incluyendo http(s):// y puerto si es necesario",
|
||||
"NotificationsJoinSettingsDeviceIds": "IDs de dispositivo",
|
||||
"NotificationsJoinValidationInvalidDeviceId": "Los IDs de dispositivo parecen inválidos.",
|
||||
"NotificationsKodiSettingAlwaysUpdate": "Actualizar siempre",
|
||||
"NotificationsPushcutSettingsApiKeyHelpText": "Las claves API pueden ser gestionadas en la vista Cuenta de la aplicación Pushcut",
|
||||
"NotificationsPushcutSettingsNotificationName": "Nombre de notificación",
|
||||
"NotificationsPushoverSettingsRetryHelpText": "Intervalo para reintentar las alertas de emergencia, mínimo 30 segundos",
|
||||
"NotificationsSettingsUpdateLibrary": "Actualizar biblioteca",
|
||||
"NotificationsSettingsUpdateMapPathsTo": "Mapear rutas a",
|
||||
"NotificationsSignalSettingsSenderNumberHelpText": "Número de teléfono del emisor registrado en signal-api",
|
||||
"NotificationsSlackSettingsChannelHelpText": "Sobrescribe el canal predeterminado para el webhook entrante (#otro-canal)",
|
||||
"NotificationsSlackSettingsIcon": "Icono",
|
||||
"NotificationsSynologyValidationInvalidOs": "Debe ser un Synology",
|
||||
"NotificationsSynologyValidationTestFailed": "No es Synology o synoindex no está disponible",
|
||||
"NotificationsTelegramSettingsBotToken": "Token de bot",
|
||||
"NotificationsTelegramSettingsChatId": "ID de chat",
|
||||
"NotificationsTelegramSettingsTopicIdHelpText": "Especifica una ID de tema para enviar notificaciones a ese tema. Deja en blanco para usar el tema general (solo supergrupos)",
|
||||
"NotificationsTraktSettingsAccessToken": "Token de acceso",
|
||||
"NotificationsTraktSettingsAuthUser": "Autenticar usuario",
|
||||
"NotificationsTwitterSettingsAccessToken": "Token de acceso",
|
||||
"NotificationsTwitterSettingsAccessTokenSecret": "Token secreto de acceso",
|
||||
"NotificationsTwitterSettingsConsumerKey": "Clave de consumidor",
|
||||
"NotificationsTwitterSettingsMention": "Mención",
|
||||
"NotificationsTwitterSettingsMentionHelpText": "Menciona este usuario en tweets enviados",
|
||||
"NotificationsValidationInvalidAccessToken": "Token de acceso inválido",
|
||||
"NotificationsValidationInvalidApiKey": "Clave API inválida",
|
||||
"ParseModalErrorParsing": "Error analizando, por favor inténtalo de nuevo.",
|
||||
"ParseModalHelpText": "Introduce un título de lanzamiento en la entrada anterior",
|
||||
"SearchByTvdbId": "También puedes buscar usando la ID de TVDB de un show. P. ej. tvdb:71663",
|
||||
"SearchForAllMissingEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios perdidos?",
|
||||
"SeriesType": "Tipo de serie",
|
||||
"TagCannotBeDeletedWhileInUse": "La etiqueta no puede ser borrada mientras esté en uso",
|
||||
"UnmonitorSpecialEpisodes": "Dejar de monitorizar especiales",
|
||||
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' está en una carpeta de translocalización de la aplicación.",
|
||||
"Yesterday": "Ayer",
|
||||
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará la descarga y el archivo(s) del cliente de descarga.",
|
||||
"RemotePathMappingLocalFolderMissingHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir. Probablemente mapeo de ruta remota perdido o incorrecto.",
|
||||
"RemotePathMappingsInfo": "Los mapeos de ruta remota son muy raramente solicitados, si {appName} y tu cliente de descarga están en el mismo sistema es mejor coincidir sus rutas. Para más información mira la [wiki]({wikiLink})",
|
||||
"UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización",
|
||||
"NotificationsTelegramSettingsChatIdHelpText": "Debes comenzar una conversación con el bot o añádelo a tu grupo para recibir mensajes",
|
||||
"NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por coma de destinatarios de e-mail",
|
||||
"NotificationsTwitterSettingsConnectToTwitter": "Conectar a Twitter / X",
|
||||
"NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}",
|
||||
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
|
||||
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
|
||||
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
|
||||
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador.",
|
||||
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList",
|
||||
"ImportListsMyAnimeListSettingsListStatus": "Estado de lista",
|
||||
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user