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

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.20'
majorVersion: '0.3.21'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'

View File

@@ -1,3 +1,5 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@@ -34,6 +36,8 @@ export interface CustomFilter {
}
interface AppState {
authors: AuthorsAppState;
commands: CommandAppState;
settings: SettingsAppState;
tags: TagsAppState;
}

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;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -5,6 +5,7 @@ import AppSectionState, {
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
@@ -27,11 +28,13 @@ export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
notifications: NotificationAppState;
uiSettings: UiSettingsAppState;

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;

View File

@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
function AuthorNameLink({ titleSlug, authorName }) {
function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
const link = `/author/${titleSlug}`;
return (
<Link to={link}>
<Link to={link} {...otherProps}>
{authorName}
</Link>
);

View File

@@ -27,3 +27,9 @@
width: 80px;
}
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'indexerFlags': string;
'monitored': string;
'pageCount': string;
'position': string;

View File

@@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
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 styles from './BookRow.css';
@@ -67,6 +72,7 @@ class BookRow extends Component {
authorMonitored,
titleSlug,
bookFiles,
indexerFlags,
isEditorActive,
isSelected,
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') {
return (
<TableRowCell
@@ -235,6 +259,7 @@ BookRow.propTypes = {
position: PropTypes.string,
pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired,
@@ -246,4 +271,8 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired
};
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow;

View File

@@ -7,21 +7,18 @@ import BookRow from './BookRow';
const selectBookFiles = createSelector(
(state) => state.bookFiles,
(bookFiles) => {
const {
items
} = bookFiles;
const { items } = bookFiles;
const bookFileDict = items.reduce((acc, file) => {
return items.reduce((acc, file) => {
const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = [];
}
acc[bookId].push(file);
return acc;
}, {});
return bookFileDict;
}
);
@@ -31,10 +28,14 @@ function createMapStateToProps() {
selectBookFiles,
(state, { id }) => id,
(author = {}, bookFiles, bookId) => {
const files = bookFiles[bookId] ?? [];
const bookFile = files[0];
return {
authorMonitored: author.monitored,
authorName: author.authorName,
bookFiles: bookFiles[bookId] ?? []
bookFiles: files,
indexerFlags: bookFile ? bookFile.indexerFlags : 0
};
}
);

View File

@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

View File

@@ -84,9 +84,15 @@
font-size: 20px;
}
.authorLink {
composes: link from '~Components/Link/Link.css';
margin-right: 15px;
color: var(--white);
}
.duration {
margin-right: 15px;
margin-left: 10px;
}
.detailsLabel {

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'alternateTitlesIconContainer': string;
'authorLink': string;
'backdrop': string;
'backdropOverlay': string;
'cover': string;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookCover from 'Book/BookCover';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -113,7 +114,7 @@ class BookDetailsHeader extends Component {
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={isSmallScreen ? 30: 40}
size={isSmallScreen ? 30 : 40}
onPress={onMonitorTogglePress}
/>
</div>
@@ -131,7 +132,12 @@ class BookDetailsHeader extends Component {
</div>
<div>
{author.authorName}
<AuthorNameLink
className={styles.authorLink}
titleSlug={author.titleSlug}
authorName={author.authorName}
/>
{
!!pageCount &&
<span className={styles.duration}>

View File

@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

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;

View File

@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
});
return acc;
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
}, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
const hasSelectedFiles = this.getSelectedIds().length > 0;

View File

@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

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;

View File

@@ -19,7 +19,7 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
cursor: not-allowed !important;
}
.dropdownArrowContainer {

View File

@@ -14,6 +14,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
@@ -83,6 +84,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
@@ -288,6 +292,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),

View File

@@ -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;

View File

@@ -39,7 +39,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -47,7 +47,7 @@ function createMapStateToProps() {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -18,7 +18,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -26,7 +26,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -17,7 +17,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -25,7 +25,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -26,7 +26,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -34,7 +34,7 @@ function createMapStateToProps() {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -2,3 +2,7 @@
margin-right: 5px;
color: var(--themeRed);
}
.rating {
margin-right: 15px;
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'heart': string;
'rating': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) {
return (
<span>
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}

View File

@@ -7,7 +7,14 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions';
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 { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -44,6 +51,7 @@ const selectAppProps = createSelector(
);
const selectIsPopulated = createSelector(
(state) => state.authors.isPopulated,
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
@@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
authorsIsPopulated,
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
@@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
metadataProfilesIsPopulated,
importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
authorsIsPopulated &&
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
@@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
metadataProfilesIsPopulated &&
importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated &&
translationsIsPopulated
);
@@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
);
const selectErrors = createSelector(
(state) => state.authors.error,
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
@@ -86,9 +100,11 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
authorsError,
customFiltersError,
tagsError,
uiSettingsError,
@@ -96,10 +112,12 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
) => {
const hasError = !!(
authorsError ||
customFiltersError ||
tagsError ||
uiSettingsError ||
@@ -107,6 +125,7 @@ const selectErrors = createSelector(
qualityProfilesError ||
metadataProfilesError ||
importListsError ||
indexerFlagsError ||
systemStatusError ||
translationsError
);
@@ -120,6 +139,7 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
};
@@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() {
dispatch(fetchUISettings());
},
@@ -218,6 +241,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
@@ -245,6 +269,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles,
dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
@@ -287,6 +312,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,

View File

@@ -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];
}

View File

@@ -60,6 +60,7 @@ import {
faFileImport as fasFileImport,
faFileInvoice as farFileInvoice,
faFilter as fasFilter,
faFlag as fasFlag,
faFolderOpen as fasFolderOpen,
faForward as fasForward,
faHeart as fasHeart,
@@ -155,6 +156,7 @@ export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILEIMPORT = fasFileImport;
export const FILTER = fasFilter;
export const FLAG = fasFlag;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup;

View File

@@ -15,6 +15,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
const columns = [
const COLUMNS = [
{
name: 'path',
label: 'Path',
@@ -74,11 +75,21 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER
kind: kinds.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
isVisible: true
@@ -102,6 +113,7 @@ const BOOK = 'book';
const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality';
const INDEXER_FLAGS = 'indexerFlags';
const replaceExistingFilesOptions = {
COMBINE: 'combine',
@@ -288,6 +300,21 @@ class InteractiveImportModalContent extends Component {
inconsistentBookReleases
} = 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 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();
@@ -299,7 +326,8 @@ class InteractiveImportModalContent extends Component {
{ key: BOOK, value: translate('SelectBook') },
{ key: EDITION, value: translate('SelectEdition') },
{ 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) {
@@ -422,6 +450,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving}
{...item}
allowAuthorChange={allowAuthorChange}
columns={columns}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
@@ -518,6 +547,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === INDEXER_FLAGS}
ids={selectedIds}
indexerFlags={0}
onModalClose={this.onSelectModalClose}
/>
<ConfirmImportModal
isOpen={isConfirmImportModalOpen}
books={booksImported}

View File

@@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component {
book,
foreignEditionId,
quality,
indexerFlags,
disableReleaseSwitching
} = item;
@@ -158,6 +159,7 @@ class InteractiveImportModalContentConnector extends Component {
bookId: book.id,
foreignEditionId,
quality,
indexerFlags,
downloadId: this.props.downloadId,
disableReleaseSwitching
});

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import FileDetails from 'BookFile/FileDetails';
import Icon from 'Components/Icon';
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 SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -34,7 +36,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen: false,
isSelectBookModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false
isSelectQualityModalOpen: false,
isSelectIndexerFlagsModalOpen: false
};
}
@@ -133,6 +136,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectQualityModalOpen: true });
};
onSelectIndexerFlagsPress = () => {
this.setState({ isSelectIndexerFlagsModalOpen: true });
};
onSelectAuthorModalClose = (changed) => {
this.setState({ isSelectAuthorModalOpen: false });
this.selectRowAfterChange(changed);
@@ -153,6 +160,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
};
onSelectIndexerFlagsModalClose = (changed) => {
this.setState({ isSelectIndexerFlagsModalOpen: false });
this.selectRowAfterChange(changed);
};
//
// Render
@@ -167,7 +179,9 @@ class InteractiveImportRow extends Component {
releaseGroup,
size,
customFormats,
indexerFlags,
rejections,
columns,
additionalFile,
isSelected,
isReprocessing,
@@ -180,7 +194,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen,
isSelectBookModalOpen,
isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen
isSelectQualityModalOpen,
isSelectIndexerFlagsModalOpen
} = this.state;
const authorName = author ? author.authorName : '';
@@ -193,6 +208,7 @@ class InteractiveImportRow extends Component {
const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
const pathCellContents = (
<div onClick={this.onDetailsPress}>
@@ -215,6 +231,8 @@ class InteractiveImportRow extends Component {
/>
);
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
return (
<TableRow
className={additionalFile ? styles.additionalFile : undefined}
@@ -307,6 +325,28 @@ class InteractiveImportRow extends Component {
}
</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>
{
rejections.length ?
@@ -378,6 +418,13 @@ class InteractiveImportRow extends Component {
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectIndexerFlagsModal
isOpen={isSelectIndexerFlagsModalOpen}
ids={[id]}
indexerFlags={indexerFlags ?? 0}
onModalClose={this.onSelectIndexerFlagsModalClose}
/>
</TableRow>
);
}
@@ -395,7 +442,9 @@ InteractiveImportRow.propTypes = {
quality: PropTypes.object,
size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired,
isReprocessing: PropTypes.bool,

View File

@@ -62,6 +62,15 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {

View File

@@ -40,6 +40,7 @@
}
.rejected,
.indexerFlags,
.download {
composes: cell;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'customFormatScore': string;
'download': string;
'indexer': string;
'indexerFlags': string;
'peers': string;
'protocol': string;
'quality': string;

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality,
customFormatScore,
customFormats,
indexerFlags = 0,
rejections,
downloadAllowed,
isGrabbing,
@@ -189,10 +191,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length)
}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.LEFT}
/>
</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}>
{
!!rejections.length &&
@@ -265,6 +278,7 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
@@ -277,6 +291,7 @@ InteractiveSearchRow.propTypes = {
};
InteractiveSearchRow.defaultProps = {
indexerFlags: 0,
rejections: [],
isGrabbing: false,
isGrabbed: false

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -32,7 +32,7 @@ const autoAddOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -1,6 +1,6 @@
.option {
display: flex;
align-items: center;
align-items: stretch;
flex-wrap: wrap;
margin: 3px;
border: 1px solid var(--borderColor);
@@ -17,7 +17,7 @@
}
.small {
width: 460px;
width: 490px;
}
.large {
@@ -26,7 +26,7 @@
.token {
flex: 0 0 50%;
padding: 6px 16px;
padding: 6px;
background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily;
}
@@ -34,9 +34,9 @@
.example {
display: flex;
align-items: center;
align-self: stretch;
justify-content: space-between;
flex: 0 0 50%;
padding: 6px 16px;
padding: 6px;
background-color: var(--popoverBodyBackgroundColor);
.footNote {

View File

@@ -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: {
}
};

View File

@@ -1,9 +1,11 @@
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
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 createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
@@ -243,6 +245,15 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{
name: 'status',
label: 'Status',

View File

@@ -207,6 +207,7 @@ export const actionHandlers = handleThunks({
foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined,
quality: item.quality,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId,
additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles,

View File

@@ -10,6 +10,7 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import importListExclusions from './Settings/importListExclusions';
import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers';
import languages from './Settings/languages';
@@ -35,6 +36,7 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/general';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/indexers';
export * from './Settings/languages';
@@ -70,6 +72,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState,
importLists: importLists.defaultState,
@@ -115,6 +118,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...indexers.actionHandlers,
...importLists.actionHandlers,
@@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...downloadClientOptions.reducers,
...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers,
...indexers.reducers,
...importLists.reducers,

View File

@@ -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;

View File

@@ -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;

View File

@@ -10,15 +10,6 @@
width: 100%;
}
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued,
.started,
.ended {

View File

@@ -2,14 +2,12 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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;

View File

@@ -0,0 +1,238 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status: string, message: string | undefined) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title,
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title,
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`,
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`,
};
default:
return {
name: icons.UNKNOWN,
title,
};
}
}
function getFormattedDates(
queued: string,
started: string | undefined,
ended: string | undefined,
showRelativeDates: boolean,
shortDateFormat: string
) {
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-',
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
};
}
interface QueuedTimes {
queuedAt: string;
startedAt: string;
endedAt: string;
}
export interface QueuedTaskRowProps {
id: number;
trigger: string;
commandName: string;
queued: string;
started?: string;
ended?: string;
status: string;
duration?: string;
message?: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
const {
id,
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
body,
clientUserAgent,
} = props;
const dispatch = useDispatch();
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const [times, setTimes] = useState<QueuedTimes>(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
const [
isCancelConfirmModalOpen,
openCancelConfirmModal,
closeCancelConfirmModal,
] = useModalOpenState(false);
const handleCancelPress = useCallback(() => {
dispatch(cancelCommand({ id }));
}, [id, dispatch]);
useEffect(() => {
updateTimeTimeoutId.current = setTimeout(() => {
setTimes(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
}, 30000);
return () => {
if (updateTimeTimeoutId.current) {
clearTimeout(updateTimeTimeoutId.current);
}
};
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
const { queuedAt, startedAt, endedAt } = times;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon name={triggerIcon} title={titleCase(trigger)} />
<Icon {...getStatusIconProps(status, message)} />
</span>
</TableRowCell>
<QueuedTaskRowNameCell
commandName={commandName}
body={body}
clientUserAgent={clientUserAgent}
/>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell className={styles.actions}>
{status === 'queued' && (
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={openCancelConfirmModal}
/>
)}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelPendingTask')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={handleCancelPress}
onCancel={closeCancelConfirmModal}
/>
</TableRow>
);
}

View File

@@ -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);

View File

@@ -0,0 +1,8 @@
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'commandName': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { fetchCommands } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import QueuedTaskRow from './QueuedTaskRow';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true,
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true,
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true,
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true,
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true,
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true,
},
{
name: 'actions',
isVisible: true,
},
];
export default function QueuedTasks() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items } = useSelector(
(state: AppState) => state.commands
);
useEffect(() => {
dispatch(fetchCommands());
}, [dispatch]);
return (
<FieldSet legend={translate('Queue')}>
{isFetching && !isPopulated && <LoadingIndicator />}
{isPopulated && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return <QueuedTaskRow key={item.id} {...item} />;
})}
</TableBody>
</Table>
)}
</FieldSet>
);
}

