mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-13 20:46:53 -04:00
Compare commits
15 Commits
v0.3.20.24
...
v0.3.21.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a9f670dd | ||
|
|
11eda3b11b | ||
|
|
04682c9d91 | ||
|
|
50fdc449ac | ||
|
|
b8c295727a | ||
|
|
93ee466780 | ||
|
|
77f1e8f8c9 | ||
|
|
1aa746bea1 | ||
|
|
490041d77c | ||
|
|
5dc5592c17 | ||
|
|
8fb1aff68a | ||
|
|
a397a19034 | ||
|
|
d0df761422 | ||
|
|
4781675c1a | ||
|
|
0361262bb4 |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -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)'
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
18
frontend/src/App/State/AuthorsAppState.ts
Normal file
18
frontend/src/App/State/AuthorsAppState.ts
Normal 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;
|
||||
6
frontend/src/App/State/CommandAppState.ts
Normal file
6
frontend/src/App/State/CommandAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
||||
@@ -5,6 +5,7 @@ import AppSectionState, {
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import 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;
|
||||
|
||||
18
frontend/src/Author/Author.ts
Normal file
18
frontend/src/Author/Author.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface Author extends ModelBase {
|
||||
added: string;
|
||||
genres: string[];
|
||||
monitored: boolean;
|
||||
overview: string;
|
||||
path: string;
|
||||
qualityProfileId: number;
|
||||
metadataProfileId: number;
|
||||
rootFolderPath: string;
|
||||
sortName: string;
|
||||
tags: number[];
|
||||
authorName: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Author;
|
||||
@@ -2,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>
|
||||
);
|
||||
|
||||
@@ -27,3 +27,9 @@
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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') }
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'alternateTitlesIconContainer': string;
|
||||
'authorLink': string;
|
||||
'backdrop': string;
|
||||
'backdropOverlay': string;
|
||||
'cover': string;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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') }
|
||||
];
|
||||
|
||||
26
frontend/src/Book/IndexerFlags.tsx
Normal file
26
frontend/src/Book/IndexerFlags.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||
|
||||
interface IndexerFlagsProps {
|
||||
indexerFlags: number;
|
||||
}
|
||||
|
||||
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||
|
||||
const flags = allIndexerFlags.items.filter(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(item) => (indexerFlags & item.id) === item.id
|
||||
);
|
||||
|
||||
return flags.length ? (
|
||||
<ul>
|
||||
{flags.map((flag, index) => {
|
||||
return <li key={index}>{flag.name}</li>;
|
||||
})}
|
||||
</ul>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default IndexerFlags;
|
||||
@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
|
||||
}, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
|
||||
|
||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||
|
||||
|
||||
@@ -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') }
|
||||
];
|
||||
|
||||
38
frontend/src/Commands/Command.ts
Normal file
38
frontend/src/Commands/Command.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface CommandBody {
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
completionMessage: string;
|
||||
requiresDiskAccess: boolean;
|
||||
isExclusive: boolean;
|
||||
isLongRunning: boolean;
|
||||
name: string;
|
||||
lastExecutionTime: string;
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
authorId?: number;
|
||||
authorIds?: number[];
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
name: string;
|
||||
commandName: string;
|
||||
message: string;
|
||||
body: CommandBody;
|
||||
priority: string;
|
||||
status: string;
|
||||
result: string;
|
||||
queued: string;
|
||||
started: string;
|
||||
ended: string;
|
||||
duration: string;
|
||||
trigger: string;
|
||||
stateChangeTime: string;
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
lastExecutionTime: string;
|
||||
}
|
||||
|
||||
export default Command;
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
||||
@@ -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),
|
||||
|
||||
62
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
62
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal 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;
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
margin-right: 5px;
|
||||
color: var(--themeRed);
|
||||
}
|
||||
|
||||
.rating {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
1
frontend/src/Components/HeartRating.css.d.ts
vendored
1
frontend/src/Components/HeartRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'heart': string;
|
||||
'rating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal 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];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector';
|
||||
|
||||
class SelectIndexerFlagsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectIndexerFlagsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerFlagsModal;
|
||||
@@ -0,0 +1,7 @@
|
||||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
7
frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts
vendored
Normal file
7
frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts
vendored
Normal 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;
|
||||
@@ -0,0 +1,106 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerFlagsModalContent.css';
|
||||
|
||||
class SelectIndexerFlagsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
indexerFlags
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
indexerFlags
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerFlagsChange = ({ value }) => {
|
||||
this.setState({ indexerFlags: value });
|
||||
};
|
||||
|
||||
onIndexerFlagsSelect = () => {
|
||||
this.props.onIndexerFlagsSelect(this.state);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
indexerFlags
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Set indexer Flags
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('IndexerFlags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_FLAGS_SELECT}
|
||||
name="indexerFlags"
|
||||
indexerFlags={indexerFlags}
|
||||
autoFocus={true}
|
||||
onChange={this.onIndexerFlagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onIndexerFlagsSelect}
|
||||
>
|
||||
{translate('SetIndexerFlags')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModalContent.propTypes = {
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
onIndexerFlagsSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerFlagsModalContent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
|
||||
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectIndexerFlagsModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerFlagsSelect = ({ indexerFlags }) => {
|
||||
const {
|
||||
ids,
|
||||
dispatchUpdateInteractiveImportItems,
|
||||
dispatchSaveInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
dispatchUpdateInteractiveImportItems({
|
||||
ids,
|
||||
indexerFlags
|
||||
});
|
||||
|
||||
dispatchSaveInteractiveImportItems({ ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectIndexerFlagsModalContent
|
||||
{...this.props}
|
||||
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);
|
||||
@@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
|
||||
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
|
||||
import 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}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
}
|
||||
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'customFormatScore': string;
|
||||
'download': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'peers': string;
|
||||
'protocol': string;
|
||||
'quality': string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -32,7 +32,7 @@ const autoAddOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -32,7 +32,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
48
frontend/src/Store/Actions/Settings/indexerFlags.js
Normal file
48
frontend/src/Store/Actions/Settings/indexerFlags.js
Normal 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: {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
14
frontend/src/Store/Selectors/createMultiAuthorsSelector.ts
Normal file
14
frontend/src/Store/Selectors/createMultiAuthorsSelector.ts
Normal 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;
|
||||
@@ -10,15 +10,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.queued,
|
||||
.started,
|
||||
.ended {
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'commandName': string;
|
||||
'duration': string;
|
||||
'ended': string;
|
||||
'queued': string;
|
||||
'started': string;
|
||||
'trigger': string;
|
||||
'triggerContent': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status, message) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(props) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
} = props;
|
||||
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
||||
};
|
||||
}
|
||||
|
||||
class QueuedTaskRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
...getFormattedDates(props),
|
||||
isCancelConfirmModalOpen: false
|
||||
};
|
||||
|
||||
this._updateTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setUpdateTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
queued,
|
||||
started,
|
||||
ended
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
queued !== prevProps.queued ||
|
||||
started !== prevProps.started ||
|
||||
ended !== prevProps.ended
|
||||
) {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._updateTimeoutId) {
|
||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setUpdateTimer() {
|
||||
this._updateTimeoutId = setTimeout(() => {
|
||||
this.setState(getFormattedDates(this.props));
|
||||
this.setUpdateTimer();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onAbortCancel = () => {
|
||||
this.setState({
|
||||
isCancelConfirmModalOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
clientUserAgent,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onCancelPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
queuedAt,
|
||||
startedAt,
|
||||
endedAt,
|
||||
isCancelConfirmModalOpen
|
||||
} = this.state;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon
|
||||
name={triggerIcon}
|
||||
title={titleCase(trigger)}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
{...getStatusIconProps(status, message)}
|
||||
/>
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<span className={styles.commandName}>
|
||||
{commandName}
|
||||
</span>
|
||||
{
|
||||
clientUserAgent ?
|
||||
<span className={styles.userAgent} title={translate('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;
|
||||
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
238
frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CommandBody } from 'Commands/Command';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||
import styles from './QueuedTaskRow.css';
|
||||
|
||||
function getStatusIconProps(status: string, message: string | undefined) {
|
||||
const title = titleCase(status);
|
||||
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return {
|
||||
name: icons.PENDING,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'started':
|
||||
return {
|
||||
name: icons.REFRESH,
|
||||
isSpinning: true,
|
||||
title,
|
||||
};
|
||||
|
||||
case 'completed':
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS,
|
||||
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||
};
|
||||
|
||||
case 'failed':
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kind: kinds.DANGER,
|
||||
title: `${title}: ${message}`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
name: icons.UNKNOWN,
|
||||
title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFormattedDates(
|
||||
queued: string,
|
||||
started: string | undefined,
|
||||
ended: string | undefined,
|
||||
showRelativeDates: boolean,
|
||||
shortDateFormat: string
|
||||
) {
|
||||
if (showRelativeDates) {
|
||||
return {
|
||||
queuedAt: moment(queued).fromNow(),
|
||||
startedAt: started ? moment(started).fromNow() : '-',
|
||||
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queuedAt: formatDate(queued, shortDateFormat),
|
||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||
};
|
||||
}
|
||||
|
||||
interface QueuedTimes {
|
||||
queuedAt: string;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
}
|
||||
|
||||
export interface QueuedTaskRowProps {
|
||||
id: number;
|
||||
trigger: string;
|
||||
commandName: string;
|
||||
queued: string;
|
||||
started?: string;
|
||||
ended?: string;
|
||||
status: string;
|
||||
duration?: string;
|
||||
message?: string;
|
||||
body: CommandBody;
|
||||
clientUserAgent?: string;
|
||||
}
|
||||
|
||||
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||
const {
|
||||
id,
|
||||
trigger,
|
||||
commandName,
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
status,
|
||||
duration,
|
||||
message,
|
||||
body,
|
||||
clientUserAgent,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const [times, setTimes] = useState<QueuedTimes>(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
|
||||
const [
|
||||
isCancelConfirmModalOpen,
|
||||
openCancelConfirmModal,
|
||||
closeCancelConfirmModal,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
dispatch(cancelCommand({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimeTimeoutId.current = setTimeout(() => {
|
||||
setTimes(
|
||||
getFormattedDates(
|
||||
queued,
|
||||
started,
|
||||
ended,
|
||||
showRelativeDates,
|
||||
shortDateFormat
|
||||
)
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (updateTimeTimeoutId.current) {
|
||||
clearTimeout(updateTimeTimeoutId.current);
|
||||
}
|
||||
};
|
||||
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||
|
||||
const { queuedAt, startedAt, endedAt } = times;
|
||||
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
} else if (trigger === 'scheduled') {
|
||||
triggerIcon = icons.SCHEDULED;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.trigger}>
|
||||
<span className={styles.triggerContent}>
|
||||
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||
|
||||
<Icon {...getStatusIconProps(status, message)} />
|
||||
</span>
|
||||
</TableRowCell>
|
||||
|
||||
<QueuedTaskRowNameCell
|
||||
commandName={commandName}
|
||||
body={body}
|
||||
clientUserAgent={clientUserAgent}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.queued}
|
||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||
>
|
||||
{queuedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.started}
|
||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||
>
|
||||
{startedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.ended}
|
||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||
>
|
||||
{endedAt}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.duration}>
|
||||
{formatTimeSpan(duration)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{status === 'queued' && (
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={openCancelConfirmModal}
|
||||
/>
|
||||
)}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isCancelConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('Cancel')}
|
||||
message={translate('CancelPendingTask')}
|
||||
confirmLabel={translate('YesCancel')}
|
||||
cancelLabel={translate('NoLeaveIt')}
|
||||
onConfirm={handleCancelPress}
|
||||
onCancel={closeCancelConfirmModal}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onCancelPress() {
|
||||
dispatch(cancelCommand({
|
||||
id: props.id
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
||||
@@ -0,0 +1,8 @@
|
||||
.commandName {
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
8
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'commandName': string;
|
||||
'userAgent': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
49
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal file
49
frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function QueuedTasks(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueuedTaskRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
QueuedTasks.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default QueuedTasks;
|
||||
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
74
frontend/src/System/Tasks/Queued/QueuedTasks.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTaskRow from './QueuedTaskRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'trigger',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'commandName',
|
||||
label: () => translate('Name'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'queued',
|
||||
label: () => translate('Queued'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'started',
|
||||
label: () => translate('Started'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'ended',
|
||||
label: () => translate('Ended'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function QueuedTasks() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, items } = useSelector(
|
||||
(state: AppState) => state.commands
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCommands());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Queue')}>
|
||||
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return <QueuedTaskRow key={item.id} {...item} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||
import QueuedTasks from './QueuedTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(commands) => {
|
||||
return commands;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCommands: fetchCommands
|
||||
};
|
||||
|
||||
class QueuedTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueuedTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueuedTasksConnector.propTypes = {
|
||||
dispatchFetchCommands: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||
import QueuedTasks from './Queued/QueuedTasks';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
|
||||
function Tasks() {
|
||||
@@ -10,7 +10,7 @@ function Tasks() {
|
||||
<PageContent title={translate('Tasks')}>
|
||||
<PageContentBody>
|
||||
<ScheduledTasksConnector />
|
||||
<QueuedTasksConnector />
|
||||
<QueuedTasks />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
||||
6
frontend/src/typings/IndexerFlag.ts
Normal file
6
frontend/src/typings/IndexerFlag.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
interface IndexerFlag {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default IndexerFlag;
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class IndexerFlagSpecificationValidator : AbstractValidator<IndexerFlagSpecification>
|
||||
{
|
||||
public IndexerFlagSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty();
|
||||
RuleFor(c => c.Value).Custom((flag, context) =>
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(IndexerFlags), flag))
|
||||
{
|
||||
context.AddFailure($"Invalid indexer flag condition value: {flag}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexerFlagSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly IndexerFlagSpecificationValidator Validator = new ();
|
||||
|
||||
public override int Order => 4;
|
||||
public override string ImplementationName => "Indexer Flag";
|
||||
|
||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationFlag", Type = FieldType.Select, SelectOptions = typeof(IndexerFlags))]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
return input.IndexerFlags.HasFlag((IndexerFlags)Value);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(040)]
|
||||
public class add_indexer_flags : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Blocklist").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
|
||||
Alter.Table("BookFiles").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using NzbDrone.Core.Download.History;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers
|
||||
public class RssSyncCommand : Command
|
||||
{
|
||||
public override bool SendUpdatesToClient => true;
|
||||
|
||||
public override bool IsLongRunning => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -631,5 +631,10 @@
|
||||
"ExtraFileExtensionsHelpTextsExamples": "أمثلة: \".sub أو .nfo\" أو \"sub، nfo\"",
|
||||
"AutoRedownloadFailed": "التحميل فشل",
|
||||
"SourceTitle": "عنوان المصدر",
|
||||
"RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟"
|
||||
"RemoveQueueItemConfirmation": "هل تريد بالتأكيد إزالة {0} عنصر {1} من قائمة الانتظار؟",
|
||||
"ImportLists": "القوائم",
|
||||
"ListsSettingsSummary": "القوائم",
|
||||
"SelectDropdown": "'تحديد...",
|
||||
"SelectQuality": "حدد الجودة",
|
||||
"CustomFilter": "مرشحات مخصصة"
|
||||
}
|
||||
|
||||
@@ -630,5 +630,11 @@
|
||||
"ExtraFileExtensionsHelpText": "Списък с допълнителни файлове за импортиране, разделени със запетая (.nfo ще бъде импортиран като .nfo-orig)",
|
||||
"ExtraFileExtensionsHelpTextsExamples": "Примери: '.sub, .nfo' или 'sub, nfo'",
|
||||
"AutoRedownloadFailed": "Изтеглянето се провали",
|
||||
"SourceTitle": "Заглавие на източника"
|
||||
"SourceTitle": "Заглавие на източника",
|
||||
"ImportLists": "Списъци",
|
||||
"ListsSettingsSummary": "Списъци",
|
||||
"SelectDropdown": "„Изберете ...",
|
||||
"SelectQuality": "Изберете Качество",
|
||||
"CustomFilter": "Персонализирани филтри",
|
||||
"RemoveQueueItemConfirmation": "Наистина ли искате да премахнете {0} елемент {1} от опашката?"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 Añ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 de póster",
|
||||
"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
Reference in New Issue
Block a user