Compare commits

...

15 Commits

Author SHA1 Message Date
Mark McDowall 37a9f670dd Fixed: Task progress messages in the UI
(cherry picked from commit c6417337812f3578a27f9dc1e44fdad80f557271)

Closes #3370
2024-03-22 11:45:46 +02:00
Bogdan 11eda3b11b Fix BookInfo tests 2024-03-17 00:36:02 +02:00
Weblate 04682c9d91 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Mark Martines <mark-martines@hotmail.com>
Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: linkin931 <931linkin@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-03-16 18:19:11 +02:00
Mina Gaid 50fdc449ac Fixed icons for macOS application 2024-03-16 17:52:10 +02:00
Bogdan b8c295727a Link to author from book details
Co-authored-by: plmcgrn <889547+plmcgrn@users.noreply.github.com>

Closes #3356
2024-03-16 00:46:31 +02:00
Mark McDowall 93ee466780 New: Show author names after task name when applicable
(cherry picked from commit 6d552f2a60f44052079b5e8944f5e1bbabac56e0)

Closes #3361
2024-03-15 23:50:22 +02:00
Mark McDowall 77f1e8f8c9 Fixed: Disabled select option still selectable
(cherry picked from commit 063dba22a803295adee4fdcbe42718af3e85ca78)

Closes #3362
2024-03-15 23:32:38 +02:00
Bogdan 1aa746bea1 Ensure authors are populated in PageConnector 2024-03-15 23:28:18 +02:00
Bogdan 490041d77c Ensure not allowed cursor is shown for disabled select inputs 2024-03-15 23:26:02 +02:00
Stevie Robinson 5dc5592c17 Fixed: Wrapping of naming tokens with alternate separators
(cherry picked from commit 80630bf97f5bb3b49d4824dc039d2edfc74e4797)

Closes #3294
Closes #3360
2024-03-15 23:25:16 +02:00
Servarr 8fb1aff68a Automated API Docs update 2024-03-14 07:58:15 +02:00
Mark McDowall a397a19034 Fixed: Release push with only Magnet URL
(cherry picked from commit 9f705e4161af3f4dd55b399d56b0b9c5a36e181b)
2024-03-14 07:13:17 +02:00
Bogdan d0df761422 New: Indexer flags
(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
2024-03-10 19:11:25 +02:00
Bogdan 4781675c1a Bump ImageSharp, Polly 2024-03-10 13:14:58 +02:00
Bogdan 0361262bb4 Bump version to 0.3.21 2024-03-10 09:06:37 +02:00
138 changed files with 1869 additions and 699 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+1 -1
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.20' majorVersion: '0.3.21'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
+4
View File
@@ -1,3 +1,5 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState'; import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState'; import TagsAppState from './TagsAppState';
@@ -34,6 +36,8 @@ export interface CustomFilter {
} }
interface AppState { interface AppState {
authors: AuthorsAppState;
commands: CommandAppState;
settings: SettingsAppState; settings: SettingsAppState;
tags: TagsAppState; tags: TagsAppState;
} }
+18
View File
@@ -0,0 +1,18 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Author from 'Author/Author';
interface AuthorsAppState
extends AppSectionState<Author>,
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
deleteOptions: {
addImportListExclusion: boolean;
};
}
export default AuthorsAppState;
@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;
@@ -5,6 +5,7 @@ import AppSectionState, {
import DownloadClient from 'typings/DownloadClient'; import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
@@ -27,11 +28,13 @@ export interface NotificationAppState
extends AppSectionState<Notification>, extends AppSectionState<Notification>,
AppSectionDeleteState {} AppSectionDeleteState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionState<UiSettings>; export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
uiSettings: UiSettingsAppState; uiSettings: UiSettingsAppState;
+18
View File
@@ -0,0 +1,18 @@
import ModelBase from 'App/ModelBase';
interface Author extends ModelBase {
added: string;
genres: string[];
monitored: boolean;
overview: string;
path: string;
qualityProfileId: number;
metadataProfileId: number;
rootFolderPath: string;
sortName: string;
tags: number[];
authorName: string;
isSaving?: boolean;
}
export default Author;
+2 -2
View File
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
function AuthorNameLink({ titleSlug, authorName }) { function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
const link = `/author/${titleSlug}`; const link = `/author/${titleSlug}`;
return ( return (
<Link to={link}> <Link to={link} {...otherProps}>
{authorName} {authorName}
</Link> </Link>
); );
+6
View File
@@ -27,3 +27,9 @@
width: 80px; width: 80px;
} }
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
+1
View File
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'indexerFlags': string;
'monitored': string; 'monitored': string;
'pageCount': string; 'pageCount': string;
'position': string; 'position': string;
+29
View File
@@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector'; import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink'; import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating'; import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookStatus from './BookStatus'; import BookStatus from './BookStatus';
import styles from './BookRow.css'; import styles from './BookRow.css';
@@ -67,6 +72,7 @@ class BookRow extends Component {
authorMonitored, authorMonitored,
titleSlug, titleSlug,
bookFiles, bookFiles,
indexerFlags,
isEditorActive, isEditorActive,
isSelected, isSelected,
onSelectedChange, onSelectedChange,
@@ -190,6 +196,24 @@ class BookRow extends Component {
); );
} }
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<TableRowCell <TableRowCell
@@ -235,6 +259,7 @@ BookRow.propTypes = {
position: PropTypes.string, position: PropTypes.string,
pageCount: PropTypes.number, pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired, authorMonitored: PropTypes.bool.isRequired,
@@ -246,4 +271,8 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired onMonitorBookPress: PropTypes.func.isRequired
}; };
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow; export default BookRow;
@@ -7,21 +7,18 @@ import BookRow from './BookRow';
const selectBookFiles = createSelector( const selectBookFiles = createSelector(
(state) => state.bookFiles, (state) => state.bookFiles,
(bookFiles) => { (bookFiles) => {
const { const { items } = bookFiles;
items
} = bookFiles;
const bookFileDict = items.reduce((acc, file) => { return items.reduce((acc, file) => {
const bookId = file.bookId; const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) { if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = []; acc[bookId] = [];
} }
acc[bookId].push(file); acc[bookId].push(file);
return acc; return acc;
}, {}); }, {});
return bookFileDict;
} }
); );
@@ -31,10 +28,14 @@ function createMapStateToProps() {
selectBookFiles, selectBookFiles,
(state, { id }) => id, (state, { id }) => id,
(author = {}, bookFiles, bookId) => { (author = {}, bookFiles, bookId) => {
const files = bookFiles[bookId] ?? [];
const bookFile = files[0];
return { return {
authorMonitored: author.monitored, authorMonitored: author.monitored,
authorName: author.authorName, authorName: author.authorName,
bookFiles: bookFiles[bookId] ?? [] bookFiles: files,
indexerFlags: bookFile ? bookFile.indexerFlags : 0
}; };
} }
); );
@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
@@ -84,9 +84,15 @@
font-size: 20px; font-size: 20px;
} }
.authorLink {
composes: link from '~Components/Link/Link.css';
margin-right: 15px;
color: var(--white);
}
.duration { .duration {
margin-right: 15px; margin-right: 15px;
margin-left: 10px;
} }
.detailsLabel { .detailsLabel {
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'alternateTitlesIconContainer': string; 'alternateTitlesIconContainer': string;
'authorLink': string;
'backdrop': string; 'backdrop': string;
'backdropOverlay': string; 'backdropOverlay': string;
'cover': string; 'cover': string;
@@ -2,6 +2,7 @@ import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookCover from 'Book/BookCover'; import BookCover from 'Book/BookCover';
import HeartRating from 'Components/HeartRating'; import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
@@ -113,7 +114,7 @@ class BookDetailsHeader extends Component {
className={styles.monitorToggleButton} className={styles.monitorToggleButton}
monitored={monitored} monitored={monitored}
isSaving={isSaving} isSaving={isSaving}
size={isSmallScreen ? 30: 40} size={isSmallScreen ? 30 : 40}
onPress={onMonitorTogglePress} onPress={onMonitorTogglePress}
/> />
</div> </div>
@@ -131,7 +132,12 @@ class BookDetailsHeader extends Component {
</div> </div>
<div> <div>
{author.authorName} <AuthorNameLink
className={styles.authorLink}
titleSlug={author.titleSlug}
authorName={author.authorName}
/>
{ {
!!pageCount && !!pageCount &&
<span className={styles.duration}> <span className={styles.duration}>
+1 -1
View File
@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;
@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
}); });
return acc; return acc;
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); }, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
const hasSelectedFiles = this.getSelectedIds().length > 0; const hasSelectedFiles = this.getSelectedIds().length > 0;
+1 -1
View File
@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
+38
View File
@@ -0,0 +1,38 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
authorId?: number;
authorIds?: number[];
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;
@@ -19,7 +19,7 @@
.isDisabled { .isDisabled {
opacity: 0.7; opacity: 0.7;
cursor: not-allowed; cursor: not-allowed !important;
} }
.dropdownArrowContainer { .dropdownArrowContainer {
@@ -14,6 +14,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
@@ -83,6 +84,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT: case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector; return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT: case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector; return DownloadClientSelectInputConnector;
@@ -288,6 +292,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool, includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object, selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool, pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object), errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object),
@@ -0,0 +1,62 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;
@@ -39,7 +39,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: includeNoChangeDisabled isDisabled: includeNoChangeDisabled
}); });
} }
@@ -47,7 +47,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -18,7 +18,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: true isDisabled: true
}); });
} }
@@ -26,7 +26,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -17,7 +17,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: true isDisabled: true
}); });
} }
@@ -25,7 +25,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
@@ -26,7 +26,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), value: translate('NoChange'),
disabled: includeNoChangeDisabled isDisabled: includeNoChangeDisabled
}); });
} }
@@ -34,7 +34,7 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', value: '(Mixed)',
disabled: true isDisabled: true
}); });
} }
+4
View File
@@ -2,3 +2,7 @@
margin-right: 5px; margin-right: 5px;
color: var(--themeRed); color: var(--themeRed);
} }
.rating {
margin-right: 15px;
}
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'heart': string; 'heart': string;
'rating': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+1 -1
View File
@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) { function HeartRating({ rating, iconSize }) {
return ( return (
<span> <span className={styles.rating}>
<Icon <Icon
className={styles.heart} className={styles.heart}
name={icons.HEART} name={icons.HEART}
+27 -1
View File
@@ -7,7 +7,14 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchMetadataProfiles,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -44,6 +51,7 @@ const selectAppProps = createSelector(
); );
const selectIsPopulated = createSelector( const selectIsPopulated = createSelector(
(state) => state.authors.isPopulated,
(state) => state.customFilters.isPopulated, (state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
@@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated, (state) => state.app.translations.isPopulated,
( (
authorsIsPopulated,
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
@@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated,
translationsIsPopulated translationsIsPopulated
) => { ) => {
return ( return (
authorsIsPopulated &&
customFiltersIsPopulated && customFiltersIsPopulated &&
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
@@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated &&
translationsIsPopulated translationsIsPopulated
); );
@@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
); );
const selectErrors = createSelector( const selectErrors = createSelector(
(state) => state.authors.error,
(state) => state.customFilters.error, (state) => state.customFilters.error,
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
@@ -86,9 +100,11 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error, (state) => state.app.translations.error,
( (
authorsError,
customFiltersError, customFiltersError,
tagsError, tagsError,
uiSettingsError, uiSettingsError,
@@ -96,10 +112,12 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
authorsError ||
customFiltersError || customFiltersError ||
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
@@ -107,6 +125,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
indexerFlagsError ||
systemStatusError || systemStatusError ||
translationsError translationsError
); );
@@ -120,6 +139,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
}; };
@@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() { dispatchFetchImportLists() {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
}, },
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() { dispatchFetchUISettings() {
dispatch(fetchUISettings()); dispatch(fetchUISettings());
}, },
@@ -218,6 +241,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles(); this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations(); this.props.dispatchFetchTranslations();
@@ -245,6 +269,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles, dispatchFetchMetadataProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations, dispatchFetchTranslations,
@@ -287,6 +312,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired, dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired,
@@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
return [isOpen, setModalOpen, setModalClosed];
}
+2
View File
@@ -60,6 +60,7 @@ import {
faFileImport as fasFileImport, faFileImport as fasFileImport,
faFileInvoice as farFileInvoice, faFileInvoice as farFileInvoice,
faFilter as fasFilter, faFilter as fasFilter,
faFlag as fasFlag,
faFolderOpen as fasFolderOpen, faFolderOpen as fasFolderOpen,
faForward as fasForward, faForward as fasForward,
faHeart as fasHeart, faHeart as fasHeart,
@@ -155,6 +156,7 @@ export const FATAL = fasTimesCircle;
export const FILE = farFile; export const FILE = farFile;
export const FILEIMPORT = fasFileImport; export const FILEIMPORT = fasFileImport;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FLAG = fasFlag;
export const FOLDER = farFolder; export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen; export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;
+1
View File
@@ -15,6 +15,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect'; export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const INDEXER_SELECT = 'indexerSelect'; export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select'; export const SELECT = 'select';
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector';
class SelectIndexerFlagsModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectIndexerFlagsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectIndexerFlagsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModal;
@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalBody': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import React, { Component } 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, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
class SelectIndexerFlagsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
indexerFlags
} = props;
this.state = {
indexerFlags
};
}
//
// Listeners
onIndexerFlagsChange = ({ value }) => {
this.setState({ indexerFlags: value });
};
onIndexerFlagsSelect = () => {
this.props.onIndexerFlagsSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
indexerFlags
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Set indexer Flags
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>
{translate('IndexerFlags')}
</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={this.onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onIndexerFlagsSelect}
>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectIndexerFlagsModalContent.propTypes = {
indexerFlags: PropTypes.number.isRequired,
onIndexerFlagsSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModalContent;
@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectIndexerFlagsModalContentConnector extends Component {
//
// Listeners
onIndexerFlagsSelect = ({ indexerFlags }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
indexerFlags
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectIndexerFlagsModalContent
{...this.props}
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
/>
);
}
}
SelectIndexerFlagsModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);
@@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal'; import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow'; import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css'; import styles from './InteractiveImportModalContent.css';
const columns = [ const COLUMNS = [
{ {
name: 'path', name: 'path',
label: 'Path', label: 'Path',
@@ -74,11 +75,21 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.DANGER, name: icons.DANGER,
kind: kinds.DANGER kind: kinds.DANGER,
title: () => translate('Rejections')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -102,6 +113,7 @@ const BOOK = 'book';
const EDITION = 'edition'; const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup'; const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality'; const QUALITY = 'quality';
const INDEXER_FLAGS = 'indexerFlags';
const replaceExistingFilesOptions = { const replaceExistingFilesOptions = {
COMBINE: 'combine', COMBINE: 'combine',
@@ -288,6 +300,21 @@ class InteractiveImportModalContent extends Component {
inconsistentBookReleases inconsistentBookReleases
} = this.state; } = this.state;
const allColumns = _.cloneDeep(COLUMNS);
const columns = allColumns.map((column) => {
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return column;
});
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value(); const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value();
@@ -299,7 +326,8 @@ class InteractiveImportModalContent extends Component {
{ key: BOOK, value: translate('SelectBook') }, { key: BOOK, value: translate('SelectBook') },
{ key: EDITION, value: translate('SelectEdition') }, { key: EDITION, value: translate('SelectEdition') },
{ key: QUALITY, value: translate('SelectQuality') }, { key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') } { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') },
{ key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') }
]; ];
if (allowAuthorChange) { if (allowAuthorChange) {
@@ -422,6 +450,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving} isSaving={isSaving}
{...item} {...item}
allowAuthorChange={allowAuthorChange} allowAuthorChange={allowAuthorChange}
columns={columns}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange} onValidRowChange={this.onValidRowChange}
/> />
@@ -518,6 +547,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose} onModalClose={this.onSelectModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={selectModalOpen === INDEXER_FLAGS}
ids={selectedIds}
indexerFlags={0}
onModalClose={this.onSelectModalClose}
/>
<ConfirmImportModal <ConfirmImportModal
isOpen={isConfirmImportModalOpen} isOpen={isConfirmImportModalOpen}
books={booksImported} books={booksImported}
@@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component {
book, book,
foreignEditionId, foreignEditionId,
quality, quality,
indexerFlags,
disableReleaseSwitching disableReleaseSwitching
} = item; } = item;
@@ -158,6 +159,7 @@ class InteractiveImportModalContentConnector extends Component {
bookId: book.id, bookId: book.id,
foreignEditionId, foreignEditionId,
quality, quality,
indexerFlags,
downloadId: this.props.downloadId, downloadId: this.props.downloadId,
disableReleaseSwitching disableReleaseSwitching
}); });
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookFormats from 'Book/BookFormats'; import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import FileDetails from 'BookFile/FileDetails'; import FileDetails from 'BookFile/FileDetails';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -14,6 +15,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal'; import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -34,7 +36,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen: false, isSelectAuthorModalOpen: false,
isSelectBookModalOpen: false, isSelectBookModalOpen: false,
isSelectReleaseGroupModalOpen: false, isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false isSelectQualityModalOpen: false,
isSelectIndexerFlagsModalOpen: false
}; };
} }
@@ -133,6 +136,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectQualityModalOpen: true }); this.setState({ isSelectQualityModalOpen: true });
}; };
onSelectIndexerFlagsPress = () => {
this.setState({ isSelectIndexerFlagsModalOpen: true });
};
onSelectAuthorModalClose = (changed) => { onSelectAuthorModalClose = (changed) => {
this.setState({ isSelectAuthorModalOpen: false }); this.setState({ isSelectAuthorModalOpen: false });
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
@@ -153,6 +160,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
}; };
onSelectIndexerFlagsModalClose = (changed) => {
this.setState({ isSelectIndexerFlagsModalOpen: false });
this.selectRowAfterChange(changed);
};
// //
// Render // Render
@@ -167,7 +179,9 @@ class InteractiveImportRow extends Component {
releaseGroup, releaseGroup,
size, size,
customFormats, customFormats,
indexerFlags,
rejections, rejections,
columns,
additionalFile, additionalFile,
isSelected, isSelected,
isReprocessing, isReprocessing,
@@ -180,7 +194,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen, isSelectAuthorModalOpen,
isSelectBookModalOpen, isSelectBookModalOpen,
isSelectReleaseGroupModalOpen, isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen isSelectQualityModalOpen,
isSelectIndexerFlagsModalOpen
} = this.state; } = this.state;
const authorName = author ? author.authorName : ''; const authorName = author ? author.authorName : '';
@@ -193,6 +208,7 @@ class InteractiveImportRow extends Component {
const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book; const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality; const showQualityPlaceholder = isSelected && !quality;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
const pathCellContents = ( const pathCellContents = (
<div onClick={this.onDetailsPress}> <div onClick={this.onDetailsPress}>
@@ -215,6 +231,8 @@ class InteractiveImportRow extends Component {
/> />
); );
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
return ( return (
<TableRow <TableRow
className={additionalFile ? styles.additionalFile : undefined} className={additionalFile ? styles.additionalFile : undefined}
@@ -307,6 +325,28 @@ class InteractiveImportRow extends Component {
} }
</TableRowCell> </TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={this.onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell> <TableRowCell>
{ {
rejections.length ? rejections.length ?
@@ -378,6 +418,13 @@ class InteractiveImportRow extends Component {
real={quality ? quality.revision.real > 0 : false} real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose} onModalClose={this.onSelectQualityModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={isSelectIndexerFlagsModalOpen}
ids={[id]}
indexerFlags={indexerFlags ?? 0}
onModalClose={this.onSelectIndexerFlagsModalClose}
/>
</TableRow> </TableRow>
); );
} }
@@ -395,7 +442,9 @@ InteractiveImportRow.propTypes = {
quality: PropTypes.object, quality: PropTypes.object,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired, audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired, additionalFile: PropTypes.bool.isRequired,
isReprocessing: PropTypes.bool, isReprocessing: PropTypes.bool,
@@ -62,6 +62,15 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { label: React.createElement(Icon, {
@@ -40,6 +40,7 @@
} }
.rejected, .rejected,
.indexerFlags,
.download { .download {
composes: cell; composes: cell;
@@ -6,6 +6,7 @@ interface CssExports {
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'indexer': string; 'indexer': string;
'indexerFlags': string;
'peers': string; 'peers': string;
'protocol': string; 'protocol': string;
'quality': string; 'quality': string;
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import BookFormats from 'Book/BookFormats'; import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality, quality,
customFormatScore, customFormatScore,
customFormats, customFormats,
indexerFlags = 0,
rejections, rejections,
downloadAllowed, downloadAllowed,
isGrabbing, isGrabbing,
@@ -189,10 +191,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<BookFormats formats={customFormats} />} tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.LEFT}
/> />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}> <TableRowCell className={styles.rejected}>
{ {
!!rejections.length && !!rejections.length &&
@@ -265,6 +278,7 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired, customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired, downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
@@ -277,6 +291,7 @@ InteractiveSearchRow.propTypes = {
}; };
InteractiveSearchRow.defaultProps = { InteractiveSearchRow.defaultProps = {
indexerFlags: 0,
rejections: [], rejections: [],
isGrabbing: false, isGrabbing: false,
isGrabbed: false isGrabbed: false
@@ -32,7 +32,7 @@ const enableOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -32,7 +32,7 @@ const autoAddOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -32,7 +32,7 @@ const enableOptions = [
get value() { get value() {
return translate('NoChange'); return translate('NoChange');
}, },
disabled: true, isDisabled: true,
}, },
{ {
key: 'enabled', key: 'enabled',
@@ -1,6 +1,6 @@
.option { .option {
display: flex; display: flex;
align-items: center; align-items: stretch;
flex-wrap: wrap; flex-wrap: wrap;
margin: 3px; margin: 3px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
@@ -17,7 +17,7 @@
} }
.small { .small {
width: 460px; width: 490px;
} }
.large { .large {
@@ -26,7 +26,7 @@
.token { .token {
flex: 0 0 50%; flex: 0 0 50%;
padding: 6px 16px; padding: 6px;
background-color: var(--popoverTitleBackgroundColor); background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily; font-family: $monoSpaceFontFamily;
} }
@@ -34,9 +34,9 @@
.example { .example {
display: flex; display: flex;
align-items: center; align-items: center;
align-self: stretch; justify-content: space-between;
flex: 0 0 50%; flex: 0 0 50%;
padding: 6px 16px; padding: 6px;
background-color: var(--popoverBodyBackgroundColor); background-color: var(--popoverBodyBackgroundColor);
.footNote { .footNote {
@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};
+12 -1
View File
@@ -1,9 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import React from 'react';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import bookEntities from 'Book/bookEntities'; import bookEntities from 'Book/bookEntities';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import Icon from 'Components/Icon';
import { filterTypePredicates, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
@@ -243,6 +245,15 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{ {
name: 'status', name: 'status',
label: 'Status', label: 'Status',
@@ -207,6 +207,7 @@ export const actionHandlers = handleThunks({
foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined, foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined,
quality: item.quality, quality: item.quality,
releaseGroup: item.releaseGroup, releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId, downloadId: item.downloadId,
additionalFile: item.additionalFile, additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles, replaceExistingFiles: item.replaceExistingFiles,
@@ -10,6 +10,7 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general'; import general from './Settings/general';
import importListExclusions from './Settings/importListExclusions'; import importListExclusions from './Settings/importListExclusions';
import importLists from './Settings/importLists'; import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions'; import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers'; import indexers from './Settings/indexers';
import languages from './Settings/languages'; import languages from './Settings/languages';
@@ -35,6 +36,7 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/general'; export * from './Settings/general';
export * from './Settings/importLists'; export * from './Settings/importLists';
export * from './Settings/importListExclusions'; export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions'; export * from './Settings/indexerOptions';
export * from './Settings/indexers'; export * from './Settings/indexers';
export * from './Settings/languages'; export * from './Settings/languages';
@@ -70,6 +72,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState, downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState, downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState, general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState, indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState, indexers: indexers.defaultState,
importLists: importLists.defaultState, importLists: importLists.defaultState,
@@ -115,6 +118,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers, ...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers, ...downloadClientOptions.actionHandlers,
...general.actionHandlers, ...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers, ...indexerOptions.actionHandlers,
...indexers.actionHandlers, ...indexers.actionHandlers,
...importLists.actionHandlers, ...importLists.actionHandlers,
@@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers, ...downloadClients.reducers,
...downloadClientOptions.reducers, ...downloadClientOptions.reducers,
...general.reducers, ...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers, ...indexerOptions.reducers,
...indexers.reducers, ...indexers.reducers,
...importLists.reducers, ...importLists.reducers,
@@ -0,0 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;
@@ -0,0 +1,14 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createMultiAuthorsSelector(authorIds: number[]) {
return createSelector(
(state: AppState) => state.authors.itemMap,
(state: AppState) => state.authors.items,
(itemMap, allAuthors) => {
return authorIds.map((authorId) => allAuthors[itemMap[authorId]]);
}
);
}
export default createMultiAuthorsSelector;
@@ -10,15 +10,6 @@
width: 100%; width: 100%;
} }
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued, .queued,
.started, .started,
.ended { .ended {
@@ -2,14 +2,12 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'commandName': string;
'duration': string; 'duration': string;
'ended': string; 'ended': string;
'queued': string; 'queued': string;
'started': string; 'started': string;
'trigger': string; 'trigger': string;
'triggerContent': string; 'triggerContent': string;
'userAgent': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default 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('UserAgentProvidedByTheAppThatCalledTheAPI')}>
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('CancelMessageText')}
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;
@@ -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;
}
@@ -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;
@@ -0,0 +1,49 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiAuthorsSelector from 'Store/Selectors/createMultiAuthorsSelector';
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 movieIds = [...(body.authorIds ?? [])];
if (body.authorId) {
movieIds.push(body.authorId);
}
const authors = useSelector(createMultiAuthorsSelector(movieIds));
const sortedAuthors = authors.sort((a, b) =>
a.sortName.localeCompare(b.sortName)
);
return (
<TableRowCell>
<span className={styles.commandName}>
{commandName}
{sortedAuthors.length ? (
<span> - {sortedAuthors.map((a) => a.authorName).join(', ')}</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;
@@ -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 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import QueuedTasksConnector from './Queued/QueuedTasksConnector'; import QueuedTasks from './Queued/QueuedTasks';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() { function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
<PageContent title={translate('Tasks')}> <PageContent title={translate('Tasks')}>
<PageContentBody> <PageContentBody>
<ScheduledTasksConnector /> <ScheduledTasksConnector />
<QueuedTasksConnector /> <QueuedTasks />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
+6
View File
@@ -0,0 +1,6 @@
interface IndexerFlag {
id: number;
name: string;
}
export default IndexerFlag;
+1 -1
View File
@@ -11,7 +11,7 @@
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/", "lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
"lint-fix": "yarn lint --fix", "lint-fix": "yarn lint --fix",
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc", "stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc" "stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
+2 -2
View File
@@ -8,7 +8,7 @@
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> <PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" /> <PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" /> <PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="Polly" Version="8.2.0" /> <PackageVersion Include="Polly" Version="8.3.1" />
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
@@ -44,7 +44,7 @@
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" /> <PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" /> <PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" /> <PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" /> <PackageVersion Include="System.Buffers" Version="4.5.1" />
@@ -9,6 +9,7 @@ using NUnit.Framework;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.BookImport; using NzbDrone.Core.MediaFiles.BookImport;
using NzbDrone.Core.MediaFiles.BookImport.Aggregation; using NzbDrone.Core.MediaFiles.BookImport.Aggregation;
@@ -134,6 +135,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.Setup(s => s.ReadTags(It.IsAny<IFileInfo>())) .Setup(s => s.ReadTags(It.IsAny<IFileInfo>()))
.Returns(new ParsedTrackInfo()); .Returns(new ParsedTrackInfo());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EntityHistory>());
GivenSpecifications(_bookpass1); GivenSpecifications(_bookpass1);
} }
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-03-15 00:00:00Z")] [Ignore("Waiting for metadata to be back again", Until = "2024-05-15 00:00:00Z")]
public class BookInfoProxyFixture : CoreTest<BookInfoProxy> public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
{ {
private MetadataProfile _metadataProfile; private MetadataProfile _metadataProfile;
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
} }
[TestCase("1128601", "Guards! Guards!")] [TestCase("1128601", "Guards! Guards!")]
[TestCase("3293141", "Ιλιάς")] [TestCase("3293141", "λιάς")]
public void should_be_able_to_get_book_detail(string mbId, string name) public void should_be_able_to_get_book_detail(string mbId, string name)
{ {
var details = Subject.GetBookInfo(mbId); var details = Subject.GetBookInfo(mbId);
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{ {
[TestFixture] [TestFixture]
[Ignore("Waiting for metadata to be back again", Until = "2024-03-15 00:00:00Z")] [Ignore("Waiting for metadata to be back again", Until = "2024-05-15 00:00:00Z")]
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy> public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
{ {
[SetUp] [SetUp]
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Blocklisting namespace NzbDrone.Core.Blocklisting
@@ -19,6 +20,7 @@ namespace NzbDrone.Core.Blocklisting
public long? Size { get; set; } public long? Size { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public string Indexer { get; set; } public string Indexer { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string TorrentInfoHash { get; set; } public string TorrentInfoHash { get; set; }
} }
@@ -188,6 +188,11 @@ namespace NzbDrone.Core.Blocklisting
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash") TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash")
}; };
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
blocklist.IndexerFlags = flags;
}
_blocklistRepository.Insert(blocklist); _blocklistRepository.Insert(blocklist);
} }
@@ -20,5 +20,7 @@ namespace NzbDrone.Core.Books.Commands
public override bool SendUpdatesToClient => true; public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => !AuthorId.HasValue; public override bool UpdateScheduledTask => !AuthorId.HasValue;
public override string CompletionMessage => "Completed";
} }
} }
@@ -18,5 +18,7 @@ namespace NzbDrone.Core.Books.Commands
public override bool SendUpdatesToClient => true; public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => !BookId.HasValue; public override bool UpdateScheduledTask => !BookId.HasValue;
public override string CompletionMessage => "Completed";
} }
} }
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -38,7 +39,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
BookInfo = remoteBook.ParsedBookInfo, BookInfo = remoteBook.ParsedBookInfo,
Author = remoteBook.Author, Author = remoteBook.Author,
Size = size Size = size,
IndexerFlags = remoteBook.Release?.IndexerFlags ?? 0
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@@ -70,7 +72,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
BookInfo = bookInfo, BookInfo = bookInfo,
Author = author, Author = author,
Size = blocklist.Size ?? 0 Size = blocklist.Size ?? 0,
IndexerFlags = blocklist.IndexerFlags
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@@ -81,6 +84,7 @@ namespace NzbDrone.Core.CustomFormats
var parsed = Parser.Parser.ParseBookTitle(history.SourceTitle); var parsed = Parser.Parser.ParseBookTitle(history.SourceTitle);
long.TryParse(history.Data.GetValueOrDefault("size"), out var size); long.TryParse(history.Data.GetValueOrDefault("size"), out var size);
Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags);
var bookInfo = new ParsedBookInfo var bookInfo = new ParsedBookInfo
{ {
@@ -94,7 +98,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
BookInfo = bookInfo, BookInfo = bookInfo,
Author = author, Author = author,
Size = size Size = size,
IndexerFlags = indexerFlags
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@@ -114,7 +119,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
BookInfo = bookInfo, BookInfo = bookInfo,
Author = localBook.Author, Author = localBook.Author,
Size = localBook.Size Size = localBook.Size,
IndexerFlags = localBook.IndexerFlags,
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@@ -181,6 +187,7 @@ namespace NzbDrone.Core.CustomFormats
BookInfo = bookInfo, BookInfo = bookInfo,
Author = author, Author = author,
Size = bookFile.Size, Size = bookFile.Size,
IndexerFlags = bookFile.IndexerFlags,
Filename = Path.GetFileName(bookFile.Path) Filename = Path.GetFileName(bookFile.Path)
}; };
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.CustomFormats
public ParsedBookInfo BookInfo { get; set; } public ParsedBookInfo BookInfo { get; set; }
public Author Author { get; set; } public Author Author { get; set; }
public long Size { get; set; } public long Size { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public string Filename { get; set; } public string Filename { get; set; }
// public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series) // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series)
@@ -0,0 +1,44 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats
{
public class IndexerFlagSpecificationValidator : AbstractValidator<IndexerFlagSpecification>
{
public IndexerFlagSpecificationValidator()
{
RuleFor(c => c.Value).NotEmpty();
RuleFor(c => c.Value).Custom((flag, context) =>
{
if (!Enum.IsDefined(typeof(IndexerFlags), flag))
{
context.AddFailure($"Invalid indexer flag condition value: {flag}");
}
});
}
}
public class IndexerFlagSpecification : CustomFormatSpecificationBase
{
private static readonly IndexerFlagSpecificationValidator Validator = new ();
public override int Order => 4;
public override string ImplementationName => "Indexer Flag";
[FieldDefinition(1, Label = "CustomFormatsSpecificationFlag", Type = FieldType.Select, SelectOptions = typeof(IndexerFlags))]
public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
{
return input.IndexerFlags.HasFlag((IndexerFlags)Value);
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(040)]
public class add_indexer_flags : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Blocklist").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
Alter.Table("BookFiles").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
}
}
}
@@ -12,6 +12,7 @@ using NzbDrone.Core.Download.History;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.TrackedDownloads namespace NzbDrone.Core.Download.TrackedDownloads
{ {
@@ -156,11 +157,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
var firstHistoryItem = historyItems.First(); var firstHistoryItem = historyItems.First();
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
if (parsedBookInfo == null || if (parsedBookInfo == null ||
trackedDownload.RemoteBook == null || trackedDownload.RemoteBook?.Author == null ||
trackedDownload.RemoteBook.Author == null ||
trackedDownload.RemoteBook.Books.Empty()) trackedDownload.RemoteBook.Books.Empty())
{ {
// Try parsing the original source title and if that fails, try parsing it as a special // Try parsing the original source title and if that fails, try parsing it as a special
@@ -192,6 +192,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
} }
if (trackedDownload.RemoteBook != null &&
Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
trackedDownload.RemoteBook.Release ??= new ReleaseInfo();
trackedDownload.RemoteBook.Release.IndexerFlags = flags;
}
} }
// Calculate custom formats // Calculate custom formats
@@ -164,6 +164,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString()); history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString());
history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString()); history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString());
history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString()); history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString());
history.Data.Add("IndexerFlags", message.Book.Release.IndexerFlags.ToString());
if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace()) if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace())
{ {
@@ -201,6 +202,8 @@ namespace NzbDrone.Core.History
history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson()); history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson());
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
history.Data.Add("IndexerFlags", message.TrackedDownload?.RemoteBook?.Release?.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
} }
@@ -237,6 +240,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name); history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup);
history.Data.Add("Size", message.BookInfo.Size.ToString()); history.Data.Add("Size", message.BookInfo.Size.ToString());
history.Data.Add("IndexerFlags", message.BookInfo.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -290,6 +294,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -313,6 +318,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Path", path); history.Data.Add("Path", path);
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("Size", message.BookFile.Size.ToString()); history.Data.Add("Size", message.BookFile.Size.ToString());
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -134,7 +134,7 @@ namespace NzbDrone.Core.IndexerSearch
var reports = batch.SelectMany(x => x).ToList(); 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 albums if at least 1 indexer was searched. // Update the last search time for all albums if at least 1 indexer was searched.
if (indexers.Any()) if (indexers.Any())
@@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
{ {
var id = result.Id; var id = result.Id;
//if (result.FreeLeech) torrentInfos.Add(new TorrentInfo
torrentInfos.Add(new TorrentInfo()
{ {
Guid = $"FileList-{id}", Guid = $"FileList-{id}",
Title = result.Name, Title = result.Name,
@@ -48,13 +47,31 @@ namespace NzbDrone.Core.Indexers.FileList
InfoUrl = GetInfoUrl(id), InfoUrl = GetInfoUrl(id),
Seeders = result.Seeders, Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders, Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate.ToUniversalTime() PublishDate = result.UploadDate.ToUniversalTime(),
IndexerFlags = GetIndexerFlags(result)
}); });
} }
return torrentInfos.ToArray(); return torrentInfos.ToArray();
} }
private static IndexerFlags GetIndexerFlags(FileListTorrent item)
{
IndexerFlags flags = 0;
if (item.FreeLeech)
{
flags |= IndexerFlags.Freeleech;
}
if (item.Internal)
{
flags |= IndexerFlags.Internal;
}
return flags;
}
private string GetDownloadUrl(string torrentId) private string GetDownloadUrl(string torrentId)
{ {
var url = new HttpUri(_settings.BaseUrl) var url = new HttpUri(_settings.BaseUrl)
@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers.FileList
public uint Files { get; set; } public uint Files { get; set; }
[JsonProperty(PropertyName = "imdb")] [JsonProperty(PropertyName = "imdb")]
public string ImdbId { get; set; } public string ImdbId { get; set; }
public bool Internal { get; set; }
[JsonProperty(PropertyName = "freeleech")] [JsonProperty(PropertyName = "freeleech")]
public bool FreeLeech { get; set; } public bool FreeLeech { get; set; }
[JsonProperty(PropertyName = "upload_date")] [JsonProperty(PropertyName = "upload_date")]
@@ -34,6 +34,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public string Leechers { get; set; } public string Leechers { get; set; }
public bool IsFreeLeech { get; set; } public bool IsFreeLeech { get; set; }
public bool IsNeutralLeech { get; set; } public bool IsNeutralLeech { get; set; }
public bool IsFreeload { get; set; }
public bool IsPersonalFreeLeech { get; set; } public bool IsPersonalFreeLeech { get; set; }
public bool CanUseToken { get; set; } public bool CanUseToken { get; set; }
} }
@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
var author = WebUtility.HtmlDecode(result.Author); var author = WebUtility.HtmlDecode(result.Author);
var book = WebUtility.HtmlDecode(result.GroupName); var book = WebUtility.HtmlDecode(result.GroupName);
torrentInfos.Add(new GazelleInfo() torrentInfos.Add(new GazelleInfo
{ {
Guid = string.Format("Gazelle-{0}", id), Guid = string.Format("Gazelle-{0}", id),
Author = author, Author = author,
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
Seeders = int.Parse(torrent.Seeders), Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(), PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene, IndexerFlags = GetIndexerFlags(torrent)
}); });
} }
} }
@@ -88,6 +88,23 @@ namespace NzbDrone.Core.Indexers.Gazelle
.ToArray(); .ToArray();
} }
private static IndexerFlags GetIndexerFlags(GazelleTorrent torrent)
{
IndexerFlags flags = 0;
if (torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech)
{
flags |= IndexerFlags.Freeleech;
}
if (torrent.Scene)
{
flags |= IndexerFlags.Scene;
}
return flags;
}
private string GetDownloadUrl(int torrentId) private string GetDownloadUrl(int torrentId)
{ {
var url = new HttpUri(_settings.BaseUrl) var url = new HttpUri(_settings.BaseUrl)
@@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
public class RssSyncCommand : Command public class RssSyncCommand : Command
{ {
public override bool SendUpdatesToClient => true; public override bool SendUpdatesToClient => true;
public override bool IsLongRunning => true; public override bool IsLongRunning => true;
} }
} }
@@ -74,6 +74,18 @@ namespace NzbDrone.Core.Indexers.Torznab
return true; return true;
} }
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
{
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
if (torrentInfo != null)
{
torrentInfo.IndexerFlags = GetFlags(item);
}
return torrentInfo;
}
protected override string GetInfoUrl(XElement item) protected override string GetInfoUrl(XElement item)
{ {
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments")); return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
@@ -194,6 +206,53 @@ namespace NzbDrone.Core.Indexers.Torznab
return base.GetPeers(item); return base.GetPeers(item);
} }
protected IndexerFlags GetFlags(XElement item)
{
IndexerFlags flags = 0;
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
if (downloadFactor == 0.5)
{
flags |= IndexerFlags.Halfleech;
}
if (downloadFactor == 0.75)
{
flags |= IndexerFlags.Freeleech25;
}
if (downloadFactor == 0.25)
{
flags |= IndexerFlags.Freeleech75;
}
if (downloadFactor == 0.0)
{
flags |= IndexerFlags.Freeleech;
}
if (uploadFactor == 2.0)
{
flags |= IndexerFlags.DoubleUpload;
}
var tags = TryGetMultipleTorznabAttributes(item, "tag");
if (tags.Any(t => t.EqualsIgnoreCase("internal")))
{
flags |= IndexerFlags.Internal;
}
if (tags.Any(t => t.EqualsIgnoreCase("scene")))
{
flags |= IndexerFlags.Scene;
}
return flags;
}
protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "") protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "")
{ {
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
@@ -209,6 +268,13 @@ namespace NzbDrone.Core.Indexers.Torznab
return defaultValue; return defaultValue;
} }
protected float TryGetFloatTorznabAttribute(XElement item, string key, float defaultValue = 0)
{
var attr = TryGetTorznabAttribute(item, key, defaultValue.ToString());
return float.TryParse(attr, out var result) ? result : defaultValue;
}
protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key) protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key)
{ {
var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
+6 -1
View File
@@ -631,5 +631,10 @@
"ExtraFileExtensionsHelpTextsExamples": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\"", "ExtraFileExtensionsHelpTextsExamples": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\"",
"AutoRedownloadFailed": "التحميل فشل", "AutoRedownloadFailed": "التحميل فشل",
"SourceTitle": "عنوان المصدر", "SourceTitle": "عنوان المصدر",
"RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟" "RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟",
"ImportLists": "القوائم",
"ListsSettingsSummary": "القوائم",
"SelectDropdown": "'تحديد...",
"SelectQuality": "حدد الجودة",
"CustomFilter": "مرشحات مخصصة"
} }
+7 -1
View File
@@ -630,5 +630,11 @@
"ExtraFileExtensionsHelpText": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)", "ExtraFileExtensionsHelpText": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Примери: '.sub, .nfo' или 'sub, nfo'", "ExtraFileExtensionsHelpTextsExamples": "Примери: '.sub, .nfo' или 'sub, nfo'",
"AutoRedownloadFailed": "Изтеглянето се провали", "AutoRedownloadFailed": "Изтеглянето се провали",
"SourceTitle": "Заглавие на източника" "SourceTitle": "Заглавие на източника",
"ImportLists": "Списъци",
"ListsSettingsSummary": "Списъци",
"SelectDropdown": "„Изберете ...",
"SelectQuality": "Изберете Качество",
"CustomFilter": "Персонализирани филтри",
"RemoveQueueItemConfirmation": "Наистина ли искате да премахнете {0} елемент {1} от опашката?"
} }
+4 -1
View File
@@ -688,5 +688,8 @@
"SourceTitle": "Název zdroje", "SourceTitle": "Název zdroje",
"AutoRedownloadFailed": "Opětovné stažení se nezdařilo", "AutoRedownloadFailed": "Opětovné stažení se nezdařilo",
"AutoRedownloadFailedFromInteractiveSearch": "Opětovné stažení z interaktivního vyhledávání selhalo", "AutoRedownloadFailedFromInteractiveSearch": "Opětovné stažení z interaktivního vyhledávání selhalo",
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Automaticky vyhledat a pokusit se o stažení jiného vydání, pokud bylo neúspěšné vydání zachyceno z interaktivního vyhledávání" "AutoRedownloadFailedFromInteractiveSearchHelpText": "Automaticky vyhledat a pokusit se o stažení jiného vydání, pokud bylo neúspěšné vydání zachyceno z interaktivního vyhledávání",
"SelectDropdown": "'Vybrat...",
"CustomFilter": "Vlastní filtry",
"SelectQuality": "Vyberte kvalitu"
} }
+12 -6
View File
@@ -363,10 +363,10 @@
"TagIsNotUsedAndCanBeDeleted": "Tag bruges ikke og kan slettes", "TagIsNotUsedAndCanBeDeleted": "Tag bruges ikke og kan slettes",
"Tags": "Mærker", "Tags": "Mærker",
"Tasks": "Opgaver", "Tasks": "Opgaver",
"TestAll": "Test alle", "TestAll": "Afprøv alle",
"TestAllClients": "Test alle klienter", "TestAllClients": "Afprøv alle klienter",
"TestAllIndexers": "Test alle indeksører", "TestAllIndexers": "Afprøv alle indeks",
"TestAllLists": "Test alle lister", "TestAllLists": "Afprøv alle lister",
"ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Dette gælder for alle indeksører. Følg de regler, der er angivet af dem", "ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Dette gælder for alle indeksører. Følg de regler, der er angivet af dem",
"TimeFormat": "Tidsformat", "TimeFormat": "Tidsformat",
"Title": "Titel", "Title": "Titel",
@@ -542,7 +542,7 @@
"Yesterday": "I går", "Yesterday": "I går",
"RestartRequiredHelpTextWarning": "Kræver genstart for at træde i kraft", "RestartRequiredHelpTextWarning": "Kræver genstart for at træde i kraft",
"AddList": "Tilføj Liste", "AddList": "Tilføj Liste",
"Test": "Prøve", "Test": "Afprøv",
"RenameFiles": "Omdøb filer", "RenameFiles": "Omdøb filer",
"ManualImportSelectEdition": "Manuel import - Vælg film", "ManualImportSelectEdition": "Manuel import - Vælg film",
"ImportListExclusions": "Slet udelukkelse af importliste", "ImportListExclusions": "Slet udelukkelse af importliste",
@@ -638,5 +638,11 @@
"ExtraFileExtensionsHelpTextsExamples": "Eksempler: '.sub, .nfo' eller 'sub, nfo'", "ExtraFileExtensionsHelpTextsExamples": "Eksempler: '.sub, .nfo' eller 'sub, nfo'",
"AutoRedownloadFailed": "Download fejlede", "AutoRedownloadFailed": "Download fejlede",
"SourceTitle": "Kildetitel", "SourceTitle": "Kildetitel",
"RemoveQueueItemConfirmation": "Er du sikker på, at du vil fjerne {0} element {1} fra køen?" "RemoveQueueItemConfirmation": "Er du sikker på, at du vil fjerne {0} element {1} fra køen?",
"ImportLists": "Lister",
"ListsSettingsSummary": "Lister",
"CustomFilter": "Bruger Tilpassede Filtere",
"SelectDropdown": "'Vælg...",
"SelectQuality": "Vælg Kvalitet",
"ApplyChanges": "Anvend ændringer"
} }
+10 -2
View File
@@ -505,7 +505,7 @@
"ThisCannotBeCancelled": "Nach dem Start kann dies nicht mehr abgebrochen werden ohne alle Indexer zu deaktivieren.", "ThisCannotBeCancelled": "Nach dem Start kann dies nicht mehr abgebrochen werden ohne alle Indexer zu deaktivieren.",
"UnselectAll": "Alle abwählen", "UnselectAll": "Alle abwählen",
"UpdateSelected": "Auswahl aktualisieren", "UpdateSelected": "Auswahl aktualisieren",
"Wanted": " Gesucht", "Wanted": "Gesucht",
"CreateEmptyAuthorFolders": "Leere Filmordner erstellen", "CreateEmptyAuthorFolders": "Leere Filmordner erstellen",
"All": "Alle", "All": "Alle",
"Country": "Land", "Country": "Land",
@@ -990,5 +990,13 @@
"AutoAdd": "Automatisch hinzufügen", "AutoAdd": "Automatisch hinzufügen",
"WouldYouLikeToRestoreBackup": "Möchten Sie die Sicherung „{name}“ wiederherstellen?", "WouldYouLikeToRestoreBackup": "Möchten Sie die Sicherung „{name}“ wiederherstellen?",
"Unmonitored": "Nicht beobachtet", "Unmonitored": "Nicht beobachtet",
"Retention": "Aufbewahrung ( Retention )" "Retention": "Aufbewahrung ( Retention )",
"ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern",
"BlocklistAndSearch": "Sperrliste und Suche",
"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",
"ChangeCategory": "Kategorie wechseln"
} }
@@ -145,6 +145,7 @@
"ChownGroupHelpText": "Group name or gid. Use gid for remote file systems.", "ChownGroupHelpText": "Group name or gid. Use gid for remote file systems.",
"ChownGroupHelpTextWarning": "This only works if the user running Readarr is the owner of the file. It's better to ensure the download client uses the same group as Readarr.", "ChownGroupHelpTextWarning": "This only works if the user running Readarr is the owner of the file. It's better to ensure the download client uses the same group as Readarr.",
"Clear": "Clear", "Clear": "Clear",
"ClickToChangeIndexerFlags": "Click to change indexer flags",
"ClickToChangeQuality": "Click to change quality", "ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group", "ClickToChangeReleaseGroup": "Click to change release group",
"ClientPriority": "Client Priority", "ClientPriority": "Client Priority",
@@ -192,6 +193,7 @@
"CustomFormatScore": "Custom Format Score", "CustomFormatScore": "Custom Format Score",
"CustomFormatSettings": "Custom Format Settings", "CustomFormatSettings": "Custom Format Settings",
"CustomFormats": "Custom Formats", "CustomFormats": "Custom Formats",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationRegularExpression": "Regular Expression", "CustomFormatsSpecificationRegularExpression": "Regular Expression",
"CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive", "CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive",
"CutoffFormatScoreHelpText": "Once this custom format score is reached Readarr will no longer grab book releases", "CutoffFormatScoreHelpText": "Once this custom format score is reached Readarr will no longer grab book releases",
@@ -439,6 +441,7 @@
"Indexer": "Indexer", "Indexer": "Indexer",
"IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {0}.", "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {0}.",
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer", "IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
"IndexerFlags": "Indexer Flags",
"IndexerIdHelpText": "Specify what indexer the profile applies to", "IndexerIdHelpText": "Specify what indexer the profile applies to",
"IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed", "IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed",
"IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}", "IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}",
@@ -735,6 +738,7 @@
"RefreshBook": "Refresh Book", "RefreshBook": "Refresh Book",
"RefreshInformation": "Refresh information", "RefreshInformation": "Refresh information",
"RefreshInformationAndScanDisk": "Refresh information and scan disk", "RefreshInformationAndScanDisk": "Refresh information and scan disk",
"Rejections": "Rejections",
"ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Readarr release branch, you will not receive updates", "ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Readarr release branch, you will not receive updates",
"ReleaseDate": "Release Date", "ReleaseDate": "Release Date",
"ReleaseGroup": "Release Group", "ReleaseGroup": "Release Group",
@@ -853,6 +857,7 @@
"SelectBook": "Select Book", "SelectBook": "Select Book",
"SelectDropdown": "Select...", "SelectDropdown": "Select...",
"SelectEdition": "Select Edition", "SelectEdition": "Select Edition",
"SelectIndexerFlags": "Select Indexer Flags",
"SelectQuality": "Select Quality", "SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group", "SelectReleaseGroup": "Select Release Group",
"SelectedCountAuthorsSelectedInterp": "{0} Author(s) Selected", "SelectedCountAuthorsSelectedInterp": "{0} Author(s) Selected",
@@ -862,6 +867,7 @@
"Series": "Series", "Series": "Series",
"SeriesNumber": "Series Number", "SeriesNumber": "Series Number",
"SeriesTotal": "Series ({0})", "SeriesTotal": "Series ({0})",
"SetIndexerFlags": "Set Indexer Flags",
"SetPermissions": "Set Permissions", "SetPermissions": "Set Permissions",
"SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?", "SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?",
"SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.", "SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.",
+153 -109
View File
@@ -2,14 +2,14 @@
"ApiKeyHelpTextWarning": "Requiere reiniciar para que surta efecto", "ApiKeyHelpTextWarning": "Requiere reiniciar para que surta efecto",
"DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{0}'?", "DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{0}'?",
"LoadingBooksFailed": "La carga de los archivos ha fallado", "LoadingBooksFailed": "La carga de los archivos ha fallado",
"ProxyUsernameHelpText": "Tienes que introducir tu nombre de usuario y contraseña sólo si son requeridos. Si no, déjalos vacios.", "ProxyUsernameHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.",
"SslPortHelpTextWarning": "Requiere reiniciar para que surta efecto", "SslPortHelpTextWarning": "Requiere reiniciar para que surta efecto",
"SslCertPathHelpTextWarning": "Requiere reiniciar para que surta efecto", "SslCertPathHelpTextWarning": "Requiere reiniciar para que surta efecto",
"UnableToLoadMetadataProfiles": "No se pueden cargar los Perfiles de Retraso", "UnableToLoadMetadataProfiles": "No se pueden cargar los Perfiles de Retraso",
"BookIsDownloading": "Le película está descargando", "BookIsDownloading": "Le película está descargando",
"Group": "Grupo", "Group": "Grupo",
"MIA": "MIA", "MIA": "MIA",
"ShowDateAdded": "Mostrar Fecha de adido", "ShowDateAdded": "Mostrar fecha de adición",
"Tags": "Etiquetas", "Tags": "Etiquetas",
"60MinutesSixty": "60 Minutos: {0}", "60MinutesSixty": "60 Minutos: {0}",
"APIKey": "Clave API", "APIKey": "Clave API",
@@ -223,10 +223,10 @@
"OnHealthIssueHelpText": "En Problema de Salud", "OnHealthIssueHelpText": "En Problema de Salud",
"OnRenameHelpText": "Al Renombrar", "OnRenameHelpText": "Al Renombrar",
"OnUpgradeHelpText": "Al Mejorar La Calidad", "OnUpgradeHelpText": "Al Mejorar La Calidad",
"OpenBrowserOnStart": "Abrir navegador al arrancar", "OpenBrowserOnStart": "Abrir navegador al inicio",
"Options": "Opciones", "Options": "Opciones",
"Original": "Original", "Original": "Original",
"Overview": "Resumen", "Overview": "Vista general",
"PackageVersion": "Versión del paquete", "PackageVersion": "Versión del paquete",
"PageSize": "Tamaño de Página", "PageSize": "Tamaño de Página",
"PageSizeHelpText": "Número de elementos por página", "PageSizeHelpText": "Número de elementos por página",
@@ -235,24 +235,24 @@
"Permissions": "Permisos", "Permissions": "Permisos",
"Port": "Puerto", "Port": "Puerto",
"PortHelpTextWarning": "Requiere reiniciar para que surta efecto", "PortHelpTextWarning": "Requiere reiniciar para que surta efecto",
"PortNumber": "Número de Puerto", "PortNumber": "Número de puerto",
"PosterSize": "Tamaño del Poster", "PosterSize": "Tamaño dester",
"PreviewRename": "Previsualizar Renombrado", "PreviewRename": "Previsualizar renombrado",
"Profiles": "Perfiles", "Profiles": "Perfiles",
"Proper": "Apropiado", "Proper": "Proper",
"PropersAndRepacks": "Propers y Repacks", "PropersAndRepacks": "Propers y Repacks",
"Protocol": "Protocolo", "Protocol": "Protocolo",
"ProtocolHelpText": "Elige qué protocolo(s) se usará y cual será el preferido cuando haya que elegir entre lanzamientos iguales", "ProtocolHelpText": "Elige qué protocolo(s) usar y cuál se prefiere cuando se elige entre lanzamientos equivalentes",
"Proxy": "Proxy", "Proxy": "Proxy",
"ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como wildcard para subdominios", "ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios",
"ProxyPasswordHelpText": "Tienes que introducir tu nombre de usuario y contraseña sólo si son requeridos. Si no, déjalos vacios.", "ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.",
"ProxyType": "Tipo de Proxy", "ProxyType": "Tipo de proxy",
"PublishedDate": "Fecha de Publicación", "PublishedDate": "Fecha de publicación",
"Quality": "Calidad", "Quality": "Calidad",
"QualityDefinitions": "Definiciones de Calidad", "QualityDefinitions": "Definiciones de calidad",
"QualityProfile": "Perfil de Calidad", "QualityProfile": "Perfil de calidad",
"QualityProfiles": "Perfiles de Calidad", "QualityProfiles": "Perfiles de calidad",
"QualitySettings": "Ajustes de Calidad", "QualitySettings": "Opciones de calidad",
"Queue": "Cola", "Queue": "Cola",
"RSSSync": "Sincronizar RSS", "RSSSync": "Sincronizar RSS",
"RSSSyncInterval": "Intervalo de Sincronización de RSS", "RSSSyncInterval": "Intervalo de Sincronización de RSS",
@@ -264,85 +264,85 @@
"RecycleBinCleanupDaysHelpText": "Ajustar a 0 para desactivar la limpieza automática", "RecycleBinCleanupDaysHelpText": "Ajustar a 0 para desactivar la limpieza automática",
"RecycleBinCleanupDaysHelpTextWarning": "Los archivos en la papelera de reciclaje más antiguos que el número de días seleccionado serán limpiados automáticamente", "RecycleBinCleanupDaysHelpTextWarning": "Los archivos en la papelera de reciclaje más antiguos que el número de días seleccionado serán limpiados automáticamente",
"RecycleBinHelpText": "Los archivos iran aquí una vez se hayan borrado en vez de ser borrados permanentemente", "RecycleBinHelpText": "Los archivos iran aquí una vez se hayan borrado en vez de ser borrados permanentemente",
"RecyclingBin": "Papelera de Reciclaje", "RecyclingBin": "Papelera de reciclaje",
"RecyclingBinCleanup": "Limpieza de Papelera de Reciclaje", "RecyclingBinCleanup": "Limpieza de la papelera de reciclaje",
"Redownload": "Volver a descargar", "Redownload": "Volver a descargar",
"Refresh": "Actualizar", "Refresh": "Actualizar",
"RefreshInformationAndScanDisk": "Actualizar la información al escanear el disco", "RefreshInformationAndScanDisk": "Actualizar la información al escanear el disco",
"ReleaseDate": "Fechas de Estreno", "ReleaseDate": "Fechas de Estreno",
"ReleaseGroup": "Grupo de Estreno", "ReleaseGroup": "Grupo de lanzamiento",
"ReleaseRejected": "Lanzamiento Rechazado", "ReleaseRejected": "Lanzamiento rechazado",
"ReleaseWillBeProcessedInterp": "El lanzamiento será procesado {0}", "ReleaseWillBeProcessedInterp": "El lanzamiento será procesado {0}",
"Reload": "Recargar", "Reload": "Recargar",
"RemotePathMappings": "Mapeados de Rutas Remotas", "RemotePathMappings": "Mapeos de ruta remota",
"Remove": "Eliminar", "Remove": "Eliminar",
"RemoveCompletedDownloadsHelpText": "Eliminar las descargas ya importadas del historial del gestor de descargas", "RemoveCompletedDownloadsHelpText": "Elimina las descargas importadas desde el historial del cliente de descarga",
"RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas del historial del gestor de descargas", "RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas desde el historial del cliente de descarga",
"RemoveFilter": "Eliminar filtro", "RemoveFilter": "Eliminar filtro",
"RemoveFromDownloadClient": "Eliminar del Gestor de Descargas", "RemoveFromDownloadClient": "Eliminar del cliente de descarga",
"RemoveFromQueue": "Eliminar de la cola", "RemoveFromQueue": "Eliminar de la cola",
"RemoveHelpTextWarning": "Eliminar borrará la descarga y el/los fichero(s) del gestor de descargas.", "RemoveHelpTextWarning": "Eliminar borrará la descarga y el/los fichero(s) del gestor de descargas.",
"RemoveSelected": "Borrar Seleccionados", "RemoveSelected": "Eliminar seleccionado",
"RemoveTagExistingTag": "Etiqueta existente", "RemoveTagExistingTag": "Etiqueta existente",
"RemoveTagRemovingTag": "Eliminando etiqueta", "RemoveTagRemovingTag": "Eliminando etiqueta",
"RemovedFromTaskQueue": "Eliminar de la cola de tareas", "RemovedFromTaskQueue": "Eliminar de la cola de tareas",
"RenameBooksHelpText": "Radarr usará el nombre del archivo si el renombrado está deshabilitado", "RenameBooksHelpText": "Radarr usará el nombre del archivo si el renombrado está deshabilitado",
"Reorder": "Reordenar", "Reorder": "Reordenar",
"ReplaceIllegalCharacters": "Reemplazar Caracteres Ilegales", "ReplaceIllegalCharacters": "Reemplazar caracteres ilegales",
"RequiredHelpText": "El comunicado debe contener al menos uno de estos términos (no distingue entre mayúsculas y minúsculas)", "RequiredHelpText": "El comunicado debe contener al menos uno de estos términos (no distingue entre mayúsculas y minúsculas)",
"RequiredPlaceHolder": "Añadir nueva restricción", "RequiredPlaceHolder": "Añadir nueva restricción",
"RescanAfterRefreshHelpTextWarning": "Radarr no detectará los cambios automáticamente en los ficheros si no se ajusta a 'Siempre'", "RescanAfterRefreshHelpTextWarning": "Radarr no detectará los cambios automáticamente en los ficheros si no se ajusta a 'Siempre'",
"RescanAuthorFolderAfterRefresh": "Reescanear la Carpeta de Películas después de Actualizar", "RescanAuthorFolderAfterRefresh": "Reescanear la Carpeta de Películas después de Actualizar",
"Reset": "Reiniciar", "Reset": "Reiniciar",
"ResetAPIKey": "Reajustar API", "ResetAPIKey": "Restablecer clave API",
"ResetAPIKeyMessageText": "¿Está seguro de que desea restablecer su clave API?", "ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?",
"Restart": "Reiniciar", "Restart": "Reiniciar",
"RestartNow": "Reiniciar Ahora", "RestartNow": "Reiniciar ahora",
"RestartReadarr": "Reiniciar Radarr", "RestartReadarr": "Reiniciar Radarr",
"Restore": "Restaurar", "Restore": "Restaurar",
"RestoreBackup": "Recuperar Backup", "RestoreBackup": "Restaurar copia de seguridad",
"Result": "Resultado", "Result": "Resultado",
"Retention": "Retención", "Retention": "Retención",
"RetentionHelpText": "Sólo Usenet: Ajustar a cero para retención ilimitada", "RetentionHelpText": "Solo usenet: Establece a cero para establecer una retención ilimitada",
"RetryingDownloadInterp": "Re-intentando descarga {0} en {1}", "RetryingDownloadInterp": "Re-intentando descarga {0} en {1}",
"RootFolder": "Carpeta de Origen", "RootFolder": "Carpeta raíz",
"RootFolders": "Carpetas de Origen", "RootFolders": "Carpetas raíz",
"RssSyncIntervalHelpText": "Intervalo en minutos. Ajustar a cero para inhabilitar (esto dentendrá toda captura de estrenos automática)", "RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)",
"SSLCertPassword": "Contraseña del Certificado SSL", "SSLCertPassword": "Contraseña del Certificado SSL",
"SSLCertPath": "Ruta del Certificado SSL", "SSLCertPath": "Ruta del Certificado SSL",
"SSLPort": "Puerto SSL", "SSLPort": "Puerto SSL",
"Scheduled": "Programado", "Scheduled": "Programado",
"ScriptPath": "Ruta del Script", "ScriptPath": "Ruta del script",
"Search": "Buscar", "Search": "Buscar",
"SearchAll": "Buscar Todas", "SearchAll": "Buscar todo",
"SearchForMissing": "Buscar faltantes", "SearchForMissing": "Buscar perdidos",
"SearchSelected": "Buscar Seleccionadas", "SearchSelected": "Buscar seleccionados",
"Security": "Seguridad", "Security": "Seguridad",
"SendAnonymousUsageData": "Enviar Datos de Uso Anónimamente", "SendAnonymousUsageData": "Enviar datos de uso anónimos",
"SetPermissions": "Ajustar Permisos", "SetPermissions": "Establecer permisos",
"SetPermissionsLinuxHelpText": "Debe chmod ser ejecutado una vez los archivos hayan sido importados/renombrados?", "SetPermissionsLinuxHelpText": "¿Debería ejecutarse chmod cuando los archivos son importados/renombrados?",
"SetPermissionsLinuxHelpTextWarning": "Si no estas seguro de lo que hacen estos ajustes, no los modifiques.", "SetPermissionsLinuxHelpTextWarning": "Si no estás seguro qué configuraciones hacer, no las cambies.",
"Settings": "Ajustes", "Settings": "Ajustes",
"ShortDateFormat": "Formato Corto de Fecha", "ShortDateFormat": "Formato de fecha breve",
"ShowCutoffUnmetIconHelpText": "Mostrar el icono para los ficheros cuando no se ha alcanzado el corte", "ShowCutoffUnmetIconHelpText": "Mostrar el icono para los ficheros cuando no se ha alcanzado el corte",
"ShowMonitored": "Mostrar Monitoreadas", "ShowMonitored": "Mostrar monitorizado",
"ShowMonitoredHelpText": "Mostrar el estado de monitoreo debajo del poster", "ShowMonitoredHelpText": "Muestra el estado monitorizado bajo el póster",
"ShowPath": "Mostrar Ruta", "ShowPath": "Mostrar ruta",
"ShowQualityProfile": "Mostrar Perfil de Calidad", "ShowQualityProfile": "Mostrar perfil de calidad",
"ShowQualityProfileHelpText": "Mostrar el perfil de calidad debajo del poster", "ShowQualityProfileHelpText": "Muestra el perfil de calidad bajo el póster",
"ShowRelativeDates": "Mostrar Fechas Relativas", "ShowRelativeDates": "Mostrar fechas relativas",
"ShowRelativeDatesHelpText": "Mostrar fechas relativas (Hoy/Ayer/etc) o absolutas", "ShowRelativeDatesHelpText": "Muestra fechas absolutas o relativas (Hoy/Ayer/etc)",
"ShowSearch": "Mostrar Búsqueda", "ShowSearch": "Mostrar búsqueda",
"ShowSearchActionHelpText": "Mostrar botón de búsqueda al pasar el cursor por encima", "ShowSearchActionHelpText": "Mostrar botón de búsqueda al pasar el cursor por encima",
"ShowSizeOnDisk": "Mostrar Tamaño en Disco", "ShowSizeOnDisk": "Mostrar tamaño en disco",
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Mostrado sobre cada columna cuando la vista activa es semana", "ShownAboveEachColumnWhenWeekIsTheActiveView": "Mostrado sobre cada columna cuando la vista activa es semana",
"Size": " Tamaño", "Size": " Tamaño",
"SkipFreeSpaceCheck": "Saltarse Comprobación de Espacio Disponible", "SkipFreeSpaceCheck": "Saltar comprobación de espacio libre",
"SkipFreeSpaceCheckWhenImportingHelpText": "Usar cuando Radarr no pueda detectar el espacio disponible en la carpeta de películas", "SkipFreeSpaceCheckWhenImportingHelpText": "Usar cuando Radarr no pueda detectar el espacio disponible en la carpeta de películas",
"SorryThatAuthorCannotBeFound": "Lo siento, no he encontrado esa película.", "SorryThatAuthorCannotBeFound": "Lo siento, no he encontrado esa película.",
"SorryThatBookCannotBeFound": "Lo siento, no he encontrado esa película.", "SorryThatBookCannotBeFound": "Lo siento, no he encontrado esa película.",
"Source": "Fuente", "Source": "Fuente",
"SourcePath": "Ruta de Origen", "SourcePath": "Ruta de la fuente",
"SslCertPasswordHelpText": "Contraseña para el archivo pfx", "SslCertPasswordHelpText": "Contraseña para el archivo pfx",
"SslCertPasswordHelpTextWarning": "Requiere reiniciar para que surta efecto", "SslCertPasswordHelpTextWarning": "Requiere reiniciar para que surta efecto",
"SslCertPathHelpText": "Ruta al archivo pfx", "SslCertPathHelpText": "Ruta al archivo pfx",
@@ -360,17 +360,17 @@
"SupportsSearchvalueWillBeUsedWhenInteractiveSearchIsUsed": "Se usará cuando se utilice la búsqueda interactiva", "SupportsSearchvalueWillBeUsedWhenInteractiveSearchIsUsed": "Se usará cuando se utilice la búsqueda interactiva",
"TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada", "TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada",
"Tasks": "Tareas", "Tasks": "Tareas",
"TestAll": "Testear Todo", "TestAll": "Probar todo",
"TestAllClients": "Comprobar Todos los Gestores", "TestAllClients": "Probar todos los clientes",
"TestAllIndexers": "Comprobar Todos los Indexers", "TestAllIndexers": "Probar todos los indexadores",
"TestAllLists": "Comprobar Todas las Listas", "TestAllLists": "Probar todas las listas",
"ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Se aplicará a todos los indexers, por favor sigue las reglas de los mismos", "ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Se aplicará a todos los indexers, por favor sigue las reglas de los mismos",
"TimeFormat": "Formato de Hora", "TimeFormat": "Formato de hora",
"Title": "Título", "Title": "Título",
"TorrentDelay": "Retraso del Torrent", "TorrentDelay": "Retraso de torrent",
"TorrentDelayHelpText": "Retraso en minutos a esperar antes de descargar un torrent", "TorrentDelayHelpText": "Retraso en minutos a esperar antes de capturar un torrent",
"Torrents": "Torrents", "Torrents": "Torrents",
"TotalFileSize": "Tamaño Total del Archivo", "TotalFileSize": "Tamaño total de archivo",
"UILanguage": "Lenguaje de UI", "UILanguage": "Lenguaje de UI",
"UILanguageHelpText": "Lenguaje que Radarr usara para el UI", "UILanguageHelpText": "Lenguaje que Radarr usara para el UI",
"UILanguageHelpTextWarning": "Recargar el Navegador", "UILanguageHelpTextWarning": "Recargar el Navegador",
@@ -385,7 +385,7 @@
"UnableToAddANewQualityProfilePleaseTryAgain": "No se ha podido añadir un nuevo perfil de calidad, prueba otra vez.", "UnableToAddANewQualityProfilePleaseTryAgain": "No se ha podido añadir un nuevo perfil de calidad, prueba otra vez.",
"UnableToAddANewRemotePathMappingPleaseTryAgain": "No se ha podido añadir una nueva ruta remota, prueba otra vez.", "UnableToAddANewRemotePathMappingPleaseTryAgain": "No se ha podido añadir una nueva ruta remota, prueba otra vez.",
"UnableToAddANewRootFolderPleaseTryAgain": "No se ha podido añadir un nuevo formato propio, prueba otra vez.", "UnableToAddANewRootFolderPleaseTryAgain": "No se ha podido añadir un nuevo formato propio, prueba otra vez.",
"UnableToLoadBackups": "No se han podido cargar las copias de seguridad", "UnableToLoadBackups": "No se pudo cargar las copias de seguridad",
"UnableToLoadDelayProfiles": "No se pueden cargar los Perfiles de Retraso", "UnableToLoadDelayProfiles": "No se pueden cargar los Perfiles de Retraso",
"UnableToLoadDownloadClientOptions": "No se han podido cargar las opciones del gestor de descargas", "UnableToLoadDownloadClientOptions": "No se han podido cargar las opciones del gestor de descargas",
"UnableToLoadDownloadClients": "No se puden cargar los gestores de descargas", "UnableToLoadDownloadClients": "No se puden cargar los gestores de descargas",
@@ -408,27 +408,27 @@
"UnableToLoadTags": "No se pueden cargar las Etiquetas", "UnableToLoadTags": "No se pueden cargar las Etiquetas",
"UnableToLoadTheCalendar": "No se ha podido cargar el calendario", "UnableToLoadTheCalendar": "No se ha podido cargar el calendario",
"UnableToLoadUISettings": "No se han podido cargar los ajustes de UI", "UnableToLoadUISettings": "No se han podido cargar los ajustes de UI",
"Ungroup": "Desagrupar", "Ungroup": "Sin agrupar",
"Unmonitored": "Sin monitorizar", "Unmonitored": "Sin monitorizar",
"UnmonitoredHelpText": "Incluir las peliculas no monitoreadas en el feed de iCal", "UnmonitoredHelpText": "Incluir las peliculas no monitoreadas en el feed de iCal",
"UpdateAll": "Actualizar Todo", "UpdateAll": "Actualizar Todo",
"UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Se podrán instalar desde Sistema: Actualizaciones también", "UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones",
"UpdateMechanismHelpText": "Usar el actualizador de Radarr o un script", "UpdateMechanismHelpText": "Usar el actualizador de Radarr o un script",
"UpdateScriptPathHelpText": "Ruta del script propio que toma el paquete de actualización y se encarga del proceso de actualización restante", "UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización",
"Updates": "Actualizaciones", "Updates": "Actualizaciones",
"UpgradeAllowedHelpText": "Si está desactivado las calidades no serán actualizadas", "UpgradeAllowedHelpText": "Si está desactivado las calidades no serán actualizadas",
"Uptime": "Tiempo de actividad", "Uptime": "Tiempo de actividad",
"UrlBaseHelpTextWarning": "Requiere reiniciar para que surta efecto", "UrlBaseHelpTextWarning": "Requiere reiniciar para que surta efecto",
"UseHardlinksInsteadOfCopy": "Usar Hardlinks en vez de Copia", "UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar",
"UseProxy": "Usar el Proxy", "UseProxy": "Usar proxy",
"Usenet": "Usenet", "Usenet": "Usenet",
"UsenetDelay": "Retraso de Usenet", "UsenetDelay": "Retraso de usenet",
"UsenetDelayHelpText": "Retraso en minutos a esperar antes de descargar un lanzamiento de Usenet", "UsenetDelayHelpText": "Retraso en minutos a esperar antes de capturar un lanzamiento desde usenet",
"Username": "Nombre de usuario", "Username": "Usuario",
"UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "Qué rama usar para actualizar Radarr", "UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "Qué rama usar para actualizar Radarr",
"UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "Rama usada por el mecanismo de actualización externo", "UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "Rama usada por el mecanismo de actualización externo",
"Version": "Versión", "Version": "Versión",
"WeekColumnHeader": "Encabezado de la columna semanal", "WeekColumnHeader": "Cabecera de columna de semana",
"Year": "Año", "Year": "Año",
"YesCancel": "Sí, Cancelar", "YesCancel": "Sí, Cancelar",
"20MinutesTwenty": "20 Minutos: {0}", "20MinutesTwenty": "20 Minutos: {0}",
@@ -437,16 +437,16 @@
"ReplaceIllegalCharactersHelpText": "Reemplazar caracteres ilegales. Si está desactivado, Radarr los eliminará si no", "ReplaceIllegalCharactersHelpText": "Reemplazar caracteres ilegales. Si está desactivado, Radarr los eliminará si no",
"Actions": "Acciones", "Actions": "Acciones",
"Today": "Hoy", "Today": "Hoy",
"ReleaseTitle": "Título del Estreno", "ReleaseTitle": "Título de lanzamiento",
"Progress": "Progreso", "Progress": "Progreso",
"Tomorrow": "mañana", "Tomorrow": "Mañana",
"OutputPath": "Ruta de Output", "OutputPath": "Ruta de salida",
"BookAvailableButMissing": "Película Disponible pero Ausente", "BookAvailableButMissing": "Película Disponible pero Ausente",
"NotAvailable": "No Disponible", "NotAvailable": "No Disponible",
"NotMonitored": "No Monitoreadas", "NotMonitored": "No Monitoreadas",
"ShowBookTitleHelpText": "Mostrar el título de la película debajo del poster", "ShowBookTitleHelpText": "Mostrar el título de la película debajo del poster",
"ShowReleaseDate": "Mostrar fecha de lanzamiento", "ShowReleaseDate": "Mostrar fecha de lanzamiento",
"ShowTitle": "Mostrar Título", "ShowTitle": "Mostrar título",
"TheAuthorFolderAndAllOfItsContentWillBeDeleted": "Se eliminará la carpeta de películas '{0}' y todo su contenido.", "TheAuthorFolderAndAllOfItsContentWillBeDeleted": "Se eliminará la carpeta de películas '{0}' y todo su contenido.",
"Component": "Componente", "Component": "Componente",
"RemoveFromBlocklist": "Eliminar de lista de bloqueados", "RemoveFromBlocklist": "Eliminar de lista de bloqueados",
@@ -457,13 +457,13 @@
"Blocklist": "Lista de bloqueos", "Blocklist": "Lista de bloqueos",
"BlocklistRelease": "Lista de bloqueos de lanzamiento", "BlocklistRelease": "Lista de bloqueos de lanzamiento",
"CreateEmptyAuthorFolders": "Crear carpetas de películas vacías", "CreateEmptyAuthorFolders": "Crear carpetas de películas vacías",
"SelectAll": "Seleccionar Todas", "SelectAll": "Seleccionar todo",
"SelectedCountBooksSelectedInterp": "{0} Película(s) Seleccionada(s)", "SelectedCountBooksSelectedInterp": "{0} Película(s) Seleccionada(s)",
"ThisCannotBeCancelled": "Esto no puede ser cancelado una vez iniciado sin deshabilitar todos sus indexadores.", "ThisCannotBeCancelled": "Esto no puede ser cancelado una vez iniciado sin deshabilitar todos sus indexadores.",
"All": "Todo", "All": "Todo",
"RescanAfterRefreshHelpText": "Reescanear la carpeta de películas después de actualizar la película", "RescanAfterRefreshHelpText": "Reescanear la carpeta de películas después de actualizar la película",
"ShowUnknownAuthorItems": "Mostrar Elementos Desconocidos", "ShowUnknownAuthorItems": "Mostrar Elementos Desconocidos",
"UnselectAll": "Deseleccionar Todo", "UnselectAll": "Desmarcar todo",
"UpdateSelected": "Actualizar Seleccionadas", "UpdateSelected": "Actualizar Seleccionadas",
"Wanted": "Buscado", "Wanted": "Buscado",
"AllAuthorBooks": "Todos los libros del autor", "AllAuthorBooks": "Todos los libros del autor",
@@ -485,9 +485,9 @@
"Yesterday": "Ayer", "Yesterday": "Ayer",
"UpdateAvailable": "La nueva actualización está disponible", "UpdateAvailable": "La nueva actualización está disponible",
"Duration": "Duración", "Duration": "Duración",
"AppDataLocationHealthCheckMessage": "No será posible actualizar para prevenir la eliminación de AppData al Actualizar", "AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar",
"Lists": "Listas", "Lists": "Listas",
"SizeLimit": "Tamaño límite", "SizeLimit": "Límite de tamaño",
"IndexerJackettAll": "Indexadores que utilizan el Endpoint Jackett 'all' no están soportados: {0}", "IndexerJackettAll": "Indexadores que utilizan el Endpoint Jackett 'all' no están soportados: {0}",
"RemotePathMappingCheckLocalFolderMissing": "El cliente de descarga remota {0} coloca las descargas en {1} pero este directorio no parece existir. Probablemente falta o el mapeo de la ruta remota es incorrecto.", "RemotePathMappingCheckLocalFolderMissing": "El cliente de descarga remota {0} coloca las descargas en {1} pero este directorio no parece existir. Probablemente falta o el mapeo de la ruta remota es incorrecto.",
"RemotePathMappingCheckLocalWrongOSPath": "El cliente de descarga local {0} coloca las descargas en {1} pero ésta no es una ruta válida {2}. Revise la configuración de su cliente de descarga.", "RemotePathMappingCheckLocalWrongOSPath": "El cliente de descarga local {0} coloca las descargas en {1} pero ésta no es una ruta válida {2}. Revise la configuración de su cliente de descarga.",
@@ -498,7 +498,7 @@
"FileWasDeletedByViaUI": "El archivo se eliminó a través de la interfaz de usuario", "FileWasDeletedByViaUI": "El archivo se eliminó a través de la interfaz de usuario",
"AllowFingerprinting": "Permitir impresión digital", "AllowFingerprinting": "Permitir impresión digital",
"DownloadClientsSettingsSummary": "Clientes de descarga, manejo de descarga y mapeo de rutas remotas", "DownloadClientsSettingsSummary": "Clientes de descarga, manejo de descarga y mapeo de rutas remotas",
"OnHealthIssue": "En Problema de Salud", "OnHealthIssue": "Al haber un problema de salud",
"DeleteBookFileMessageText": "¿Seguro que quieres eliminar {0}?", "DeleteBookFileMessageText": "¿Seguro que quieres eliminar {0}?",
"HealthNoIssues": "No hay problemas con tu configuración", "HealthNoIssues": "No hay problemas con tu configuración",
"ImportListStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores", "ImportListStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores",
@@ -537,9 +537,9 @@
"OnBookFileDeleteForUpgrade": "En archivo de película Eliminar para actualizar", "OnBookFileDeleteForUpgrade": "En archivo de película Eliminar para actualizar",
"OnBookFileDeleteForUpgradeHelpText": "En archivo de película Eliminar para actualizar", "OnBookFileDeleteForUpgradeHelpText": "En archivo de película Eliminar para actualizar",
"OnBookFileDeleteHelpText": "Al eliminar archivo de película", "OnBookFileDeleteHelpText": "Al eliminar archivo de película",
"OnGrab": "Al Capturar", "OnGrab": "Al capturar",
"OnRename": "Al Renombrar", "OnRename": "Al renombrar",
"OnUpgrade": "Al Mejorar La Calidad", "OnUpgrade": "Al actualizar",
"ProxyCheckBadRequestMessage": "Fallo al comprobar el proxy. StatusCode: {0}", "ProxyCheckBadRequestMessage": "Fallo al comprobar el proxy. StatusCode: {0}",
"ProxyCheckFailedToTestMessage": "Fallo al comprobar el proxy: {0}", "ProxyCheckFailedToTestMessage": "Fallo al comprobar el proxy: {0}",
"QualitySettingsSummary": "Tamaños de calidad y nombrado", "QualitySettingsSummary": "Tamaños de calidad y nombrado",
@@ -574,10 +574,10 @@
"WriteTagsNo": "Nunca", "WriteTagsNo": "Nunca",
"FileWasDeletedByUpgrade": "Se eliminó el archivo para importar una actualización", "FileWasDeletedByUpgrade": "Se eliminó el archivo para importar una actualización",
"IndexersSettingsSummary": "Indexadores y restricciones de lanzamiento", "IndexersSettingsSummary": "Indexadores y restricciones de lanzamiento",
"RestartRequiredHelpTextWarning": "Requiere reiniciar para que surta efecto", "RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto",
"AddList": "Añadir Lista", "AddList": "Añadir Lista",
"RenameFiles": "Renombrar Archivos", "RenameFiles": "Renombrar archivos",
"Test": "Test", "Test": "Prueba",
"InstanceName": "Nombre de la Instancia", "InstanceName": "Nombre de la Instancia",
"InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog", "InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog",
"Database": "Base de datos", "Database": "Base de datos",
@@ -587,7 +587,7 @@
"ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento", "ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento",
"HardlinkCopyFiles": "Enlace permanente/Copiar archivos", "HardlinkCopyFiles": "Enlace permanente/Copiar archivos",
"MoveFiles": "Mover archivos", "MoveFiles": "Mover archivos",
"OnApplicationUpdate": "Al Actualizar La Aplicación", "OnApplicationUpdate": "Al actualizar la aplicación",
"OnApplicationUpdateHelpText": "Al Actualizar La Aplicación", "OnApplicationUpdateHelpText": "Al Actualizar La Aplicación",
"BypassIfHighestQuality": "Pasar sí es la calidad más alta", "BypassIfHighestQuality": "Pasar sí es la calidad más alta",
"CustomFormatScore": "Puntuación de Formato personalizado", "CustomFormatScore": "Puntuación de Formato personalizado",
@@ -602,17 +602,17 @@
"IncludeCustomFormatWhenRenamingHelpText": "Incluir en el formato de renombrado {Formatos Propios}", "IncludeCustomFormatWhenRenamingHelpText": "Incluir en el formato de renombrado {Formatos Propios}",
"MinFormatScoreHelpText": "Puntuación mínima del formato propio permitida para descargar", "MinFormatScoreHelpText": "Puntuación mínima del formato propio permitida para descargar",
"NegateHelpText": "Si se activa, el formato propio no se aplicará si esta condición {0} se iguala.", "NegateHelpText": "Si se activa, el formato propio no se aplicará si esta condición {0} se iguala.",
"ResetDefinitionTitlesHelpText": "Restablecer los títulos y valores de las definiciones", "ResetDefinitionTitlesHelpText": "Restablecer títulos de definición también como valores",
"ResetDefinitions": "Restablecer definiciones", "ResetDefinitions": "Restablecer definiciones",
"UnableToLoadCustomFormats": "No se pueden cargar los Formatos Propios", "UnableToLoadCustomFormats": "No se pueden cargar los Formatos Propios",
"Theme": "Tema", "Theme": "Tema",
"ThemeHelpText": "Cambia el tema de la interfaz de usuario de la aplicación. El tema \"automático\" utilizará el tema de tu sistema operativo para establecer el modo claro u oscuro. Inspirado por Theme.Park", "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",
"CustomFormatSettings": "Ajustes de Formatos Propios", "CustomFormatSettings": "Ajustes de Formatos Propios",
"CutoffFormatScoreHelpText": "Una vez alcanzada esta puntuación del formato propio Radarr dejará de descargar películas", "CutoffFormatScoreHelpText": "Una vez alcanzada esta puntuación del formato propio Radarr dejará de descargar películas",
"DeleteCustomFormatMessageText": "Seguro que quieres eliminar el indexer '{name}'?", "DeleteCustomFormatMessageText": "Seguro que quieres eliminar el indexer '{name}'?",
"ExportCustomFormat": "Exportar formato personalizado", "ExportCustomFormat": "Exportar formato personalizado",
"ResetTitles": "Restablecer títulos", "ResetTitles": "Restablecer títulos",
"UpgradesAllowed": "Mejoras permitidas", "UpgradesAllowed": "Actualizaciones permitidas",
"EnableRssHelpText": "Se utilizará cuando Radarr busque periódicamente publicaciones a través de RSS Sync", "EnableRssHelpText": "Se utilizará cuando Radarr busque periódicamente publicaciones a través de RSS Sync",
"IndexerTagsHelpText": "Solo utilizar este indexador para películas que coincidan con al menos una etiqueta. Déjelo en blanco para utilizarlo con todas las películas.", "IndexerTagsHelpText": "Solo utilizar este indexador para películas que coincidan con al menos una etiqueta. Déjelo en blanco para utilizarlo con todas las películas.",
"ImportListMissingRoot": "Falta la capeta raíz para las listas: {0}", "ImportListMissingRoot": "Falta la capeta raíz para las listas: {0}",
@@ -620,32 +620,32 @@
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador", "IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
"HiddenClickToShow": "Oculto, click para mostrar", "HiddenClickToShow": "Oculto, click para mostrar",
"HideAdvanced": "Ocultar avanzado", "HideAdvanced": "Ocultar avanzado",
"ShowAdvanced": "Mostrar Avanzado", "ShowAdvanced": "Mostrar avanzado",
"ShownClickToHide": "Mostrado, clic para ocultar", "ShownClickToHide": "Mostrado, haz clic para ocultar",
"ReplaceWithDash": "Reemplazar con Dash", "ReplaceWithDash": "Reemplazar con guion",
"ReplaceWithSpaceDash": "Reemplazar con Space Dash", "ReplaceWithSpaceDash": "Reemplazar por barra espaciadora",
"ReplaceWithSpaceDashSpace": "Reemplazar con Space Dash Space", "ReplaceWithSpaceDashSpace": "Reemplazar por espacio en la barra espaciadora",
"DeleteRemotePathMapping": "Borrar mapeo de ruta remota", "DeleteRemotePathMapping": "Borrar mapeo de ruta remota",
"BlocklistReleases": "Lista de bloqueos de lanzamientos", "BlocklistReleases": "Lista de bloqueos de lanzamientos",
"DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{0}'?", "DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{0}'?",
"Negated": "Anulado", "Negated": "Anulado",
"RemoveSelectedItem": "Eliminar el elemento seleccionado", "RemoveSelectedItem": "Eliminar elemento seleccionado",
"RemoveSelectedItemBlocklistMessageText": "¿Está seguro de que desea eliminar los elementos seleccionados de la lista negra?", "RemoveSelectedItemBlocklistMessageText": "¿Está seguro de que desea eliminar los elementos seleccionados de la lista negra?",
"RemoveSelectedItemQueueMessageText": "¿Está seguro de que desea eliminar el {0} elemento {1} de la cola?", "RemoveSelectedItemQueueMessageText": "¿Estás seguro que quieres eliminar 1 elemento de la cola?",
"RemoveSelectedItems": "Eliminar los elementos seleccionados", "RemoveSelectedItems": "Eliminar elementos seleccionados",
"RemoveSelectedItemsQueueMessageText": "¿Estás seguro de que quieres eliminar {0} elementos de la cola?", "RemoveSelectedItemsQueueMessageText": "¿Estás seguro de que quieres eliminar {0} elementos de la cola?",
"Required": "Necesario", "Required": "Solicitado",
"ResetQualityDefinitions": "Restablecer definiciones de calidad", "ResetQualityDefinitions": "Restablecer definiciones de calidad",
"ResetQualityDefinitionsMessageText": "¿Está seguro de que desea restablecer las definiciones de calidad?", "ResetQualityDefinitionsMessageText": "¿Estás seguro que quieres restablecer las definiciones de calidad?",
"BlocklistReleaseHelpText": "Evita que Radarr vuelva a capturar esta película automáticamente", "BlocklistReleaseHelpText": "Evita que Radarr vuelva a capturar esta película automáticamente",
"NoEventsFound": "Ningún evento encontrado", "NoEventsFound": "Ningún evento encontrado",
"ApplyTagsHelpTextHowToApplyAuthors": "Cómo añadir etiquetas a las películas seleccionadas", "ApplyTagsHelpTextHowToApplyAuthors": "Cómo añadir etiquetas a las películas seleccionadas",
"DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?", "DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?",
"Yes": "Sí", "Yes": "Sí",
"RedownloadFailed": "La descarga ha fallado", "RedownloadFailed": "La descarga ha fallado",
"RemoveCompleted": "Eliminación completada", "RemoveCompleted": "Eliminar completado",
"RemoveDownloadsAlert": "Los ajustes de eliminación se han trasladado a los ajustes individuales del cliente de descarga en la tabla anterior.", "RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.",
"RemoveFailed": "La eliminación falló", "RemoveFailed": "Fallo al eliminar",
"ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente", "ApplyTagsHelpTextAdd": "Añadir: Añade las etiquetas a la lista de etiquetas existente",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Cómo añadir etiquetas a los clientes de descargas seleccionados", "ApplyTagsHelpTextHowToApplyDownloadClients": "Cómo añadir etiquetas a los clientes de descargas seleccionados",
"ApplyTagsHelpTextHowToApplyImportLists": "Cómo añadir etiquetas a las listas de importación seleccionadas", "ApplyTagsHelpTextHowToApplyImportLists": "Cómo añadir etiquetas a las listas de importación seleccionadas",
@@ -661,7 +661,7 @@
"No": "No", "No": "No",
"NoChange": "Sin cambio", "NoChange": "Sin cambio",
"RemovingTag": "Eliminando etiqueta", "RemovingTag": "Eliminando etiqueta",
"SetTags": "Poner Etiquetas", "SetTags": "Establecer etiquetas",
"DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?", "DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?",
"ApplicationURL": "URL de la aplicación", "ApplicationURL": "URL de la aplicación",
"ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base", "ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base",
@@ -699,8 +699,8 @@
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.", "ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.",
"NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {0}", "NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {0}",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos", "NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos",
"ReleaseProfiles": "perfil de lanzamiento", "ReleaseProfiles": "Perfiles de lanzamiento",
"Small": "Pequeña", "Small": "Pequeño",
"DeleteImportList": "Eliminar Lista(s) de Importación", "DeleteImportList": "Eliminar Lista(s) de Importación",
"Large": "Grande", "Large": "Grande",
"Library": "Biblioteca", "Library": "Biblioteca",
@@ -801,5 +801,49 @@
"MediaManagementSettingsSummary": "Nombrado, opciones de gestión de archivos y carpetas raíz", "MediaManagementSettingsSummary": "Nombrado, opciones de gestión de archivos y carpetas raíz",
"MonitoringOptions": "Opciones de monitorización", "MonitoringOptions": "Opciones de monitorización",
"NoImportListsFound": "Ninguna lista de importación encontrada", "NoImportListsFound": "Ninguna lista de importación encontrada",
"Monitoring": "Monitorizando" "Monitoring": "Monitorizando",
"NoMissingItems": "No hay elementos faltantes",
"DefaultMetadataProfileIdHelpText": "Perfil de metadatos predeterminado para los artistas detectados en esta carpeta",
"MetadataProfileIdHelpText": "Los elementos de la lista del Perfil de Calidad se añadirán con",
"DefaultQualityProfileIdHelpText": "Perfil de calidad predeterminado para los artistas detectados en esta carpeta",
"ContinuingAllBooksDownloaded": "Continúa (Todas las pistas descargadas)",
"DataListMonitorAll": "Supervisar los artistas y todos los álbumes de cada artista incluido en la lista de importación",
"MetadataSettingsSummary": "Crea archivos de metadatos cuando los episodios son importados o las series son refrescadas",
"MonitoredAuthorIsUnmonitored": "El artista no está vigilado",
"SearchForAllCutoffUnmetBooks": "Buscar todos los episodios en Umbrales no alcanzados",
"ConsoleLogLevel": "Nivel de Registro de la Consola",
"DataMissingBooks": "Monitoriza episodios que no tienen archivos o que no se han emitido aún",
"EnabledHelpText": "Señalar para habilitar el perfil de lanzamiento",
"FilterAnalyticsEvents": "Filtrar Eventos Analíticos",
"FilterSentryEventsHelpText": "Filtrar eventos de error de usuario conocidos para que no se envíen como Análisis",
"RootFolderPathHelpText": "Los elementos de la lista de carpetas raíz se añadirán a",
"StatusEndedDeceased": "Fallecido",
"LogRotateHelpText": "Número máximo de archivos de registro que se guardan en la carpeta de registros",
"LogRotation": "Rotación de Registros",
"QualityProfileIdHelpText": "Los elementos de la lista del Perfil de Calidad se añadirán con",
"SelectDropdown": "Seleccionar...",
"CollapseMultipleBooksHelpText": "Colapsar varios álbumes que salen el mismo día",
"ContinuingNoAdditionalBooksAreExpected": "No se esperan álbumes adicionales",
"DefaultMonitorOptionHelpText": "Qué álbumes se deben supervisar en la adición inicial para los artistas detectados en esta carpeta",
"CustomFilter": "Filtros personalizados",
"LabelIsRequired": "Se requiere etiqueta",
"RemoveQueueItemConfirmation": "¿Estás seguro que quieres eliminar '{sourceTitle}' de la cola?",
"SelectQuality": "Seleccionar calidad",
"SelectReleaseGroup": "Seleccionar grupo de lanzamiento",
"ThereWasAnErrorLoadingThisItem": "Hubo un error cargando este elemento",
"ThereWasAnErrorLoadingThisPage": "Hubo un error cargando esta página",
"SourceTitle": "Título de la fuente",
"ShowBanners": "Mostrar banners",
"SearchMonitored": "Buscar monitorizados",
"Other": "Otro",
"RemoveCompletedDownloads": "Eliminar descargas completadas",
"RemoveFailedDownloads": "Eliminar descargas fallidas",
"SkipRedownload": "Saltar redescarga",
"SmartReplace": "Reemplazo inteligente",
"RemoveQueueItemRemovalMethod": "Método de eliminación",
"RemoveFromDownloadClientHint": "Elimina la descarga y archivo(s) del cliente de descarga",
"RemoveMultipleFromDownloadClientHint": "Elimina descargas y archivos del cliente de descarga",
"RemoveQueueItem": "Eliminar - {sourceTitle}",
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará la descarga y el archivo(s) del cliente de descarga.",
"RemoveQueueItemsRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará las descargas y los archivos del cliente de descarga."
} }

Some files were not shown because too many files have changed in this diff Show More