View File

@@ -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);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
import QueuedTasks from './Queued/QueuedTasks';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
<PageContent title={translate('Tasks')}>
<PageContentBody>
<ScheduledTasksConnector />
<QueuedTasksConnector />
<QueuedTasks />
</PageContentBody>
</PageContent>
);

View File

@@ -0,0 +1,6 @@
interface IndexerFlag {
id: number;
name: string;
}
export default IndexerFlag;

View File

@@ -11,7 +11,7 @@
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
"lint-fix": "yarn lint --fix",
"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": {
"type": "git",

View File

@@ -8,7 +8,7 @@
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<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.SQLite" 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="Sentry" Version="3.31.0" />
<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="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />

View File

@@ -9,6 +9,7 @@ using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.BookImport;
using NzbDrone.Core.MediaFiles.BookImport.Aggregation;
@@ -134,6 +135,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.Setup(s => s.ReadTags(It.IsAny<IFileInfo>()))
.Returns(new ParsedTrackInfo());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EntityHistory>());
GivenSpecifications(_bookpass1);
}

View File

@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[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>
{
private MetadataProfile _metadataProfile;
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.MetadataSource.Goodreads
}
[TestCase("1128601", "Guards! Guards!")]
[TestCase("3293141", "Ιλιάς")]
[TestCase("3293141", "λιάς")]
public void should_be_able_to_get_book_detail(string mbId, string name)
{
var details = Subject.GetBookInfo(mbId);

View File

@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
{
[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>
{
[SetUp]

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Blocklisting
@@ -19,6 +20,7 @@ namespace NzbDrone.Core.Blocklisting
public long? Size { get; set; }
public DownloadProtocol Protocol { get; set; }
public string Indexer { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public string Message { get; set; }
public string TorrentInfoHash { get; set; }
}

View File

@@ -188,6 +188,11 @@ namespace NzbDrone.Core.Blocklisting
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash")
};
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
blocklist.IndexerFlags = flags;
}
_blocklistRepository.Insert(blocklist);
}

View File

@@ -20,5 +20,7 @@ namespace NzbDrone.Core.Books.Commands
public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => !AuthorId.HasValue;
public override string CompletionMessage => "Completed";
}
}

View File

@@ -18,5 +18,7 @@ namespace NzbDrone.Core.Books.Commands
public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => !BookId.HasValue;
public override string CompletionMessage => "Completed";
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -38,7 +39,8 @@ namespace NzbDrone.Core.CustomFormats
{
BookInfo = remoteBook.ParsedBookInfo,
Author = remoteBook.Author,
Size = size
Size = size,
IndexerFlags = remoteBook.Release?.IndexerFlags ?? 0
};
return ParseCustomFormat(input);
@@ -70,7 +72,8 @@ namespace NzbDrone.Core.CustomFormats
{
BookInfo = bookInfo,
Author = author,
Size = blocklist.Size ?? 0
Size = blocklist.Size ?? 0,
IndexerFlags = blocklist.IndexerFlags
};
return ParseCustomFormat(input);
@@ -81,6 +84,7 @@ namespace NzbDrone.Core.CustomFormats
var parsed = Parser.Parser.ParseBookTitle(history.SourceTitle);
long.TryParse(history.Data.GetValueOrDefault("size"), out var size);
Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags);
var bookInfo = new ParsedBookInfo
{
@@ -94,7 +98,8 @@ namespace NzbDrone.Core.CustomFormats
{
BookInfo = bookInfo,
Author = author,
Size = size
Size = size,
IndexerFlags = indexerFlags
};
return ParseCustomFormat(input);
@@ -114,7 +119,8 @@ namespace NzbDrone.Core.CustomFormats
{
BookInfo = bookInfo,
Author = localBook.Author,
Size = localBook.Size
Size = localBook.Size,
IndexerFlags = localBook.IndexerFlags,
};
return ParseCustomFormat(input);
@@ -181,6 +187,7 @@ namespace NzbDrone.Core.CustomFormats
BookInfo = bookInfo,
Author = author,
Size = bookFile.Size,
IndexerFlags = bookFile.IndexerFlags,
Filename = Path.GetFileName(bookFile.Path)
};

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.CustomFormats
public ParsedBookInfo BookInfo { get; set; }
public Author Author { get; set; }
public long Size { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public string Filename { get; set; }
// public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series)

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -12,6 +12,7 @@ using NzbDrone.Core.Download.History;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.TrackedDownloads
{
@@ -156,11 +157,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
var firstHistoryItem = historyItems.First();
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
if (parsedBookInfo == null ||
trackedDownload.RemoteBook == null ||
trackedDownload.RemoteBook.Author == null ||
trackedDownload.RemoteBook?.Author == null ||
trackedDownload.RemoteBook.Books.Empty())
{
// 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

View File

@@ -164,6 +164,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString());
history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString());
history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString());
history.Data.Add("IndexerFlags", message.Book.Release.IndexerFlags.ToString());
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("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
history.Data.Add("IndexerFlags", message.TrackedDownload?.RemoteBook?.Release?.IndexerFlags.ToString());
_historyRepository.Insert(history);
}
}
@@ -237,6 +240,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup);
history.Data.Add("Size", message.BookInfo.Size.ToString());
history.Data.Add("IndexerFlags", message.BookInfo.IndexerFlags.ToString());
_historyRepository.Insert(history);
}
@@ -290,6 +294,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history);
}
@@ -313,6 +318,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Path", path);
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("Size", message.BookFile.Size.ToString());
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history);
}

