mirror of
https://github.com/Readarr/Readarr
synced 2026-01-09 09:04:15 +01:00
New: Indexer flags
(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
This commit is contained in:
parent
4781675c1a
commit
d0df761422
59 changed files with 877 additions and 29 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
@ -51,6 +58,7 @@ 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,
|
||||
(
|
||||
|
|
@ -61,6 +69,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated,
|
||||
metadataProfilesIsPopulated,
|
||||
importListsIsPopulated,
|
||||
indexerFlagsIsPopulated,
|
||||
systemStatusIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
|
|
@ -72,6 +81,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated &&
|
||||
metadataProfilesIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
indexerFlagsIsPopulated &&
|
||||
systemStatusIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
|
|
@ -86,6 +96,7 @@ 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,
|
||||
(
|
||||
|
|
@ -96,6 +107,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
) => {
|
||||
|
|
@ -107,6 +119,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError ||
|
||||
metadataProfilesError ||
|
||||
importListsError ||
|
||||
indexerFlagsError ||
|
||||
systemStatusError ||
|
||||
translationsError
|
||||
);
|
||||
|
|
@ -120,6 +133,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
};
|
||||
|
|
@ -177,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchImportLists() {
|
||||
dispatch(fetchImportLists());
|
||||
},
|
||||
dispatchFetchIndexerFlags() {
|
||||
dispatch(fetchIndexerFlags());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
|
|
@ -218,6 +235,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 +263,7 @@ class PageConnector extends Component {
|
|||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchMetadataProfiles,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchIndexerFlags,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
|
|
@ -287,6 +306,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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;
|
||||
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;
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
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 @@ public void Setup()
|
|||
.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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
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 @@ public class Blocklist : ModelBase
|
|||
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 @@ public void Handle(DownloadFailedEvent message)
|
|||
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash")
|
||||
};
|
||||
|
||||
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
||||
{
|
||||
blocklist.IndexerFlags = flags;
|
||||
}
|
||||
|
||||
_blocklistRepository.Insert(blocklist);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -38,7 +39,8 @@ public List<CustomFormat> ParseCustomFormat(RemoteBook remoteBook, long size)
|
|||
{
|
||||
BookInfo = remoteBook.ParsedBookInfo,
|
||||
Author = remoteBook.Author,
|
||||
Size = size
|
||||
Size = size,
|
||||
IndexerFlags = remoteBook.Release?.IndexerFlags ?? 0
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
|
@ -70,7 +72,8 @@ public List<CustomFormat> ParseCustomFormat(Blocklist blocklist, Author author)
|
|||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = blocklist.Size ?? 0
|
||||
Size = blocklist.Size ?? 0,
|
||||
IndexerFlags = blocklist.IndexerFlags
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
|
@ -81,6 +84,7 @@ public List<CustomFormat> ParseCustomFormat(EntityHistory history, Author author
|
|||
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 @@ public List<CustomFormat> ParseCustomFormat(EntityHistory history, Author author
|
|||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = size
|
||||
Size = size,
|
||||
IndexerFlags = indexerFlags
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
|
@ -114,7 +119,8 @@ public List<CustomFormat> ParseCustomFormat(LocalBook localBook)
|
|||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = localBook.Author,
|
||||
Size = localBook.Size
|
||||
Size = localBook.Size,
|
||||
IndexerFlags = localBook.IndexerFlags,
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
|
@ -181,6 +187,7 @@ private List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, L
|
|||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = bookFile.Size,
|
||||
IndexerFlags = bookFile.IndexerFlags,
|
||||
Filename = Path.GetFileName(bookFile.Path)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public class CustomFormatInput
|
|||
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.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
{
|
||||
|
|
@ -156,11 +157,10 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
|||
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 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public void Handle(BookGrabbedEvent message)
|
|||
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 @@ public void Handle(BookImportIncompleteEvent message)
|
|||
|
||||
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 @@ public void Handle(TrackImportedEvent message)
|
|||
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 @@ public void Handle(BookFileDeletedEvent message)
|
|||
|
||||
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 @@ public void Handle(BookFileRenamedEvent message)
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|||
{
|
||||
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 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|||
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 @@ public class FileListTorrent
|
|||
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 @@ public class GazelleTorrent
|
|||
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 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|||
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 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|||
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 @@ public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|||
.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)
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ protected override bool PostProcess(IndexerResponse indexerResponse, List<XEleme
|
|||
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 @@ protected override string GetMagnetUrl(XElement item)
|
|||
return base.GetPeers(item);
|
||||
}
|
||||
|
||||
protected IndexerFlags GetFlags(XElement item)
|
||||
{
|
||||
IndexerFlags flags = 0;
|
||||
|
||||
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
|
||||
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
|
||||
|
||||
if (downloadFactor == 0.5)
|
||||
{
|
||||
flags |= IndexerFlags.Halfleech;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.75)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech25;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.25)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech75;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.0)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech;
|
||||
}
|
||||
|
||||
if (uploadFactor == 2.0)
|
||||
{
|
||||
flags |= IndexerFlags.DoubleUpload;
|
||||
}
|
||||
|
||||
var tags = TryGetMultipleTorznabAttributes(item, "tag");
|
||||
|
||||
if (tags.Any(t => t.EqualsIgnoreCase("internal")))
|
||||
{
|
||||
flags |= IndexerFlags.Internal;
|
||||
}
|
||||
|
||||
if (tags.Any(t => t.EqualsIgnoreCase("scene")))
|
||||
{
|
||||
flags |= IndexerFlags.Scene;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "")
|
||||
{
|
||||
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
|
|
@ -209,6 +268,13 @@ protected string TryGetTorznabAttribute(XElement item, string key, string defaul
|
|||
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));
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class BookFile : ModelBase
|
|||
public string SceneName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public int EditionId { get; set; }
|
||||
public int CalibreId { get; set; }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Extras;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
|
@ -44,6 +45,7 @@ public class ImportApprovedBooks : IImportApprovedBooks
|
|||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly IExtraService _extraService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly Logger _logger;
|
||||
|
|
@ -59,6 +61,7 @@ public ImportApprovedBooks(IUpgradeMediaFiles bookFileUpgrader,
|
|||
IRecycleBinProvider recycleBinProvider,
|
||||
IExtraService extraService,
|
||||
IDiskProvider diskProvider,
|
||||
IHistoryService historyService,
|
||||
IEventAggregator eventAggregator,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
|
|
@ -74,6 +77,7 @@ public ImportApprovedBooks(IUpgradeMediaFiles bookFileUpgrader,
|
|||
_recycleBinProvider = recycleBinProvider;
|
||||
_extraService = extraService;
|
||||
_diskProvider = diskProvider;
|
||||
_historyService = historyService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
|
|
@ -193,6 +197,22 @@ public List<ImportResult> Import(List<ImportDecision<LocalBook>> decisions, bool
|
|||
Edition = localTrack.Edition
|
||||
};
|
||||
|
||||
if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true)
|
||||
{
|
||||
var grabHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.FirstOrDefault(h => h.EventType == EntityHistoryEventType.Grabbed);
|
||||
|
||||
if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
||||
{
|
||||
bookFile.IndexerFlags = flags;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bookFile.IndexerFlags = localTrack.IndexerFlags;
|
||||
}
|
||||
|
||||
bool copyOnly;
|
||||
switch (importMode)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public class ManualImportFile : IEquatable<ManualImportFile>
|
|||
public int BookId { get; set; }
|
||||
public string ForeignEditionId { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public bool DisableReleaseSwitching { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public ManualImportItem()
|
|||
public string ReleaseGroup { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public List<CustomFormat> CustomFormats { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo Tags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ private ManualImportItem MapItem(ImportDecision<LocalBook> decision, string down
|
|||
}
|
||||
|
||||
item.Quality = decision.Item.Quality;
|
||||
item.IndexerFlags = (int)decision.Item.IndexerFlags;
|
||||
item.Size = _diskProvider.GetFileSize(decision.Item.Path);
|
||||
item.Rejections = decision.Rejections;
|
||||
item.Tags = decision.Item.FileTrackInfo;
|
||||
|
|
@ -345,6 +346,7 @@ public void Execute(ManualImportCommand message)
|
|||
Size = fileInfo.Length,
|
||||
Modified = fileInfo.LastWriteTimeUtc,
|
||||
Quality = file.Quality,
|
||||
IndexerFlags = (IndexerFlags)file.IndexerFlags,
|
||||
Author = author,
|
||||
Book = book,
|
||||
Edition = edition
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ public override void OnGrab(GrabMessage message)
|
|||
environmentVariables.Add("Readarr_Release_Quality", remoteBook.ParsedBookInfo.Quality.Quality.Name);
|
||||
environmentVariables.Add("Readarr_Release_QualityVersion", remoteBook.ParsedBookInfo.Quality.Revision.Version.ToString());
|
||||
environmentVariables.Add("Readarr_Release_ReleaseGroup", releaseGroup ?? string.Empty);
|
||||
environmentVariables.Add("Readarr_Release_IndexerFlags", remoteBook.Release.IndexerFlags.ToString());
|
||||
environmentVariables.Add("Readarr_Download_Client", message.DownloadClientName ?? string.Empty);
|
||||
environmentVariables.Add("Readarr_Download_Client_Type", message.DownloadClientType ?? string.Empty);
|
||||
environmentVariables.Add("Readarr_Download_Id", message.DownloadId ?? string.Empty);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public class LocalBook
|
|||
public Edition Edition { get; set; }
|
||||
public Distance Distance { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public bool ExistingFile { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool SceneSource { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
|
@ -37,6 +37,9 @@ public ReleaseInfo()
|
|||
|
||||
public List<Language> Languages { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
|
||||
// Used to track pending releases that are being reprocessed
|
||||
[JsonIgnore]
|
||||
public PendingReleaseReason? PendingReleaseReason { get; set; }
|
||||
|
|
@ -85,4 +88,16 @@ public virtual string ToString(string format)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum IndexerFlags
|
||||
{
|
||||
Freeleech = 1, // General
|
||||
Halfleech = 2, // General, only 1/2 of download counted
|
||||
DoubleUpload = 4, // General
|
||||
Internal = 8, // General, uploader is an internal release group
|
||||
Scene = 16, // General, the torrent comes from a "scene" group
|
||||
Freeleech75 = 32, // Signifies a torrent counts towards 75 percent of your download quota.
|
||||
Freeleech25 = 64, // Signifies a torrent counts towards 25 percent of your download quota.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public class BookFileResource : RestResource
|
|||
public DateTime DateAdded { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public int? IndexerFlags { get; set; }
|
||||
public MediaInfoResource MediaInfo { get; set; }
|
||||
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
|
|
@ -77,7 +78,8 @@ public static BookFileResource ToResource(this BookFile model, NzbDrone.Core.Boo
|
|||
Quality = model.Quality,
|
||||
QualityWeight = QualityWeight(model.Quality),
|
||||
MediaInfo = model.MediaInfo.ToResource(),
|
||||
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(author.QualityProfile.Value, model.Quality)
|
||||
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(author.QualityProfile.Value, model.Quality),
|
||||
IndexerFlags = (int)model.IndexerFlags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
src/Readarr.Api.V1/Indexers/IndexerFlagController.cs
Normal file
23
src/Readarr.Api.V1/Indexers/IndexerFlagController.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Readarr.Http;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
[V1ApiController]
|
||||
public class IndexerFlagController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public List<IndexerFlagResource> GetAll()
|
||||
{
|
||||
return Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource
|
||||
{
|
||||
Id = (int)f,
|
||||
Name = f.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Readarr.Api.V1/Indexers/IndexerFlagResource.cs
Normal file
13
src/Readarr.Api.V1/Indexers/IndexerFlagResource.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Newtonsoft.Json;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerFlagResource : RestResource
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
|
||||
public new int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string NameLower => Name.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ public class ReleaseResource : RestResource
|
|||
public int? Seeders { get; set; }
|
||||
public int? Leechers { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
|
||||
// Sent when queuing an unknown release
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
|
|
@ -72,6 +73,7 @@ public static ReleaseResource ToResource(this DownloadDecision model)
|
|||
var parsedBookInfo = model.RemoteBook.ParsedBookInfo;
|
||||
var remoteBook = model.RemoteBook;
|
||||
var torrentInfo = (model.RemoteBook.Release as TorrentInfo) ?? new TorrentInfo();
|
||||
var indexerFlags = torrentInfo.IndexerFlags;
|
||||
|
||||
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?)
|
||||
return new ReleaseResource
|
||||
|
|
@ -111,6 +113,7 @@ public static ReleaseResource ToResource(this DownloadDecision model)
|
|||
Seeders = torrentInfo.Seeders,
|
||||
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
|
||||
Protocol = releaseInfo.DownloadProtocol,
|
||||
IndexerFlags = (int)indexerFlags,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ private List<ManualImportResource> UpdateImportItems(List<ManualImportUpdateReso
|
|||
Edition = resource.ForeignEditionId == null ? null : _editionService.GetEditionByForeignEditionId(resource.ForeignEditionId),
|
||||
Quality = resource.Quality,
|
||||
ReleaseGroup = resource.ReleaseGroup,
|
||||
IndexerFlags = resource.IndexerFlags,
|
||||
DownloadId = resource.DownloadId,
|
||||
AdditionalFile = resource.AdditionalFile,
|
||||
ReplaceExistingFiles = resource.ReplaceExistingFiles,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class ManualImportResource : RestResource
|
|||
public string ReleaseGroup { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo AudioTags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
|
|
@ -52,7 +53,9 @@ public static ManualImportResource ToResource(this ManualImportItem model)
|
|||
|
||||
//QualityWeight
|
||||
DownloadId = model.DownloadId,
|
||||
IndexerFlags = model.IndexerFlags,
|
||||
Rejections = model.Rejections,
|
||||
|
||||
AudioTags = model.Tags,
|
||||
AdditionalFile = model.AdditionalFile,
|
||||
ReplaceExistingFiles = model.ReplaceExistingFiles,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public class ManualImportUpdateResource : RestResource
|
|||
public string ForeignEditionId { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool ReplaceExistingFiles { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue