New: Indexer flags

(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
pull/3356/head
Bogdan 3 months ago
parent 4781675c1a
commit d0df761422

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

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

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

@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector'; import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink'; import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating'; import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookStatus from './BookStatus'; import BookStatus from './BookStatus';
import styles from './BookRow.css'; import styles from './BookRow.css';
@ -67,6 +72,7 @@ class BookRow extends Component {
authorMonitored, authorMonitored,
titleSlug, titleSlug,
bookFiles, bookFiles,
indexerFlags,
isEditorActive, isEditorActive,
isSelected, isSelected,
onSelectedChange, onSelectedChange,
@ -190,6 +196,24 @@ class BookRow extends Component {
); );
} }
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<TableRowCell <TableRowCell
@ -235,6 +259,7 @@ BookRow.propTypes = {
position: PropTypes.string, position: PropTypes.string,
pageCount: PropTypes.number, pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired, authorMonitored: PropTypes.bool.isRequired,
@ -246,4 +271,8 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired onMonitorBookPress: PropTypes.func.isRequired
}; };
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow; export default BookRow;

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

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

@ -0,0 +1,62 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;

@ -7,7 +7,14 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchMetadataProfiles,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -51,6 +58,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated, (state) => state.app.translations.isPopulated,
( (
@ -61,6 +69,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated,
translationsIsPopulated translationsIsPopulated
) => { ) => {
@ -72,6 +81,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated &&
translationsIsPopulated translationsIsPopulated
); );
@ -86,6 +96,7 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error, (state) => state.app.translations.error,
( (
@ -96,6 +107,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
) => { ) => {
@ -107,6 +119,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
indexerFlagsError ||
systemStatusError || systemStatusError ||
translationsError translationsError
); );
@ -120,6 +133,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
}; };
@ -177,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() { dispatchFetchImportLists() {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
}, },
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() { dispatchFetchUISettings() {
dispatch(fetchUISettings()); dispatch(fetchUISettings());
}, },
@ -218,6 +235,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles(); this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations(); this.props.dispatchFetchTranslations();
@ -245,6 +263,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles, dispatchFetchMetadataProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations, dispatchFetchTranslations,
@ -287,6 +306,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired, dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired, dispatchFetchTranslations: PropTypes.func.isRequired,

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

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

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector';
class SelectIndexerFlagsModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectIndexerFlagsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectIndexerFlagsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModal;

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

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

@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
class SelectIndexerFlagsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
indexerFlags
} = props;
this.state = {
indexerFlags
};
}
//
// Listeners
onIndexerFlagsChange = ({ value }) => {
this.setState({ indexerFlags: value });
};
onIndexerFlagsSelect = () => {
this.props.onIndexerFlagsSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
indexerFlags
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Set indexer Flags
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>
{translate('IndexerFlags')}
</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={this.onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onIndexerFlagsSelect}
>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectIndexerFlagsModalContent.propTypes = {
indexerFlags: PropTypes.number.isRequired,
onIndexerFlagsSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModalContent;

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectIndexerFlagsModalContentConnector extends Component {
//
// Listeners
onIndexerFlagsSelect = ({ indexerFlags }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
indexerFlags
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectIndexerFlagsModalContent
{...this.props}
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
/>
);
}
}
SelectIndexerFlagsModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);

@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal'; import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal'; import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow'; import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css'; import styles from './InteractiveImportModalContent.css';
const columns = [ const COLUMNS = [
{ {
name: 'path', name: 'path',
label: 'Path', label: 'Path',
@ -74,11 +75,21 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.DANGER, name: icons.DANGER,
kind: kinds.DANGER kind: kinds.DANGER,
title: () => translate('Rejections')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@ -102,6 +113,7 @@ const BOOK = 'book';
const EDITION = 'edition'; const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup'; const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality'; const QUALITY = 'quality';
const INDEXER_FLAGS = 'indexerFlags';
const replaceExistingFilesOptions = { const replaceExistingFilesOptions = {
COMBINE: 'combine', COMBINE: 'combine',
@ -288,6 +300,21 @@ class InteractiveImportModalContent extends Component {
inconsistentBookReleases inconsistentBookReleases
} = this.state; } = this.state;
const allColumns = _.cloneDeep(COLUMNS);
const columns = allColumns.map((column) => {
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return column;
});
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value(); const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value();
@ -299,7 +326,8 @@ class InteractiveImportModalContent extends Component {
{ key: BOOK, value: translate('SelectBook') }, { key: BOOK, value: translate('SelectBook') },
{ key: EDITION, value: translate('SelectEdition') }, { key: EDITION, value: translate('SelectEdition') },
{ key: QUALITY, value: translate('SelectQuality') }, { key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') } { key: RELEASE_GROUP, value: translate('SelectReleaseGroup') },
{ key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') }
]; ];
if (allowAuthorChange) { if (allowAuthorChange) {
@ -422,6 +450,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving} isSaving={isSaving}
{...item} {...item}
allowAuthorChange={allowAuthorChange} allowAuthorChange={allowAuthorChange}
columns={columns}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange} onValidRowChange={this.onValidRowChange}
/> />
@ -518,6 +547,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose} onModalClose={this.onSelectModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={selectModalOpen === INDEXER_FLAGS}
ids={selectedIds}
indexerFlags={0}
onModalClose={this.onSelectModalClose}
/>
<ConfirmImportModal <ConfirmImportModal
isOpen={isConfirmImportModalOpen} isOpen={isConfirmImportModalOpen}
books={booksImported} books={booksImported}

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

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

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

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

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

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import BookFormats from 'Book/BookFormats'; import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality'; import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality, quality,
customFormatScore, customFormatScore,
customFormats, customFormats,
indexerFlags = 0,
rejections, rejections,
downloadAllowed, downloadAllowed,
isGrabbing, isGrabbing,
@ -189,10 +191,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<BookFormats formats={customFormats} />} tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.LEFT}
/> />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}> <TableRowCell className={styles.rejected}>
{ {
!!rejections.length && !!rejections.length &&
@ -265,6 +278,7 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired, customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired, downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
@ -277,6 +291,7 @@ InteractiveSearchRow.propTypes = {
}; };
InteractiveSearchRow.defaultProps = { InteractiveSearchRow.defaultProps = {
indexerFlags: 0,
rejections: [], rejections: [],
isGrabbing: false, isGrabbing: false,
isGrabbed: false isGrabbed: false

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

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

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

@ -0,0 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;

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

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

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

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

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

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

@ -0,0 +1,44 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.CustomFormats
{
public class IndexerFlagSpecificationValidator : AbstractValidator<IndexerFlagSpecification>
{
public IndexerFlagSpecificationValidator()
{
RuleFor(c => c.Value).NotEmpty();
RuleFor(c => c.Value).Custom((flag, context) =>
{
if (!Enum.IsDefined(typeof(IndexerFlags), flag))
{
context.AddFailure($"Invalid indexer flag condition value: {flag}");
}
});
}
}
public class IndexerFlagSpecification : CustomFormatSpecificationBase
{
private static readonly IndexerFlagSpecificationValidator Validator = new ();
public override int Order => 4;
public override string ImplementationName => "Indexer Flag";
[FieldDefinition(1, Label = "CustomFormatsSpecificationFlag", Type = FieldType.Select, SelectOptions = typeof(IndexerFlags))]
public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
{
return input.IndexerFlags.HasFlag((IndexerFlags)Value);
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(040)]
public class add_indexer_flags : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Blocklist").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
Alter.Table("BookFiles").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
}
}
}

@ -12,6 +12,7 @@ using NzbDrone.Core.Download.History;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.TrackedDownloads namespace NzbDrone.Core.Download.TrackedDownloads
{ {
@ -156,11 +157,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
var firstHistoryItem = historyItems.First(); var firstHistoryItem = historyItems.First();
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
if (parsedBookInfo == null || if (parsedBookInfo == null ||
trackedDownload.RemoteBook == null || trackedDownload.RemoteBook?.Author == null ||
trackedDownload.RemoteBook.Author == null ||
trackedDownload.RemoteBook.Books.Empty()) trackedDownload.RemoteBook.Books.Empty())
{ {
// Try parsing the original source title and if that fails, try parsing it as a special // Try parsing the original source title and if that fails, try parsing it as a special
@ -192,6 +192,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
} }
if (trackedDownload.RemoteBook != null &&
Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
trackedDownload.RemoteBook.Release ??= new ReleaseInfo();
trackedDownload.RemoteBook.Release.IndexerFlags = flags;
}
} }
// Calculate custom formats // Calculate custom formats

@ -164,6 +164,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString()); history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString());
history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString()); history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString());
history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString()); history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString());
history.Data.Add("IndexerFlags", message.Book.Release.IndexerFlags.ToString());
if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace()) if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace())
{ {
@ -201,6 +202,8 @@ namespace NzbDrone.Core.History
history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson()); history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson());
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
history.Data.Add("IndexerFlags", message.TrackedDownload?.RemoteBook?.Release?.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
} }
@ -237,6 +240,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name); history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup);
history.Data.Add("Size", message.BookInfo.Size.ToString()); history.Data.Add("Size", message.BookInfo.Size.ToString());
history.Data.Add("IndexerFlags", message.BookInfo.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -290,6 +294,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -313,6 +318,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Path", path); history.Data.Add("Path", path);
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
history.Data.Add("Size", message.BookFile.Size.ToString()); history.Data.Add("Size", message.BookFile.Size.ToString());
history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }

@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
{ {
var id = result.Id; var id = result.Id;
//if (result.FreeLeech) torrentInfos.Add(new TorrentInfo
torrentInfos.Add(new TorrentInfo()
{ {
Guid = $"FileList-{id}", Guid = $"FileList-{id}",
Title = result.Name, Title = result.Name,
@ -48,13 +47,31 @@ namespace NzbDrone.Core.Indexers.FileList
InfoUrl = GetInfoUrl(id), InfoUrl = GetInfoUrl(id),
Seeders = result.Seeders, Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders, Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate.ToUniversalTime() PublishDate = result.UploadDate.ToUniversalTime(),
IndexerFlags = GetIndexerFlags(result)
}); });
} }
return torrentInfos.ToArray(); return torrentInfos.ToArray();
} }
private static IndexerFlags GetIndexerFlags(FileListTorrent item)
{
IndexerFlags flags = 0;
if (item.FreeLeech)
{
flags |= IndexerFlags.Freeleech;
}
if (item.Internal)
{
flags |= IndexerFlags.Internal;
}
return flags;
}
private string GetDownloadUrl(string torrentId) private string GetDownloadUrl(string torrentId)
{ {
var url = new HttpUri(_settings.BaseUrl) var url = new HttpUri(_settings.BaseUrl)

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

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

@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
var author = WebUtility.HtmlDecode(result.Author); var author = WebUtility.HtmlDecode(result.Author);
var book = WebUtility.HtmlDecode(result.GroupName); var book = WebUtility.HtmlDecode(result.GroupName);
torrentInfos.Add(new GazelleInfo() torrentInfos.Add(new GazelleInfo
{ {
Guid = string.Format("Gazelle-{0}", id), Guid = string.Format("Gazelle-{0}", id),
Author = author, Author = author,
@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
Seeders = int.Parse(torrent.Seeders), Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(), PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene, IndexerFlags = GetIndexerFlags(torrent)
}); });
} }
} }
@ -88,6 +88,23 @@ namespace NzbDrone.Core.Indexers.Gazelle
.ToArray(); .ToArray();
} }
private static IndexerFlags GetIndexerFlags(GazelleTorrent torrent)
{
IndexerFlags flags = 0;
if (torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech)
{
flags |= IndexerFlags.Freeleech;
}
if (torrent.Scene)
{
flags |= IndexerFlags.Scene;
}
return flags;
}
private string GetDownloadUrl(int torrentId) private string GetDownloadUrl(int torrentId)
{ {
var url = new HttpUri(_settings.BaseUrl) var url = new HttpUri(_settings.BaseUrl)

@ -74,6 +74,18 @@ namespace NzbDrone.Core.Indexers.Torznab
return true; return true;
} }
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
{
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
if (torrentInfo != null)
{
torrentInfo.IndexerFlags = GetFlags(item);
}
return torrentInfo;
}
protected override string GetInfoUrl(XElement item) protected override string GetInfoUrl(XElement item)
{ {
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments")); return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
@ -194,6 +206,53 @@ namespace NzbDrone.Core.Indexers.Torznab
return base.GetPeers(item); return base.GetPeers(item);
} }
protected IndexerFlags GetFlags(XElement item)
{
IndexerFlags flags = 0;
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
if (downloadFactor == 0.5)
{
flags |= IndexerFlags.Halfleech;
}
if (downloadFactor == 0.75)
{
flags |= IndexerFlags.Freeleech25;
}
if (downloadFactor == 0.25)
{
flags |= IndexerFlags.Freeleech75;
}
if (downloadFactor == 0.0)
{
flags |= IndexerFlags.Freeleech;
}
if (uploadFactor == 2.0)
{
flags |= IndexerFlags.DoubleUpload;
}
var tags = TryGetMultipleTorznabAttributes(item, "tag");
if (tags.Any(t => t.EqualsIgnoreCase("internal")))
{
flags |= IndexerFlags.Internal;
}
if (tags.Any(t => t.EqualsIgnoreCase("scene")))
{
flags |= IndexerFlags.Scene;
}
return flags;
}
protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "") protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "")
{ {
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
@ -209,6 +268,13 @@ namespace NzbDrone.Core.Indexers.Torznab
return defaultValue; return defaultValue;
} }
protected float TryGetFloatTorznabAttribute(XElement item, string key, float defaultValue = 0)
{
var attr = TryGetTorznabAttribute(item, key, defaultValue.ToString());
return float.TryParse(attr, out var result) ? result : defaultValue;
}
protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key) protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key)
{ {
var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));

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

@ -18,6 +18,7 @@ namespace NzbDrone.Core.MediaFiles
public string SceneName { get; set; } public string SceneName { get; set; }
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public MediaInfoModel MediaInfo { get; set; } public MediaInfoModel MediaInfo { get; set; }
public int EditionId { get; set; } public int EditionId { get; set; }
public int CalibreId { get; set; } public int CalibreId { get; set; }

@ -14,6 +14,7 @@ using NzbDrone.Core.Books.Events;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Extras; using NzbDrone.Core.Extras;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -44,6 +45,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IExtraService _extraService; private readonly IExtraService _extraService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger; private readonly Logger _logger;
@ -59,6 +61,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
IRecycleBinProvider recycleBinProvider, IRecycleBinProvider recycleBinProvider,
IExtraService extraService, IExtraService extraService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IHistoryService historyService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager, IManageCommandQueue commandQueueManager,
Logger logger) Logger logger)
@ -74,6 +77,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_extraService = extraService; _extraService = extraService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_historyService = historyService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
_logger = logger; _logger = logger;
@ -193,6 +197,22 @@ namespace NzbDrone.Core.MediaFiles.BookImport
Edition = localTrack.Edition 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; bool copyOnly;
switch (importMode) switch (importMode)
{ {

@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
public int BookId { get; set; } public int BookId { get; set; }
public string ForeignEditionId { get; set; } public string ForeignEditionId { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public int IndexerFlags { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public bool DisableReleaseSwitching { get; set; } public bool DisableReleaseSwitching { get; set; }

@ -25,6 +25,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public List<CustomFormat> CustomFormats { get; set; } public List<CustomFormat> CustomFormats { get; set; }
public int IndexerFlags { get; set; }
public IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }
public ParsedTrackInfo Tags { get; set; } public ParsedTrackInfo Tags { get; set; }
public bool AdditionalFile { get; set; } public bool AdditionalFile { get; set; }

@ -286,6 +286,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
} }
item.Quality = decision.Item.Quality; item.Quality = decision.Item.Quality;
item.IndexerFlags = (int)decision.Item.IndexerFlags;
item.Size = _diskProvider.GetFileSize(decision.Item.Path); item.Size = _diskProvider.GetFileSize(decision.Item.Path);
item.Rejections = decision.Rejections; item.Rejections = decision.Rejections;
item.Tags = decision.Item.FileTrackInfo; item.Tags = decision.Item.FileTrackInfo;
@ -345,6 +346,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
Size = fileInfo.Length, Size = fileInfo.Length,
Modified = fileInfo.LastWriteTimeUtc, Modified = fileInfo.LastWriteTimeUtc,
Quality = file.Quality, Quality = file.Quality,
IndexerFlags = (IndexerFlags)file.IndexerFlags,
Author = author, Author = author,
Book = book, Book = book,
Edition = edition Edition = edition

@ -57,6 +57,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Readarr_Release_Quality", remoteBook.ParsedBookInfo.Quality.Quality.Name); 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_QualityVersion", remoteBook.ParsedBookInfo.Quality.Revision.Version.ToString());
environmentVariables.Add("Readarr_Release_ReleaseGroup", releaseGroup ?? string.Empty); 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", message.DownloadClientName ?? string.Empty);
environmentVariables.Add("Readarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); environmentVariables.Add("Readarr_Download_Client_Type", message.DownloadClientType ?? string.Empty);
environmentVariables.Add("Readarr_Download_Id", message.DownloadId ?? string.Empty); environmentVariables.Add("Readarr_Download_Id", message.DownloadId ?? string.Empty);

@ -23,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model
public Edition Edition { get; set; } public Edition Edition { get; set; }
public Distance Distance { get; set; } public Distance Distance { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public bool ExistingFile { get; set; } public bool ExistingFile { get; set; }
public bool AdditionalFile { get; set; } public bool AdditionalFile { get; set; }
public bool SceneSource { get; set; } public bool SceneSource { get; set; }

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Newtonsoft.Json; using System.Text.Json.Serialization;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
@ -37,6 +37,9 @@ namespace NzbDrone.Core.Parser.Model
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
[JsonIgnore]
public IndexerFlags IndexerFlags { get; set; }
// Used to track pending releases that are being reprocessed // Used to track pending releases that are being reprocessed
[JsonIgnore] [JsonIgnore]
public PendingReleaseReason? PendingReleaseReason { get; set; } public PendingReleaseReason? PendingReleaseReason { get; set; }
@ -85,4 +88,16 @@ namespace NzbDrone.Core.Parser.Model
} }
} }
} }
[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 @@ namespace Readarr.Api.V1.BookFiles
public DateTime DateAdded { get; set; } public DateTime DateAdded { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public int QualityWeight { get; set; } public int QualityWeight { get; set; }
public int? IndexerFlags { get; set; }
public MediaInfoResource MediaInfo { get; set; } public MediaInfoResource MediaInfo { get; set; }
public bool QualityCutoffNotMet { get; set; } public bool QualityCutoffNotMet { get; set; }
@ -77,7 +78,8 @@ namespace Readarr.Api.V1.BookFiles
Quality = model.Quality, Quality = model.Quality,
QualityWeight = QualityWeight(model.Quality), QualityWeight = QualityWeight(model.Quality),
MediaInfo = model.MediaInfo.ToResource(), MediaInfo = model.MediaInfo.ToResource(),
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(author.QualityProfile.Value, model.Quality) QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(author.QualityProfile.Value, model.Quality),
IndexerFlags = (int)model.IndexerFlags
}; };
} }
} }

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

@ -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 @@ namespace Readarr.Api.V1.Indexers
public int? Seeders { get; set; } public int? Seeders { get; set; }
public int? Leechers { get; set; } public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public int IndexerFlags { get; set; }
// Sent when queuing an unknown release // Sent when queuing an unknown release
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
@ -72,6 +73,7 @@ namespace Readarr.Api.V1.Indexers
var parsedBookInfo = model.RemoteBook.ParsedBookInfo; var parsedBookInfo = model.RemoteBook.ParsedBookInfo;
var remoteBook = model.RemoteBook; var remoteBook = model.RemoteBook;
var torrentInfo = (model.RemoteBook.Release as TorrentInfo) ?? new TorrentInfo(); 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?) // 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 return new ReleaseResource
@ -111,6 +113,7 @@ namespace Readarr.Api.V1.Indexers
Seeders = torrentInfo.Seeders, Seeders = torrentInfo.Seeders,
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
Protocol = releaseInfo.DownloadProtocol, Protocol = releaseInfo.DownloadProtocol,
IndexerFlags = (int)indexerFlags,
}; };
} }

@ -80,6 +80,7 @@ namespace Readarr.Api.V1.ManualImport
Edition = resource.ForeignEditionId == null ? null : _editionService.GetEditionByForeignEditionId(resource.ForeignEditionId), Edition = resource.ForeignEditionId == null ? null : _editionService.GetEditionByForeignEditionId(resource.ForeignEditionId),
Quality = resource.Quality, Quality = resource.Quality,
ReleaseGroup = resource.ReleaseGroup, ReleaseGroup = resource.ReleaseGroup,
IndexerFlags = resource.IndexerFlags,
DownloadId = resource.DownloadId, DownloadId = resource.DownloadId,
AdditionalFile = resource.AdditionalFile, AdditionalFile = resource.AdditionalFile,
ReplaceExistingFiles = resource.ReplaceExistingFiles, ReplaceExistingFiles = resource.ReplaceExistingFiles,

@ -22,6 +22,7 @@ namespace Readarr.Api.V1.ManualImport
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public int QualityWeight { get; set; } public int QualityWeight { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public int IndexerFlags { get; set; }
public IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }
public ParsedTrackInfo AudioTags { get; set; } public ParsedTrackInfo AudioTags { get; set; }
public bool AdditionalFile { get; set; } public bool AdditionalFile { get; set; }
@ -52,7 +53,9 @@ namespace Readarr.Api.V1.ManualImport
//QualityWeight //QualityWeight
DownloadId = model.DownloadId, DownloadId = model.DownloadId,
IndexerFlags = model.IndexerFlags,
Rejections = model.Rejections, Rejections = model.Rejections,
AudioTags = model.Tags, AudioTags = model.Tags,
AdditionalFile = model.AdditionalFile, AdditionalFile = model.AdditionalFile,
ReplaceExistingFiles = model.ReplaceExistingFiles, ReplaceExistingFiles = model.ReplaceExistingFiles,

@ -14,6 +14,7 @@ namespace Readarr.Api.V1.ManualImport
public string ForeignEditionId { get; set; } public string ForeignEditionId { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public int IndexerFlags { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public bool AdditionalFile { get; set; } public bool AdditionalFile { get; set; }
public bool ReplaceExistingFiles { get; set; } public bool ReplaceExistingFiles { get; set; }

Loading…
Cancel
Save