View File

@@ -134,7 +134,7 @@ namespace NzbDrone.Core.IndexerSearch
var reports = batch.SelectMany(x => x).ToList();
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
_logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
// Update the last search time for all albums if at least 1 indexer was searched.
if (indexers.Any())

View File

@@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
{
var id = result.Id;
//if (result.FreeLeech)
torrentInfos.Add(new TorrentInfo()
torrentInfos.Add(new TorrentInfo
{
Guid = $"FileList-{id}",
Title = result.Name,
@@ -48,13 +47,31 @@ namespace NzbDrone.Core.Indexers.FileList
InfoUrl = GetInfoUrl(id),
Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate.ToUniversalTime()
PublishDate = result.UploadDate.ToUniversalTime(),
IndexerFlags = GetIndexerFlags(result)
});
}
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)
{
var url = new HttpUri(_settings.BaseUrl)

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers.FileList
public uint Files { get; set; }
[JsonProperty(PropertyName = "imdb")]
public string ImdbId { get; set; }
public bool Internal { get; set; }
[JsonProperty(PropertyName = "freeleech")]
public bool FreeLeech { get; set; }
[JsonProperty(PropertyName = "upload_date")]

View File

@@ -34,6 +34,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public string Leechers { get; set; }
public bool IsFreeLeech { get; set; }
public bool IsNeutralLeech { get; set; }
public bool IsFreeload { get; set; }
public bool IsPersonalFreeLeech { get; set; }
public bool CanUseToken { get; set; }
}

View File

@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
var author = WebUtility.HtmlDecode(result.Author);
var book = WebUtility.HtmlDecode(result.GroupName);
torrentInfos.Add(new GazelleInfo()
torrentInfos.Add(new GazelleInfo
{
Guid = string.Format("Gazelle-{0}", id),
Author = author,
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene,
IndexerFlags = GetIndexerFlags(torrent)
});
}
}
@@ -88,6 +88,23 @@ namespace NzbDrone.Core.Indexers.Gazelle
.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)
{
var url = new HttpUri(_settings.BaseUrl)

View File

@@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
public class RssSyncCommand : Command
{
public override bool SendUpdatesToClient => true;
public override bool IsLongRunning => true;
}
}

View File

@@ -74,6 +74,18 @@ namespace NzbDrone.Core.Indexers.Torznab
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)
{
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
@@ -194,6 +206,53 @@ namespace NzbDrone.Core.Indexers.Torznab
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 = "")
{
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;
}
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)
{
var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));

View File

@@ -631,5 +631,10 @@
"ExtraFileExtensionsHelpTextsExamples": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\"",
"AutoRedownloadFailed": "التحميل فشل",
"SourceTitle": "عنوان المصدر",
"RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟"
"RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟",
"ImportLists": "القوائم",
"ListsSettingsSummary": "القوائم",
"SelectDropdown": "'تحديد...",
"SelectQuality": "حدد الجودة",
"CustomFilter": "مرشحات مخصصة"
}

View File

@@ -630,5 +630,11 @@
"ExtraFileExtensionsHelpText": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)",
"ExtraFileExtensionsHelpTextsExamples": "Примери: '.sub, .nfo' или 'sub, nfo'",
"AutoRedownloadFailed": "Изтеглянето се провали",
"SourceTitle": "Заглавие на източника"
"SourceTitle": "Заглавие на източника",
"ImportLists": "Списъци",
"ListsSettingsSummary": "Списъци",
"SelectDropdown": "„Изберете ...",
"SelectQuality": "Изберете Качество",
"CustomFilter": "Персонализирани филтри",
"RemoveQueueItemConfirmation": "Наистина ли искате да премахнете {0} елемент {1} от опашката?"
}

View File

@@ -688,5 +688,8 @@
"SourceTitle": "Název zdroje",
"AutoRedownloadFailed": "Opětovné stažení se nezdařilo",
"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"
}

View File

