diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 4c0680956..16ca08257 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -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, AppSectionDeleteState {} +export type IndexerFlagSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionState; interface SettingsAppState { downloadClients: DownloadClientAppState; importLists: ImportListAppState; + indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; notifications: NotificationAppState; uiSettings: UiSettingsAppState; diff --git a/frontend/src/Author/Details/BookRow.css b/frontend/src/Author/Details/BookRow.css index adad8e9da..924820942 100644 --- a/frontend/src/Author/Details/BookRow.css +++ b/frontend/src/Author/Details/BookRow.css @@ -27,3 +27,9 @@ width: 80px; } + +.indexerFlags { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} diff --git a/frontend/src/Author/Details/BookRow.css.d.ts b/frontend/src/Author/Details/BookRow.css.d.ts index 1a58936a5..f1d343daa 100644 --- a/frontend/src/Author/Details/BookRow.css.d.ts +++ b/frontend/src/Author/Details/BookRow.css.d.ts @@ -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; diff --git a/frontend/src/Author/Details/BookRow.js b/frontend/src/Author/Details/BookRow.js index 20f411bb2..810a5fa73 100644 --- a/frontend/src/Author/Details/BookRow.js +++ b/frontend/src/Author/Details/BookRow.js @@ -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 ( + + {indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={} + position={tooltipPositions.LEFT} + /> + ) : null} + + ); + } + if (name === 'status') { return ( 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 }; } ); diff --git a/frontend/src/Book/IndexerFlags.tsx b/frontend/src/Book/IndexerFlags.tsx new file mode 100644 index 000000000..74e2e033c --- /dev/null +++ b/frontend/src/Book/IndexerFlags.tsx @@ -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 ? ( +
    + {flags.map((flag, index) => { + return
  • {flag.name}
  • ; + })} +
+ ) : null; +} + +export default IndexerFlags; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 5a3df488a..c23af1d8d 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -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), diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx new file mode 100644 index 000000000..8dbd27a70 --- /dev/null +++ b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx @@ -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 ( + + ); +} + +export default IndexerFlagsSelectInput; diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index d480c824a..4f5ace521 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -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, diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index 5f24bab88..0b67791a8 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -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; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 16369a5e3..22387d474 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -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'; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js new file mode 100644 index 000000000..04359b96a --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.js @@ -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 ( + + + + ); + } +} + +SelectIndexerFlagsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectIndexerFlagsModal; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css new file mode 100644 index 000000000..72dfb1cb6 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css @@ -0,0 +1,7 @@ +.modalBody { + composes: modalBody from '~Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts new file mode 100644 index 000000000..3fc49a060 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts @@ -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; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js new file mode 100644 index 000000000..b30f76775 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.js @@ -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 ( + + + Manual Import - Set indexer Flags + + + +
+ + + {translate('IndexerFlags')} + + + + +
+
+ + + + + + +
+ ); + } +} + +SelectIndexerFlagsModalContent.propTypes = { + indexerFlags: PropTypes.number.isRequired, + onIndexerFlagsSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectIndexerFlagsModalContent; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js new file mode 100644 index 000000000..7a9af7353 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContentConnector.js @@ -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 ( + + ); + } +} + +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); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index 256e87b15..abb96aebb 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -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} /> + + { + 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 = (
@@ -215,6 +231,8 @@ class InteractiveImportRow extends Component { /> ); + const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false; + return ( + {isIndexerFlagsColumnVisible ? ( + + {showIndexerFlagsPlaceholder ? ( + + ) : ( + <> + {indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={} + position={tooltipPositions.LEFT} + /> + ) : null} + + )} + + ) : null} + { rejections.length ? @@ -378,6 +418,13 @@ class InteractiveImportRow extends Component { real={quality ? quality.revision.real > 0 : false} onModalClose={this.onSelectQualityModalClose} /> + + ); } @@ -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, diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearch.js index f5b678a1a..c9636bc34 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearch.js @@ -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, { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index 3b5933653..998b8f41a 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -40,6 +40,7 @@ } .rejected, +.indexerFlags, .download { composes: cell; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts index 23189f749..33a3f261e 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css.d.ts @@ -6,6 +6,7 @@ interface CssExports { 'customFormatScore': string; 'download': string; 'indexer': string; + 'indexerFlags': string; 'peers': string; 'protocol': string; 'quality': string; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js index 53d578d7a..3f107707c 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js @@ -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={} - position={tooltipPositions.BOTTOM} + position={tooltipPositions.LEFT} /> + + {indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={} + position={tooltipPositions.LEFT} + /> + ) : null} + + { !!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 diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js new file mode 100644 index 000000000..a53fe1c61 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/indexerFlags.js @@ -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: { + + } + +}; diff --git a/frontend/src/Store/Actions/bookActions.js b/frontend/src/Store/Actions/bookActions.js index 3aa49cfae..53686e359 100644 --- a/frontend/src/Store/Actions/bookActions.js +++ b/frontend/src/Store/Actions/bookActions.js @@ -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', diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index f853825cf..053f4f370 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -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, diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 5fbb28cd3..5961962f2 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -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, diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts new file mode 100644 index 000000000..90587639c --- /dev/null +++ b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts @@ -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; diff --git a/frontend/src/typings/IndexerFlag.ts b/frontend/src/typings/IndexerFlag.ts new file mode 100644 index 000000000..2c7d97a73 --- /dev/null +++ b/frontend/src/typings/IndexerFlag.ts @@ -0,0 +1,6 @@ +interface IndexerFlag { + id: number; + name: string; +} + +export default IndexerFlag; diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs index 2e3a3d213..812102547 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using NzbDrone.Core.Books; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; +using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.BookImport; using NzbDrone.Core.MediaFiles.BookImport.Aggregation; @@ -134,6 +135,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport .Setup(s => s.ReadTags(It.IsAny())) .Returns(new ParsedTrackInfo()); + Mocker.GetMock() + .Setup(x => x.FindByDownloadId(It.IsAny())) + .Returns(new List()); + GivenSpecifications(_bookpass1); } diff --git a/src/NzbDrone.Core/Blocklisting/Blocklist.cs b/src/NzbDrone.Core/Blocklisting/Blocklist.cs index 7ecb9372b..04120a119 100644 --- a/src/NzbDrone.Core/Blocklisting/Blocklist.cs +++ b/src/NzbDrone.Core/Blocklisting/Blocklist.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Core.Books; using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Blocklisting @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Blocklisting public long? Size { get; set; } public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } + public IndexerFlags IndexerFlags { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } } diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 54b19882d..f49485d20 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -188,6 +188,11 @@ namespace NzbDrone.Core.Blocklisting TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash") }; + if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + blocklist.IndexerFlags = flags; + } + _blocklistRepository.Insert(blocklist); } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index f14e65572..d9fedcf69 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -38,7 +39,8 @@ namespace NzbDrone.Core.CustomFormats { BookInfo = remoteBook.ParsedBookInfo, Author = remoteBook.Author, - Size = size + Size = size, + IndexerFlags = remoteBook.Release?.IndexerFlags ?? 0 }; return ParseCustomFormat(input); @@ -70,7 +72,8 @@ namespace NzbDrone.Core.CustomFormats { BookInfo = bookInfo, Author = author, - Size = blocklist.Size ?? 0 + Size = blocklist.Size ?? 0, + IndexerFlags = blocklist.IndexerFlags }; return ParseCustomFormat(input); @@ -81,6 +84,7 @@ namespace NzbDrone.Core.CustomFormats var parsed = Parser.Parser.ParseBookTitle(history.SourceTitle); long.TryParse(history.Data.GetValueOrDefault("size"), out var size); + Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags); var bookInfo = new ParsedBookInfo { @@ -94,7 +98,8 @@ namespace NzbDrone.Core.CustomFormats { BookInfo = bookInfo, Author = author, - Size = size + Size = size, + IndexerFlags = indexerFlags }; return ParseCustomFormat(input); @@ -114,7 +119,8 @@ namespace NzbDrone.Core.CustomFormats { BookInfo = bookInfo, Author = localBook.Author, - Size = localBook.Size + Size = localBook.Size, + IndexerFlags = localBook.IndexerFlags, }; return ParseCustomFormat(input); @@ -181,6 +187,7 @@ namespace NzbDrone.Core.CustomFormats BookInfo = bookInfo, Author = author, Size = bookFile.Size, + IndexerFlags = bookFile.IndexerFlags, Filename = Path.GetFileName(bookFile.Path) }; diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs index 72b13dfe6..f9872dcfe 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.CustomFormats public ParsedBookInfo BookInfo { get; set; } public Author Author { get; set; } public long Size { get; set; } + public IndexerFlags IndexerFlags { get; set; } public string Filename { get; set; } // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series) diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs new file mode 100644 index 000000000..3eaeeb5f6 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -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 + { + 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)); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/040_add_indexer_flags.cs b/src/NzbDrone.Core/Datastore/Migration/040_add_indexer_flags.cs new file mode 100644 index 000000000..fe4bc236b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/040_add_indexer_flags.cs @@ -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); + } + } +} diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 2b7fb32ff..aa3b6a0b5 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Download.History; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.TrackedDownloads { @@ -156,11 +157,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.First(); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed); - trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer"); if (parsedBookInfo == null || - trackedDownload.RemoteBook == null || - trackedDownload.RemoteBook.Author == null || + trackedDownload.RemoteBook?.Author == null || trackedDownload.RemoteBook.Books.Empty()) { // Try parsing the original source title and if that fails, try parsing it as a special @@ -192,6 +192,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } } + + if (trackedDownload.RemoteBook != null && + Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + trackedDownload.RemoteBook.Release ??= new ReleaseInfo(); + trackedDownload.RemoteBook.Release.IndexerFlags = flags; + } } // Calculate custom formats diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index a9aae87dd..36b066820 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -164,6 +164,7 @@ namespace NzbDrone.Core.History history.Data.Add("DownloadForced", (!message.Book.DownloadAllowed).ToString()); history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString()); history.Data.Add("ReleaseSource", message.Book.ReleaseSource.ToString()); + history.Data.Add("IndexerFlags", message.Book.Release.IndexerFlags.ToString()); if (!message.Book.ParsedBookInfo.ReleaseHash.IsNullOrWhiteSpace()) { @@ -201,6 +202,8 @@ namespace NzbDrone.Core.History history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson()); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup); + history.Data.Add("IndexerFlags", message.TrackedDownload?.RemoteBook?.Release?.IndexerFlags.ToString()); + _historyRepository.Insert(history); } } @@ -237,6 +240,7 @@ namespace NzbDrone.Core.History history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name); history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup); history.Data.Add("Size", message.BookInfo.Size.ToString()); + history.Data.Add("IndexerFlags", message.BookInfo.IndexerFlags.ToString()); _historyRepository.Insert(history); } @@ -290,6 +294,7 @@ namespace NzbDrone.Core.History history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); + history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString()); _historyRepository.Insert(history); } @@ -313,6 +318,7 @@ namespace NzbDrone.Core.History history.Data.Add("Path", path); history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup); history.Data.Add("Size", message.BookFile.Size.ToString()); + history.Data.Add("IndexerFlags", message.BookFile.IndexerFlags.ToString()); _historyRepository.Insert(history); } diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index eaad3ce80..f29f22354 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList { var id = result.Id; - //if (result.FreeLeech) - torrentInfos.Add(new TorrentInfo() + torrentInfos.Add(new TorrentInfo { Guid = $"FileList-{id}", Title = result.Name, @@ -48,13 +47,31 @@ namespace NzbDrone.Core.Indexers.FileList InfoUrl = GetInfoUrl(id), Seeders = result.Seeders, Peers = result.Leechers + result.Seeders, - PublishDate = result.UploadDate.ToUniversalTime() + PublishDate = result.UploadDate.ToUniversalTime(), + IndexerFlags = GetIndexerFlags(result) }); } return torrentInfos.ToArray(); } + private static IndexerFlags GetIndexerFlags(FileListTorrent item) + { + IndexerFlags flags = 0; + + if (item.FreeLeech) + { + flags |= IndexerFlags.Freeleech; + } + + if (item.Internal) + { + flags |= IndexerFlags.Internal; + } + + return flags; + } + private string GetDownloadUrl(string torrentId) { var url = new HttpUri(_settings.BaseUrl) diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs b/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs index 01ea834ed..a22fc1c9b 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListTorrent.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers.FileList public uint Files { get; set; } [JsonProperty(PropertyName = "imdb")] public string ImdbId { get; set; } + public bool Internal { get; set; } [JsonProperty(PropertyName = "freeleech")] public bool FreeLeech { get; set; } [JsonProperty(PropertyName = "upload_date")] diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs index ba1e7e43b..7d83975e2 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleApi.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.Indexers.Gazelle public string Leechers { get; set; } public bool IsFreeLeech { get; set; } public bool IsNeutralLeech { get; set; } + public bool IsFreeload { get; set; } public bool IsPersonalFreeLeech { get; set; } public bool CanUseToken { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs index ed1f3fc2f..55f00b308 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleParser.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.Gazelle var author = WebUtility.HtmlDecode(result.Author); var book = WebUtility.HtmlDecode(result.GroupName); - torrentInfos.Add(new GazelleInfo() + torrentInfos.Add(new GazelleInfo { Guid = string.Format("Gazelle-{0}", id), Author = author, @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Indexers.Gazelle Seeders = int.Parse(torrent.Seeders), Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), PublishDate = torrent.Time.ToUniversalTime(), - Scene = torrent.Scene, + IndexerFlags = GetIndexerFlags(torrent) }); } } @@ -88,6 +88,23 @@ namespace NzbDrone.Core.Indexers.Gazelle .ToArray(); } + private static IndexerFlags GetIndexerFlags(GazelleTorrent torrent) + { + IndexerFlags flags = 0; + + if (torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsFreeload || torrent.IsPersonalFreeLeech) + { + flags |= IndexerFlags.Freeleech; + } + + if (torrent.Scene) + { + flags |= IndexerFlags.Scene; + } + + return flags; + } + private string GetDownloadUrl(int torrentId) { var url = new HttpUri(_settings.BaseUrl) diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 4a441f35d..6736955cf 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -74,6 +74,18 @@ namespace NzbDrone.Core.Indexers.Torznab return true; } + protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) + { + var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; + + if (torrentInfo != null) + { + torrentInfo.IndexerFlags = GetFlags(item); + } + + return torrentInfo; + } + protected override string GetInfoUrl(XElement item) { return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments")); @@ -194,6 +206,53 @@ namespace NzbDrone.Core.Indexers.Torznab return base.GetPeers(item); } + protected IndexerFlags GetFlags(XElement item) + { + IndexerFlags flags = 0; + + var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1); + var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1); + + if (downloadFactor == 0.5) + { + flags |= IndexerFlags.Halfleech; + } + + if (downloadFactor == 0.75) + { + flags |= IndexerFlags.Freeleech25; + } + + if (downloadFactor == 0.25) + { + flags |= IndexerFlags.Freeleech75; + } + + if (downloadFactor == 0.0) + { + flags |= IndexerFlags.Freeleech; + } + + if (uploadFactor == 2.0) + { + flags |= IndexerFlags.DoubleUpload; + } + + var tags = TryGetMultipleTorznabAttributes(item, "tag"); + + if (tags.Any(t => t.EqualsIgnoreCase("internal"))) + { + flags |= IndexerFlags.Internal; + } + + if (tags.Any(t => t.EqualsIgnoreCase("scene"))) + { + flags |= IndexerFlags.Scene; + } + + return flags; + } + protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "") { var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); @@ -209,6 +268,13 @@ namespace NzbDrone.Core.Indexers.Torznab return defaultValue; } + protected float TryGetFloatTorznabAttribute(XElement item, string key, float defaultValue = 0) + { + var attr = TryGetTorznabAttribute(item, key, defaultValue.ToString()); + + return float.TryParse(attr, out var result) ? result : defaultValue; + } + protected List TryGetMultipleTorznabAttributes(XElement item, string key) { var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 42922d523..d1711e68d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -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.", diff --git a/src/NzbDrone.Core/MediaFiles/BookFile.cs b/src/NzbDrone.Core/MediaFiles/BookFile.cs index 40c7e8d91..d6bf85b15 100644 --- a/src/NzbDrone.Core/MediaFiles/BookFile.cs +++ b/src/NzbDrone.Core/MediaFiles/BookFile.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.MediaFiles 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; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs b/src/NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs index 0b8a80560..4b1e3de26 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/ImportApprovedBooks.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Books.Events; 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 @@ namespace NzbDrone.Core.MediaFiles.BookImport 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 @@ namespace NzbDrone.Core.MediaFiles.BookImport IRecycleBinProvider recycleBinProvider, IExtraService extraService, IDiskProvider diskProvider, + IHistoryService historyService, IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager, Logger logger) @@ -74,6 +77,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport _recycleBinProvider = recycleBinProvider; _extraService = extraService; _diskProvider = diskProvider; + _historyService = historyService; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; _logger = logger; @@ -193,6 +197,22 @@ namespace NzbDrone.Core.MediaFiles.BookImport 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) { diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs index 777f3b3f9..38d1f7a89 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual 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; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs index 48c3604ea..846eba024 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportItem.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual public string ReleaseGroup { get; set; } public string DownloadId { get; set; } public List CustomFormats { get; set; } + public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } public ParsedTrackInfo Tags { get; set; } public bool AdditionalFile { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs index 320709d1e..8f64b463c 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs @@ -286,6 +286,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual } 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 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual Size = fileInfo.Length, Modified = fileInfo.LastWriteTimeUtc, Quality = file.Quality, + IndexerFlags = (IndexerFlags)file.IndexerFlags, Author = author, Book = book, Edition = edition diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index a3b53420a..d3c513224 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -57,6 +57,7 @@ namespace NzbDrone.Core.Notifications.CustomScript 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); diff --git a/src/NzbDrone.Core/Parser/Model/LocalBook.cs b/src/NzbDrone.Core/Parser/Model/LocalBook.cs index 077588f43..cbdf042f4 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalBook.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalBook.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model 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; } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 71c4d5caa..63a66eb4b 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -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 @@ namespace NzbDrone.Core.Parser.Model public List 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 @@ 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. + } } diff --git a/src/Readarr.Api.V1/BookFiles/BookFileResource.cs b/src/Readarr.Api.V1/BookFiles/BookFileResource.cs index 01b8d51c6..34005fe9b 100644 --- a/src/Readarr.Api.V1/BookFiles/BookFileResource.cs +++ b/src/Readarr.Api.V1/BookFiles/BookFileResource.cs @@ -17,6 +17,7 @@ namespace Readarr.Api.V1.BookFiles 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 @@ namespace Readarr.Api.V1.BookFiles 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 }; } } diff --git a/src/Readarr.Api.V1/Indexers/IndexerFlagController.cs b/src/Readarr.Api.V1/Indexers/IndexerFlagController.cs new file mode 100644 index 000000000..4354e9f8f --- /dev/null +++ b/src/Readarr.Api.V1/Indexers/IndexerFlagController.cs @@ -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 GetAll() + { + return Enum.GetValues(typeof(IndexerFlags)).Cast().Select(f => new IndexerFlagResource + { + Id = (int)f, + Name = f.ToString() + }).ToList(); + } + } +} diff --git a/src/Readarr.Api.V1/Indexers/IndexerFlagResource.cs b/src/Readarr.Api.V1/Indexers/IndexerFlagResource.cs new file mode 100644 index 000000000..e8fdddbca --- /dev/null +++ b/src/Readarr.Api.V1/Indexers/IndexerFlagResource.cs @@ -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(); + } +} diff --git a/src/Readarr.Api.V1/Indexers/ReleaseResource.cs b/src/Readarr.Api.V1/Indexers/ReleaseResource.cs index 2d3f08cc8..e358d8b09 100644 --- a/src/Readarr.Api.V1/Indexers/ReleaseResource.cs +++ b/src/Readarr.Api.V1/Indexers/ReleaseResource.cs @@ -49,6 +49,7 @@ namespace Readarr.Api.V1.Indexers 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 @@ namespace Readarr.Api.V1.Indexers 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 @@ namespace Readarr.Api.V1.Indexers Seeders = torrentInfo.Seeders, Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, Protocol = releaseInfo.DownloadProtocol, + IndexerFlags = (int)indexerFlags, }; } diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs index d36e5d2ff..95c3690fd 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs @@ -80,6 +80,7 @@ namespace Readarr.Api.V1.ManualImport 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, diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs index 763d6959a..fee5c306b 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs @@ -22,6 +22,7 @@ namespace Readarr.Api.V1.ManualImport public string ReleaseGroup { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } + public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } public ParsedTrackInfo AudioTags { get; set; } public bool AdditionalFile { get; set; } @@ -52,7 +53,9 @@ namespace Readarr.Api.V1.ManualImport //QualityWeight DownloadId = model.DownloadId, + IndexerFlags = model.IndexerFlags, Rejections = model.Rejections, + AudioTags = model.Tags, AdditionalFile = model.AdditionalFile, ReplaceExistingFiles = model.ReplaceExistingFiles, diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs index b87f12489..1168371fd 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -14,6 +14,7 @@ namespace Readarr.Api.V1.ManualImport 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; }