@@ -363,10 +363,10 @@
"TagIsNotUsedAndCanBeDeleted": "Tag bruges ikke og kan slettes",
"Tags": "Mærker",
"Tasks": "Opgaver",
"TestAll": "Test alle",
"TestAllClients": "Test alle klienter",
"TestAllIndexers": "Test alle indeksører",
"TestAllLists": "Test alle lister",
"TestAll": "Afprøv alle",
"TestAllClients": "Afprøv alle klienter",
"TestAllIndexers": "Afprøv alle indeks",
"TestAllLists": "Afprøv alle lister",
"ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "Dette gælder for alle indeksører. Følg de regler, der er angivet af dem",
"TimeFormat": "Tidsformat",
"Title": "Titel",
@@ -542,7 +542,7 @@
"Yesterday": "I går",
"RestartRequiredHelpTextWarning": "Kræver genstart for at træde i kraft",
"AddList": "Tilføj Liste",
"Test": "Prøve",
"Test": "Afprøv",
"RenameFiles": "Omdøb filer",
"ManualImportSelectEdition": "Manuel import - Vælg film",
"ImportListExclusions": "Slet udelukkelse af importliste",
@@ -638,5 +638,11 @@
"ExtraFileExtensionsHelpTextsExamples": "Eksempler: '.sub, .nfo' eller 'sub, nfo'",
"AutoRedownloadFailed": "Download fejlede",
"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"
}

View File

@@ -505,7 +505,7 @@
"ThisCannotBeCancelled": "Nach dem Start kann dies nicht mehr abgebrochen werden ohne alle Indexer zu deaktivieren.",
"UnselectAll": "Alle abwählen",
"UpdateSelected": "Auswahl aktualisieren",
"Wanted": " Gesucht",
"Wanted": "Gesucht",
"CreateEmptyAuthorFolders": "Leere Filmordner erstellen",
"All": "Alle",
"Country": "Land",
@@ -990,5 +990,13 @@
"AutoAdd": "Automatisch hinzufügen",
"WouldYouLikeToRestoreBackup": "Möchten Sie die Sicherung „{name}“ wiederherstellen?",
"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"
}

View File

@@ -145,6 +145,7 @@
"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.",
"Clear": "Clear",
"ClickToChangeIndexerFlags": "Click to change indexer flags",
"ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group",
"ClientPriority": "Client Priority",
@@ -192,6 +193,7 @@
"CustomFormatScore": "Custom Format Score",
"CustomFormatSettings": "Custom Format Settings",
"CustomFormats": "Custom Formats",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationRegularExpression": "Regular Expression",
"CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive",
"CutoffFormatScoreHelpText": "Once this custom format score is reached Readarr will no longer grab book releases",
@@ -439,6 +441,7 @@
"Indexer": "Indexer",
"IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {0}.",
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
"IndexerFlags": "Indexer Flags",
"IndexerIdHelpText": "Specify what indexer the profile applies to",
"IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed",
"IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}",
@@ -735,6 +738,7 @@
"RefreshBook": "Refresh Book",
"RefreshInformation": "Refresh information",
"RefreshInformationAndScanDisk": "Refresh information and scan disk",
"Rejections": "Rejections",
"ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Readarr release branch, you will not receive updates",
"ReleaseDate": "Release Date",
"ReleaseGroup": "Release Group",
@@ -853,6 +857,7 @@
"SelectBook": "Select Book",
"SelectDropdown": "Select...",
"SelectEdition": "Select Edition",
"SelectIndexerFlags": "Select Indexer Flags",
"SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group",
"SelectedCountAuthorsSelectedInterp": "{0} Author(s) Selected",
@@ -862,6 +867,7 @@
"Series": "Series",
"SeriesNumber": "Series Number",
"SeriesTotal": "Series ({0})",
"SetIndexerFlags": "Set Indexer Flags",
"SetPermissions": "Set Permissions",
"SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?",
"SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.",

View File

@@ -2,14 +2,14 @@
"ApiKeyHelpTextWarning": "Requiere reiniciar para que surta efecto",
"DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{0}'?",
"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",
"SslCertPathHelpTextWarning": "Requiere reiniciar para que surta efecto",
"UnableToLoadMetadataProfiles": "No se pueden cargar los Perfiles de Retraso",
"BookIsDownloading": "Le película está descargando",
"Group": "Grupo",
"MIA": "MIA",
"ShowDateAdded": "Mostrar Fecha de adido",
"ShowDateAdded": "Mostrar fecha de adición",
"Tags": "Etiquetas",
"60MinutesSixty": "60 Minutos: {0}",
"APIKey": "Clave API",
@@ -223,10 +223,10 @@
"OnHealthIssueHelpText": "En Problema de Salud",
"OnRenameHelpText": "Al Renombrar",
"OnUpgradeHelpText": "Al Mejorar La Calidad",
"OpenBrowserOnStart": "Abrir navegador al arrancar",
"OpenBrowserOnStart": "Abrir navegador al inicio",
"Options": "Opciones",
"Original": "Original",
"Overview": "Resumen",
"Overview": "Vista general",
"PackageVersion": "Versión del paquete",
"PageSize": "Tamaño de Página",
"PageSizeHelpText": "Número de elementos por página",
@@ -235,24 +235,24 @@
"Permissions": "Permisos",
"Port": "Puerto",
"PortHelpTextWarning": "Requiere reiniciar para que surta efecto",
"PortNumber": "Número de Puerto",
"PosterSize": "Tamaño del Poster",
"PreviewRename": "Previsualizar Renombrado",
"PortNumber": "Número de puerto",
"PosterSize": "Tamaño dester",
"PreviewRename": "Previsualizar renombrado",
"Profiles": "Perfiles",
"Proper": "Apropiado",
"Proper": "Proper",
"PropersAndRepacks": "Propers y Repacks",
"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",
"ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como wildcard para subdominios",
"ProxyPasswordHelpText": "Tienes que introducir tu nombre de usuario y contraseña sólo si son requeridos. Si no, déjalos vacios.",
"ProxyType": "Tipo de Proxy",
"PublishedDate": "Fecha de Publicación",
"ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios",
"ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.",
"ProxyType": "Tipo de proxy",
"PublishedDate": "Fecha de publicación",
"Quality": "Calidad",
"QualityDefinitions": "Definiciones de Calidad",
"QualityProfile": "Perfil de Calidad",
"QualityProfiles": "Perfiles de Calidad",
"QualitySettings": "Ajustes de Calidad",
"QualityDefinitions": "Definiciones de calidad",
"QualityProfile": "Perfil de calidad",
"QualityProfiles": "Perfiles de calidad",
"QualitySettings": "Opciones de calidad",
"Queue": "Cola",
"RSSSync": "Sincronizar RSS",
"RSSSyncInterval": "Intervalo de Sincronización de RSS",
@@ -264,85 +264,85 @@
"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",
"RecycleBinHelpText": "Los archivos iran aquí una vez se hayan borrado en vez de ser borrados permanentemente",
"RecyclingBin": "Papelera de Reciclaje",
"RecyclingBinCleanup": "Limpieza de Papelera de Reciclaje",
"RecyclingBin": "Papelera de reciclaje",
"RecyclingBinCleanup": "Limpieza de la papelera de reciclaje",
"Redownload": "Volver a descargar",
"Refresh": "Actualizar",
"RefreshInformationAndScanDisk": "Actualizar la información al escanear el disco",
"ReleaseDate": "Fechas de Estreno",
"ReleaseGroup": "Grupo de Estreno",
"ReleaseRejected": "Lanzamiento Rechazado",
"ReleaseGroup": "Grupo de lanzamiento",
"ReleaseRejected": "Lanzamiento rechazado",
"ReleaseWillBeProcessedInterp": "El lanzamiento será procesado {0}",
"Reload": "Recargar",
"RemotePathMappings": "Mapeados de Rutas Remotas",
"RemotePathMappings": "Mapeos de ruta remota",
"Remove": "Eliminar",
"RemoveCompletedDownloadsHelpText": "Eliminar las descargas ya importadas del historial del gestor de descargas",
"RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas del historial del gestor de descargas",
"RemoveCompletedDownloadsHelpText": "Elimina las descargas importadas desde el historial del cliente de descarga",
"RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas desde el historial del cliente de descarga",
"RemoveFilter": "Eliminar filtro",
"RemoveFromDownloadClient": "Eliminar del Gestor de Descargas",
"RemoveFromDownloadClient": "Eliminar del cliente de descarga",
"RemoveFromQueue": "Eliminar de la cola",
"RemoveHelpTextWarning": "Eliminar borrará la descarga y el/los fichero(s) del gestor de descargas.",
"RemoveSelected": "Borrar Seleccionados",
"RemoveSelected": "Eliminar seleccionado",
"RemoveTagExistingTag": "Etiqueta existente",
"RemoveTagRemovingTag": "Eliminando etiqueta",
"RemovedFromTaskQueue": "Eliminar de la cola de tareas",
"RenameBooksHelpText": "Radarr usará el nombre del archivo si el renombrado está deshabilitado",
"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)",
"RequiredPlaceHolder": "Añadir nueva restricción",
"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",
"Reset": "Reiniciar",
"ResetAPIKey": "Reajustar API",
"ResetAPIKeyMessageText": "¿Está seguro de que desea restablecer su clave API?",
"ResetAPIKey": "Restablecer clave API",
"ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?",
"Restart": "Reiniciar",
"RestartNow": "Reiniciar Ahora",
"RestartNow": "Reiniciar ahora",
"RestartReadarr": "Reiniciar Radarr",
"Restore": "Restaurar",
"RestoreBackup": "Recuperar Backup",
"RestoreBackup": "Restaurar copia de seguridad",
"Result": "Resultado",
"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}",
"RootFolder": "Carpeta de Origen",
"RootFolders": "Carpetas de Origen",
"RssSyncIntervalHelpText": "Intervalo en minutos. Ajustar a cero para inhabilitar (esto dentendrá toda captura de estrenos automática)",
"RootFolder": "Carpeta raíz",
"RootFolders": "Carpetas raíz",
"RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)",
"SSLCertPassword": "Contraseña del Certificado SSL",
"SSLCertPath": "Ruta del Certificado SSL",
"SSLPort": "Puerto SSL",
"Scheduled": "Programado",
"ScriptPath": "Ruta del Script",
"ScriptPath": "Ruta del script",
"Search": "Buscar",
"SearchAll": "Buscar Todas",
"SearchForMissing": "Buscar faltantes",
"SearchSelected": "Buscar Seleccionadas",
"SearchAll": "Buscar todo",
"SearchForMissing": "Buscar perdidos",
"SearchSelected": "Buscar seleccionados",
"Security": "Seguridad",
"SendAnonymousUsageData": "Enviar Datos de Uso Anónimamente",
"SetPermissions": "Ajustar Permisos",
"SetPermissionsLinuxHelpText": "Debe chmod ser ejecutado una vez los archivos hayan sido importados/renombrados?",
"SetPermissionsLinuxHelpTextWarning": "Si no estas seguro de lo que hacen estos ajustes, no los modifiques.",
"SendAnonymousUsageData": "Enviar datos de uso anónimos",
"SetPermissions": "Establecer permisos",
"SetPermissionsLinuxHelpText": "¿Debería ejecutarse chmod cuando los archivos son importados/renombrados?",
"SetPermissionsLinuxHelpTextWarning": "Si no estás seguro qué configuraciones hacer, no las cambies.",
"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",
"ShowMonitored": "Mostrar Monitoreadas",
"ShowMonitoredHelpText": "Mostrar el estado de monitoreo debajo del poster",
"ShowPath": "Mostrar Ruta",
"ShowQualityProfile": "Mostrar Perfil de Calidad",
"ShowQualityProfileHelpText": "Mostrar el perfil de calidad debajo del poster",
"ShowRelativeDates": "Mostrar Fechas Relativas",
"ShowRelativeDatesHelpText": "Mostrar fechas relativas (Hoy/Ayer/etc) o absolutas",
"ShowSearch": "Mostrar Búsqueda",
"ShowMonitored": "Mostrar monitorizado",
"ShowMonitoredHelpText": "Muestra el estado monitorizado bajo el póster",
"ShowPath": "Mostrar ruta",
"ShowQualityProfile": "Mostrar perfil de calidad",
"ShowQualityProfileHelpText": "Muestra el perfil de calidad bajo el póster",
"ShowRelativeDates": "Mostrar fechas relativas",
"ShowRelativeDatesHelpText": "Muestra fechas absolutas o relativas (Hoy/Ayer/etc)",
"ShowSearch": "Mostrar búsqueda",
"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",
"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",
"SorryThatAuthorCannotBeFound": "Lo siento, no he encontrado esa película.",
"SorryThatBookCannotBeFound": "Lo siento, no he encontrado esa película.",
"Source": "Fuente",
"SourcePath": "Ruta de Origen",
"SourcePath": "Ruta de la fuente",
"SslCertPasswordHelpText": "Contraseña para el archivo pfx",
"SslCertPasswordHelpTextWarning": "Requiere reiniciar para que surta efecto",
"SslCertPathHelpText": "Ruta al archivo pfx",
@@ -360,17 +360,17 @@
"SupportsSearchvalueWillBeUsedWhenInteractiveSearchIsUsed": "Se usará cuando se utilice la búsqueda interactiva",
"TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada",
"Tasks": "Tareas",
"TestAll": "Testear Todo",
"TestAllClients": "Comprobar Todos los Gestores",
"TestAllIndexers": "Comprobar Todos los Indexers",
"TestAllLists": "Comprobar Todas las Listas",
"TestAll": "Probar todo",
"TestAllClients": "Probar todos los clientes",
"TestAllIndexers": "Probar todos los indexadores",
"TestAllLists": "Probar todas las listas",
"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",
"TorrentDelay": "Retraso del Torrent",
"TorrentDelayHelpText": "Retraso en minutos a esperar antes de descargar un torrent",
"TorrentDelay": "Retraso de torrent",
"TorrentDelayHelpText": "Retraso en minutos a esperar antes de capturar un torrent",
"Torrents": "Torrents",
"TotalFileSize": "Tamaño Total del Archivo",
"TotalFileSize": "Tamaño total de archivo",
"UILanguage": "Lenguaje de UI",
"UILanguageHelpText": "Lenguaje que Radarr usara para el UI",
"UILanguageHelpTextWarning": "Recargar el Navegador",
@@ -385,7 +385,7 @@
"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.",
"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",
"UnableToLoadDownloadClientOptions": "No se han podido cargar las opciones del gestor de descargas",
"UnableToLoadDownloadClients": "No se puden cargar los gestores de descargas",
@@ -408,27 +408,27 @@
"UnableToLoadTags": "No se pueden cargar las Etiquetas",
"UnableToLoadTheCalendar": "No se ha podido cargar el calendario",
"UnableToLoadUISettings": "No se han podido cargar los ajustes de UI",
"Ungroup": "Desagrupar",
"Ungroup": "Sin agrupar",
"Unmonitored": "Sin monitorizar",
"UnmonitoredHelpText": "Incluir las peliculas no monitoreadas en el feed de iCal",
"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",
"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",
"UpgradeAllowedHelpText": "Si está desactivado las calidades no serán actualizadas",
"Uptime": "Tiempo de actividad",
"UrlBaseHelpTextWarning": "Requiere reiniciar para que surta efecto",
"UseHardlinksInsteadOfCopy": "Usar Hardlinks en vez de Copia",
"UseProxy": "Usar el Proxy",
"UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar",
"UseProxy": "Usar proxy",
"Usenet": "Usenet",
"UsenetDelay": "Retraso de Usenet",
"UsenetDelayHelpText": "Retraso en minutos a esperar antes de descargar un lanzamiento de Usenet",
"Username": "Nombre de usuario",
"UsenetDelay": "Retraso de usenet",
"UsenetDelayHelpText": "Retraso en minutos a esperar antes de capturar un lanzamiento desde usenet",
"Username": "Usuario",
"UsingExternalUpdateMechanismBranchToUseToUpdateReadarr": "Qué rama usar para actualizar Radarr",
"UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism": "Rama usada por el mecanismo de actualización externo",
"Version": "Versión",
"WeekColumnHeader": "Encabezado de la columna semanal",
"WeekColumnHeader": "Cabecera de columna de semana",
"Year": "Año",
"YesCancel": "Sí, Cancelar",
"20MinutesTwenty": "20 Minutos: {0}",
@@ -437,16 +437,16 @@
"ReplaceIllegalCharactersHelpText": "Reemplazar caracteres ilegales. Si está desactivado, Radarr los eliminará si no",
"Actions": "Acciones",
"Today": "Hoy",
"ReleaseTitle": "Título del Estreno",
"ReleaseTitle": "Título de lanzamiento",
"Progress": "Progreso",
"Tomorrow": "mañana",
"OutputPath": "Ruta de Output",
"Tomorrow": "Mañana",
"OutputPath": "Ruta de salida",
"BookAvailableButMissing": "Película Disponible pero Ausente",
"NotAvailable": "No Disponible",
"NotMonitored": "No Monitoreadas",
"ShowBookTitleHelpText": "Mostrar el título de la película debajo del poster",
"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.",
"Component": "Componente",
"RemoveFromBlocklist": "Eliminar de lista de bloqueados",
@@ -457,13 +457,13 @@
"Blocklist": "Lista de bloqueos",
"BlocklistRelease": "Lista de bloqueos de lanzamiento",
"CreateEmptyAuthorFolders": "Crear carpetas de películas vacías",
"SelectAll": "Seleccionar Todas",
"SelectAll": "Seleccionar todo",
"SelectedCountBooksSelectedInterp": "{0} Película(s) Seleccionada(s)",
"ThisCannotBeCancelled": "Esto no puede ser cancelado una vez iniciado sin deshabilitar todos sus indexadores.",
"All": "Todo",
"RescanAfterRefreshHelpText": "Reescanear la carpeta de películas después de actualizar la película",
"ShowUnknownAuthorItems": "Mostrar Elementos Desconocidos",
"UnselectAll": "Deseleccionar Todo",
"UnselectAll": "Desmarcar todo",
"UpdateSelected": "Actualizar Seleccionadas",
"Wanted": "Buscado",
"AllAuthorBooks": "Todos los libros del autor",
@@ -485,9 +485,9 @@
"Yesterday": "Ayer",
"UpdateAvailable": "La nueva actualización está disponible",
"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",
"SizeLimit": "Tamaño límite",
"SizeLimit": "Límite de tamaño",
"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.",
"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",
"AllowFingerprinting": "Permitir impresión digital",
"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}?",
"HealthNoIssues": "No hay problemas con tu configuración",
"ImportListStatusCheckAllClientMessage": "Las listas no están disponibles debido a errores",
@@ -537,9 +537,9 @@
"OnBookFileDeleteForUpgrade": "En archivo de película Eliminar para actualizar",
"OnBookFileDeleteForUpgradeHelpText": "En archivo de película Eliminar para actualizar",
"OnBookFileDeleteHelpText": "Al eliminar archivo de película",
"OnGrab": "Al Capturar",
"OnRename": "Al Renombrar",
"OnUpgrade": "Al Mejorar La Calidad",
"OnGrab": "Al capturar",
"OnRename": "Al renombrar",
"OnUpgrade": "Al actualizar",
"ProxyCheckBadRequestMessage": "Fallo al comprobar el proxy. StatusCode: {0}",
"ProxyCheckFailedToTestMessage": "Fallo al comprobar el proxy: {0}",
"QualitySettingsSummary": "Tamaños de calidad y nombrado",
@@ -574,10 +574,10 @@
"WriteTagsNo": "Nunca",
"FileWasDeletedByUpgrade": "Se eliminó el archivo para importar una actualización",
"IndexersSettingsSummary": "Indexadores y restricciones de lanzamiento",
"RestartRequiredHelpTextWarning": "Requiere reiniciar para que surta efecto",
"RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto",
"AddList": "Añadir Lista",
"RenameFiles": "Renombrar Archivos",
"Test": "Test",
"RenameFiles": "Renombrar archivos",
"Test": "Prueba",
"InstanceName": "Nombre de la Instancia",
"InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog",
"Database": "Base de datos",
@@ -587,7 +587,7 @@
"ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento",
"HardlinkCopyFiles": "Enlace permanente/Copiar archivos",
"MoveFiles": "Mover archivos",
"OnApplicationUpdate": "Al Actualizar La Aplicación",
"OnApplicationUpdate": "Al actualizar la aplicación",
"OnApplicationUpdateHelpText": "Al Actualizar La Aplicación",
"BypassIfHighestQuality": "Pasar sí es la calidad más alta",
"CustomFormatScore": "Puntuación de Formato personalizado",
@@ -602,17 +602,17 @@
"IncludeCustomFormatWhenRenamingHelpText": "Incluir en el formato de renombrado {Formatos Propios}",
"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.",
"ResetDefinitionTitlesHelpText": "Restablecer los títulos y valores de las definiciones",
"ResetDefinitionTitlesHelpText": "Restablecer títulos de definición también como valores",
"ResetDefinitions": "Restablecer definiciones",
"UnableToLoadCustomFormats": "No se pueden cargar los Formatos Propios",
"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",
"CutoffFormatScoreHelpText": "Una vez alcanzada esta puntuación del formato propio Radarr dejará de descargar películas",
"DeleteCustomFormatMessageText": "Seguro que quieres eliminar el indexer '{name}'?",
"ExportCustomFormat": "Exportar formato personalizado",
"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",
"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}",
@@ -620,32 +620,32 @@
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
"HiddenClickToShow": "Oculto, click para mostrar",
"HideAdvanced": "Ocultar avanzado",
"ShowAdvanced": "Mostrar Avanzado",
"ShownClickToHide": "Mostrado, clic para ocultar",
"ReplaceWithDash": "Reemplazar con Dash",
"ReplaceWithSpaceDash": "Reemplazar con Space Dash",
"ReplaceWithSpaceDashSpace": "Reemplazar con Space Dash Space",
"ShowAdvanced": "Mostrar avanzado",
"ShownClickToHide": "Mostrado, haz clic para ocultar",
"ReplaceWithDash": "Reemplazar con guion",
"ReplaceWithSpaceDash": "Reemplazar por barra espaciadora",
"ReplaceWithSpaceDashSpace": "Reemplazar por espacio en la barra espaciadora",
"DeleteRemotePathMapping": "Borrar mapeo de ruta remota",
"BlocklistReleases": "Lista de bloqueos de lanzamientos",
"DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{0}'?",
"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?",
"RemoveSelectedItemQueueMessageText": "¿Está seguro de que desea eliminar el {0} elemento {1} de la cola?",
"RemoveSelectedItems": "Eliminar los elementos seleccionados",
"RemoveSelectedItemQueueMessageText": "¿Estás seguro que quieres eliminar 1 elemento de la cola?",
"RemoveSelectedItems": "Eliminar elementos seleccionados",
"RemoveSelectedItemsQueueMessageText": "¿Estás seguro de que quieres eliminar {0} elementos de la cola?",
"Required": "Necesario",
"Required": "Solicitado",
"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",
"NoEventsFound": "Ningún evento encontrado",
"ApplyTagsHelpTextHowToApplyAuthors": "Cómo añadir etiquetas a las películas seleccionadas",
"DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?",
"Yes": "Sí",
"RedownloadFailed": "La descarga ha fallado",
"RemoveCompleted": "Eliminación completada",
"RemoveDownloadsAlert": "Los ajustes de eliminación se han trasladado a los ajustes individuales del cliente de descarga en la tabla anterior.",
"RemoveFailed": "La eliminación falló",
"RemoveCompleted": "Eliminar completado",
"RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.",
"RemoveFailed": "Fallo al eliminar",
"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",
"ApplyTagsHelpTextHowToApplyImportLists": "Cómo añadir etiquetas a las listas de importación seleccionadas",
@@ -661,7 +661,7 @@
"No": "No",
"NoChange": "Sin cambio",
"RemovingTag": "Eliminando etiqueta",
"SetTags": "Poner Etiquetas",
"SetTags": "Establecer etiquetas",
"DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?",
"ApplicationURL": "URL de la aplicación",
"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.",
"NotificationStatusSingleClientHealthCheckMessage": "Listas no disponibles debido a errores: {0}",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos",
"ReleaseProfiles": "perfil de lanzamiento",
"Small": "Pequeña",
"ReleaseProfiles": "Perfiles de lanzamiento",
"Small": "Pequeño",
"DeleteImportList": "Eliminar Lista(s) de Importación",
"Large": "Grande",
"Library": "Biblioteca",
@@ -801,5 +801,49 @@
"MediaManagementSettingsSummary": "Nombrado, opciones de gestión de archivos y carpetas raíz",
"MonitoringOptions": "Opciones de monitorización",
"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