New: Indexer flags

(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
pull/4681/head
Bogdan 3 months ago
parent 967b58017a
commit 1db0eb1029

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

@ -4,6 +4,7 @@ interface CssExports {
'audio': string; 'audio': string;
'customFormatScore': string; 'customFormatScore': string;
'duration': string; 'duration': string;
'indexerFlags': string;
'monitored': string; 'monitored': string;
'size': string; 'size': string;
'status': string; 'status': string;

@ -2,15 +2,19 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AlbumFormats from 'Album/AlbumFormats'; import AlbumFormats from 'Album/AlbumFormats';
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector'; import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
import IndexerFlags from 'Album/IndexerFlags';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MediaInfoConnector from 'TrackFile/MediaInfoConnector'; import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes'; import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import TrackActionsCell from './TrackActionsCell'; import TrackActionsCell from './TrackActionsCell';
import styles from './TrackRow.css'; import styles from './TrackRow.css';
@ -32,6 +36,7 @@ class TrackRow extends Component {
trackFileSize, trackFileSize,
customFormats, customFormats,
customFormatScore, customFormatScore,
indexerFlags,
columns, columns,
deleteTrackFile deleteTrackFile
} = this.props; } = this.props;
@ -141,12 +146,30 @@ class TrackRow extends Component {
customFormats.length customFormats.length
)} )}
tooltip={<AlbumFormats formats={customFormats} />} tooltip={<AlbumFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.LEFT}
/> />
</TableRowCell> </TableRowCell>
); );
} }
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 === 'size') { if (name === 'size') {
return ( return (
<TableRowCell <TableRowCell
@ -208,12 +231,14 @@ TrackRow.propTypes = {
trackFileSize: PropTypes.number, trackFileSize: PropTypes.number,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired, customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
mediaInfo: PropTypes.object, mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired columns: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
TrackRow.defaultProps = { TrackRow.defaultProps = {
customFormats: [] customFormats: [],
indexerFlags: 0
}; };
export default TrackRow; export default TrackRow;

@ -13,7 +13,8 @@ function createMapStateToProps() {
trackFilePath: trackFile ? trackFile.path : null, trackFilePath: trackFile ? trackFile.path : null,
trackFileSize: trackFile ? trackFile.size : null, trackFileSize: trackFile ? trackFile.size : null,
customFormats: trackFile ? trackFile.customFormats : [], customFormats: trackFile ? trackFile.customFormats : [],
customFormatScore: trackFile ? trackFile.customFormatScore : 0 customFormatScore: trackFile ? trackFile.customFormatScore : 0,
indexerFlags: trackFile ? trackFile.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;

@ -6,6 +6,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 MetadataProfile from 'typings/MetadataProfile'; import MetadataProfile from 'typings/MetadataProfile';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
@ -44,11 +45,13 @@ export interface RootFolderAppState
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState {} AppSectionSaveState {}
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;
metadataProfiles: MetadataProfilesAppState; metadataProfiles: MetadataProfilesAppState;
notifications: NotificationAppState; notifications: NotificationAppState;

@ -12,6 +12,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;
@ -292,6 +296,7 @@ FormInputGroup.propTypes = {
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
includeNone: PropTypes.bool, includeNone: 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;

@ -6,7 +6,14 @@ import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchArtist } from 'Store/Actions/artistActions'; import { fetchArtist } from 'Store/Actions/artistActions';
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());
}, },
@ -217,6 +234,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();
@ -243,6 +261,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles, dispatchFetchMetadataProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations, dispatchFetchTranslations,
@ -284,6 +303,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,
@ -158,6 +159,7 @@ export const FILE = farFile;
export const FILE_IMPORT = fasFileImport; export const FILE_IMPORT = fasFileImport;
export const FILE_MISSING = fasFileCircleQuestion; export const FILE_MISSING = fasFileCircleQuestion;
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 ALBUM_RELEASE_SELECT = 'albumReleaseSelect'; export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect';
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 SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal'; import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal'; import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
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: () => translate('Path'), label: () => translate('Path'),
@ -79,11 +80,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
@ -107,6 +118,7 @@ const ALBUM = 'album';
const ALBUM_RELEASE = 'albumRelease'; const ALBUM_RELEASE = 'albumRelease';
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',
@ -301,6 +313,21 @@ class InteractiveImportModalContent extends Component {
inconsistentAlbumReleases inconsistentAlbumReleases
} = 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 errorMessage = getErrorMessage(error, 'Unable to load manual import items'); const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
@ -310,7 +337,8 @@ class InteractiveImportModalContent extends Component {
{ key: ALBUM, value: translate('SelectAlbum') }, { key: ALBUM, value: translate('SelectAlbum') },
{ key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') }, { key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') },
{ 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 (allowArtistChange) { if (allowArtistChange) {
@ -433,6 +461,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving} isSaving={isSaving}
{...item} {...item}
allowArtistChange={allowArtistChange} allowArtistChange={allowArtistChange}
columns={columns}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange} onValidRowChange={this.onValidRowChange}
/> />
@ -547,6 +576,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}
albums={albumsImported} albums={albumsImported}

@ -135,6 +135,7 @@ class InteractiveImportModalContentConnector extends Component {
albumReleaseId, albumReleaseId,
tracks, tracks,
quality, quality,
indexerFlags,
disableReleaseSwitching disableReleaseSwitching
} = item; } = item;
@ -165,6 +166,7 @@ class InteractiveImportModalContentConnector extends Component {
albumReleaseId, albumReleaseId,
trackIds: _.map(tracks, 'id'), trackIds: _.map(tracks, 'id'),
quality, quality,
indexerFlags,
downloadId: this.props.downloadId, downloadId: this.props.downloadId,
disableReleaseSwitching disableReleaseSwitching
}); });

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AlbumFormats from 'Album/AlbumFormats'; import AlbumFormats from 'Album/AlbumFormats';
import IndexerFlags from 'Album/IndexerFlags';
import TrackQuality from 'Album/TrackQuality'; import TrackQuality from 'Album/TrackQuality';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -13,6 +14,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props';
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
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 SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal'; import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal';
@ -35,7 +37,8 @@ class InteractiveImportRow extends Component {
isSelectAlbumModalOpen: false, isSelectAlbumModalOpen: false,
isSelectTrackModalOpen: false, isSelectTrackModalOpen: false,
isSelectReleaseGroupModalOpen: false, isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false isSelectQualityModalOpen: false,
isSelectIndexerFlagsModalOpen: false
}; };
} }
@ -130,6 +133,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectQualityModalOpen: true }); this.setState({ isSelectQualityModalOpen: true });
}; };
onSelectIndexerFlagsPress = () => {
this.setState({ isSelectIndexerFlagsModalOpen: true });
};
onSelectArtistModalClose = (changed) => { onSelectArtistModalClose = (changed) => {
this.setState({ isSelectArtistModalOpen: false }); this.setState({ isSelectArtistModalOpen: false });
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
@ -155,6 +162,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed); this.selectRowAfterChange(changed);
}; };
onSelectIndexerFlagsModalClose = (changed) => {
this.setState({ isSelectIndexerFlagsModalOpen: false });
this.selectRowAfterChange(changed);
};
// //
// Render // Render
@ -171,7 +183,9 @@ class InteractiveImportRow extends Component {
releaseGroup, releaseGroup,
size, size,
customFormats, customFormats,
indexerFlags,
rejections, rejections,
columns,
isReprocessing, isReprocessing,
audioTags, audioTags,
additionalFile, additionalFile,
@ -184,7 +198,8 @@ class InteractiveImportRow extends Component {
isSelectAlbumModalOpen, isSelectAlbumModalOpen,
isSelectTrackModalOpen, isSelectTrackModalOpen,
isSelectReleaseGroupModalOpen, isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen isSelectQualityModalOpen,
isSelectIndexerFlagsModalOpen
} = this.state; } = this.state;
const artistName = artist ? artist.artistName : ''; const artistName = artist ? artist.artistName : '';
@ -204,6 +219,7 @@ class InteractiveImportRow extends Component {
const showTrackNumbersLoading = isReprocessing && !tracks.length; const showTrackNumbersLoading = isReprocessing && !tracks.length;
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> <div>
@ -219,6 +235,8 @@ class InteractiveImportRow extends Component {
/> />
) : pathCellContents; ) : pathCellContents;
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
return ( return (
<TableRow <TableRow
className={additionalFile ? styles.additionalFile : undefined} className={additionalFile ? styles.additionalFile : undefined}
@ -322,6 +340,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 ?
@ -395,6 +435,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>
); );
} }
@ -413,7 +460,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,

@ -65,6 +65,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, {

@ -35,6 +35,7 @@
} }
.rejected, .rejected,
.indexerFlags,
.download { .download {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';

@ -5,6 +5,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;

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import AlbumFormats from 'Album/AlbumFormats'; import AlbumFormats from 'Album/AlbumFormats';
import IndexerFlags from 'Album/IndexerFlags';
import TrackQuality from 'Album/TrackQuality'; import TrackQuality from 'Album/TrackQuality';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality, quality,
customFormatScore, customFormatScore,
customFormats, customFormats,
indexerFlags = 0,
rejections, rejections,
downloadAllowed, downloadAllowed,
isGrabbing, isGrabbing,
@ -187,10 +189,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<AlbumFormats formats={customFormats} />} tooltip={<AlbumFormats 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: {
}
};

@ -208,6 +208,7 @@ export const actionHandlers = handleThunks({
trackIds: (item.tracks || []).map((e) => e.id), trackIds: (item.tracks || []).map((e) => e.id),
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,

@ -11,6 +11,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';
@ -38,6 +39,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';
@ -73,6 +75,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,
@ -119,6 +122,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,
@ -156,6 +160,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,

@ -77,6 +77,15 @@ export const defaultState = {
}), }),
isVisible: false isVisible: false
}, },
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{ {
name: 'status', name: 'status',
label: () => translate('Status'), label: () => translate('Status'),

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

@ -13,6 +13,7 @@ export interface TrackFile extends ModelBase {
releaseGroup: string; releaseGroup: string;
quality: QualityModel; quality: QualityModel;
customFormats: CustomFormat[]; customFormats: CustomFormat[];
indexerFlags: number;
mediaInfo: MediaInfo; mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean; qualityCutoffNotMet: boolean;
} }

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

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Parser.Model;
namespace Lidarr.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 Lidarr.Http.REST;
using Newtonsoft.Json;
namespace Lidarr.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 Lidarr.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)]
@ -76,6 +77,7 @@ namespace Lidarr.Api.V1.Indexers
var parsedAlbumInfo = model.RemoteAlbum.ParsedAlbumInfo; var parsedAlbumInfo = model.RemoteAlbum.ParsedAlbumInfo;
var remoteAlbum = model.RemoteAlbum; var remoteAlbum = model.RemoteAlbum;
var torrentInfo = (model.RemoteAlbum.Release as TorrentInfo) ?? new TorrentInfo(); var torrentInfo = (model.RemoteAlbum.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
@ -115,6 +117,7 @@ namespace Lidarr.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 Lidarr.Api.V1.ManualImport
Release = resource.AlbumReleaseId.HasValue ? _releaseService.GetRelease(resource.AlbumReleaseId.Value) : null, Release = resource.AlbumReleaseId.HasValue ? _releaseService.GetRelease(resource.AlbumReleaseId.Value) : null,
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,

@ -24,6 +24,7 @@ namespace Lidarr.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; }
@ -55,7 +56,9 @@ namespace Lidarr.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,

@ -17,6 +17,7 @@ namespace Lidarr.Api.V1.ManualImport
public List<int> TrackIds { get; set; } public List<int> TrackIds { 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; }

@ -24,6 +24,7 @@ namespace Lidarr.Api.V1.TrackFiles
public int QualityWeight { get; set; } public int QualityWeight { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; } public List<CustomFormatResource> CustomFormats { get; set; }
public int CustomFormatScore { get; set; } public int CustomFormatScore { 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; }
@ -94,7 +95,8 @@ namespace Lidarr.Api.V1.TrackFiles
MediaInfo = model.MediaInfo.ToResource(), MediaInfo = model.MediaInfo.ToResource(),
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.QualityProfile.Value, model.Quality), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.QualityProfile.Value, model.Quality),
CustomFormats = customFormats.ToResource(false), CustomFormats = customFormats.ToResource(false),
CustomFormatScore = customFormatScore CustomFormatScore = customFormatScore,
IndexerFlags = (int)model.IndexerFlags
}; };
} }
} }

@ -8,6 +8,7 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
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.TrackImport; using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
@ -130,6 +131,10 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>())) .Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, FilterFilesType filter) => files); .Returns((List<IFileInfo> files, FilterFilesType filter) => files);
Mocker.GetMock<IHistoryService>()
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EntityHistory>());
GivenSpecifications(_albumpass1); GivenSpecifications(_albumpass1);
} }

@ -3,6 +3,7 @@ using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
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
{ {
AlbumInfo = remoteAlbum.ParsedAlbumInfo, AlbumInfo = remoteAlbum.ParsedAlbumInfo,
Artist = remoteAlbum.Artist, Artist = remoteAlbum.Artist,
Size = size Size = size,
IndexerFlags = remoteAlbum.Release?.IndexerFlags ?? 0
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -70,7 +72,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
AlbumInfo = albumInfo, AlbumInfo = albumInfo,
Artist = artist, Artist = artist,
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.ParseAlbumTitle(history.SourceTitle); var parsed = Parser.Parser.ParseAlbumTitle(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 albumInfo = new ParsedAlbumInfo var albumInfo = new ParsedAlbumInfo
{ {
@ -94,7 +98,8 @@ namespace NzbDrone.Core.CustomFormats
{ {
AlbumInfo = albumInfo, AlbumInfo = albumInfo,
Artist = artist, Artist = artist,
Size = size Size = size,
IndexerFlags = indexerFlags
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -115,7 +120,8 @@ namespace NzbDrone.Core.CustomFormats
AlbumInfo = albumInfo, AlbumInfo = albumInfo,
Artist = localTrack.Artist, Artist = localTrack.Artist,
Size = localTrack.Size, Size = localTrack.Size,
Filename = Path.GetFileName(localTrack.Path) Filename = Path.GetFileName(localTrack.Path),
IndexerFlags = localTrack.IndexerFlags,
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -182,6 +188,7 @@ namespace NzbDrone.Core.CustomFormats
AlbumInfo = albumInfo, AlbumInfo = albumInfo,
Artist = artist, Artist = artist,
Size = trackFile.Size, Size = trackFile.Size,
IndexerFlags = trackFile.IndexerFlags,
Filename = Path.GetFileName(trackFile.Path) Filename = Path.GetFileName(trackFile.Path)
}; };

@ -8,6 +8,7 @@ namespace NzbDrone.Core.CustomFormats
public ParsedAlbumInfo AlbumInfo { get; set; } public ParsedAlbumInfo AlbumInfo { get; set; }
public Artist Artist { get; set; } public Artist Artist { 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(078)]
public class add_indexer_flags : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Blocklist").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
Alter.Table("TrackFiles").AddColumn("IndexerFlags").AsInt32().WithDefaultValue(0);
}
}
}

@ -12,6 +12,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events; using NzbDrone.Core.Music.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.TrackedDownloads namespace NzbDrone.Core.Download.TrackedDownloads
{ {
@ -144,12 +145,11 @@ 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");
trackedDownload.Added = grabbedEvent?.Date; trackedDownload.Added = grabbedEvent?.Date;
if (parsedAlbumInfo == null || if (parsedAlbumInfo == null ||
trackedDownload.RemoteAlbum == null || trackedDownload.RemoteAlbum?.Artist == null ||
trackedDownload.RemoteAlbum.Artist == null ||
trackedDownload.RemoteAlbum.Albums.Empty()) trackedDownload.RemoteAlbum.Albums.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
@ -181,6 +181,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
} }
if (trackedDownload.RemoteAlbum != null &&
Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
trackedDownload.RemoteAlbum.Release ??= new ReleaseInfo();
trackedDownload.RemoteAlbum.Release.IndexerFlags = flags;
}
} }
// Calculate custom formats // Calculate custom formats

@ -166,6 +166,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadForced", (!message.Album.DownloadAllowed).ToString()); history.Data.Add("DownloadForced", (!message.Album.DownloadAllowed).ToString());
history.Data.Add("CustomFormatScore", message.Album.CustomFormatScore.ToString()); history.Data.Add("CustomFormatScore", message.Album.CustomFormatScore.ToString());
history.Data.Add("ReleaseSource", message.Album.ReleaseSource.ToString()); history.Data.Add("ReleaseSource", message.Album.ReleaseSource.ToString());
history.Data.Add("IndexerFlags", message.Album.Release.IndexerFlags.ToString());
if (!message.Album.ParsedAlbumInfo.ReleaseHash.IsNullOrWhiteSpace()) if (!message.Album.ParsedAlbumInfo.ReleaseHash.IsNullOrWhiteSpace())
{ {
@ -203,6 +204,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?.RemoteAlbum?.ParsedAlbumInfo?.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteAlbum?.ParsedAlbumInfo?.ReleaseGroup);
history.Data.Add("IndexerFlags", message.TrackedDownload?.RemoteAlbum?.Release?.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
} }
@ -241,6 +244,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClient", message.DownloadClientInfo?.Name); history.Data.Add("DownloadClient", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.TrackInfo.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackInfo.ReleaseGroup);
history.Data.Add("Size", message.TrackInfo.Size.ToString()); history.Data.Add("Size", message.TrackInfo.Size.ToString());
history.Data.Add("IndexerFlags", message.ImportedTrack.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -324,6 +328,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.TrackFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackFile.ReleaseGroup);
history.Data.Add("Size", message.TrackFile.Size.ToString()); history.Data.Add("Size", message.TrackFile.Size.ToString());
history.Data.Add("IndexerFlags", message.TrackFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -351,6 +356,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Path", path); history.Data.Add("Path", path);
history.Data.Add("ReleaseGroup", message.TrackFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.TrackFile.ReleaseGroup);
history.Data.Add("Size", message.TrackFile.Size.ToString()); history.Data.Add("Size", message.TrackFile.Size.ToString());
history.Data.Add("IndexerFlags", message.TrackFile.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; }
} }

@ -63,7 +63,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
title += " [Cue]"; title += " [Cue]";
} }
torrentInfos.Add(new GazelleInfo() torrentInfos.Add(new GazelleInfo
{ {
Guid = string.Format("Gazelle-{0}", id), Guid = string.Format("Gazelle-{0}", id),
Artist = artist, Artist = artist,
@ -79,7 +79,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)
}); });
} }
} }
@ -92,6 +92,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)

@ -65,7 +65,7 @@ namespace NzbDrone.Core.Indexers.Redacted
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)
}); });
} }
} }
@ -111,6 +111,23 @@ namespace NzbDrone.Core.Indexers.Redacted
return $"{title} [{string.Join(" / ", flags)}]"; return $"{title} [{string.Join(" / ", flags)}]";
} }
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, bool canUseToken) private string GetDownloadUrl(int torrentId, bool canUseToken)
{ {
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"));
@ -180,6 +192,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));
@ -195,6 +254,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));

@ -201,6 +201,7 @@
"Clear": "Clear", "Clear": "Clear",
"ClearBlocklist": "Clear blocklist", "ClearBlocklist": "Clear blocklist",
"ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?", "ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?",
"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",
@ -257,6 +258,7 @@
"CustomFormats": "Custom Formats", "CustomFormats": "Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings", "CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsSummary": "Custom Formats and Settings", "CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationRegularExpression": "Regular Expression", "CustomFormatsSpecificationRegularExpression": "Regular Expression",
"CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive", "CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive",
"Customformat": "Custom Format", "Customformat": "Custom Format",
@ -574,6 +576,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}",
@ -901,6 +904,7 @@
"RegularExpressionsCanBeTested": "Regular expressions can be tested [here](http://regexstorm.net/tester).", "RegularExpressionsCanBeTested": "Regular expressions can be tested [here](http://regexstorm.net/tester).",
"RegularExpressionsTutorialLink": "More details on regular expressions can be found [here](https://www.regular-expressions.info/tutorial.html).", "RegularExpressionsTutorialLink": "More details on regular expressions can be found [here](https://www.regular-expressions.info/tutorial.html).",
"RejectionCount": "Rejection Count", "RejectionCount": "Rejection Count",
"Rejections": "Rejections",
"Release": " Release", "Release": " Release",
"ReleaseDate": "Release Date", "ReleaseDate": "Release Date",
"ReleaseGroup": "Release Group", "ReleaseGroup": "Release Group",
@ -1043,12 +1047,14 @@
"SelectAlbumRelease": "Select Album Release", "SelectAlbumRelease": "Select Album Release",
"SelectArtist": "Select Artist", "SelectArtist": "Select Artist",
"SelectFolder": "Select Folder", "SelectFolder": "Select Folder",
"SelectIndexerFlags": "Select Indexer Flags",
"SelectQuality": "Select Quality", "SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group", "SelectReleaseGroup": "Select Release Group",
"SelectTracks": "Select Tracks", "SelectTracks": "Select Tracks",
"SelectedCountArtistsSelectedInterp": "{selectedCount} Artist(s) Selected", "SelectedCountArtistsSelectedInterp": "{selectedCount} Artist(s) Selected",
"SendAnonymousUsageData": "Send Anonymous Usage Data", "SendAnonymousUsageData": "Send Anonymous Usage Data",
"SetAppTags": "Set {appName} Tags", "SetAppTags": "Set {appName} Tags",
"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.",

@ -19,6 +19,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 AlbumId { get; set; } public int AlbumId { get; set; }

@ -9,6 +9,7 @@ using NzbDrone.Common.Instrumentation.Extensions;
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;
@ -40,6 +41,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
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 IReleaseService _releaseService; private readonly IReleaseService _releaseService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
@ -57,6 +59,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
IRecycleBinProvider recycleBinProvider, IRecycleBinProvider recycleBinProvider,
IExtraService extraService, IExtraService extraService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IHistoryService historyService,
IReleaseService releaseService, IReleaseService releaseService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager, IManageCommandQueue commandQueueManager,
@ -74,6 +77,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_extraService = extraService; _extraService = extraService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_historyService = historyService;
_releaseService = releaseService; _releaseService = releaseService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
@ -197,6 +201,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
Tracks = localTrack.Tracks Tracks = localTrack.Tracks
}; };
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))
{
trackFile.IndexerFlags = flags;
}
}
else
{
trackFile.IndexerFlags = localTrack.IndexerFlags;
}
bool copyOnly; bool copyOnly;
switch (importMode) switch (importMode)
{ {

@ -13,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public int AlbumReleaseId { get; set; } public int AlbumReleaseId { get; set; }
public List<int> TrackIds { get; set; } public List<int> TrackIds { 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; }

@ -27,6 +27,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.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; }

@ -293,6 +293,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.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;
@ -344,6 +345,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
Size = fileInfo.Length, Size = fileInfo.Length,
Modified = fileInfo.LastWriteTimeUtc, Modified = fileInfo.LastWriteTimeUtc,
Quality = file.Quality, Quality = file.Quality,
IndexerFlags = (IndexerFlags)file.IndexerFlags,
Artist = artist, Artist = artist,
Album = album, Album = album,
Release = release Release = release

@ -75,6 +75,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Lidarr_Release_Quality", remoteAlbum.ParsedAlbumInfo.Quality.Quality.Name); environmentVariables.Add("Lidarr_Release_Quality", remoteAlbum.ParsedAlbumInfo.Quality.Quality.Name);
environmentVariables.Add("Lidarr_Release_QualityVersion", remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.ToString()); environmentVariables.Add("Lidarr_Release_QualityVersion", remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.ToString());
environmentVariables.Add("Lidarr_Release_ReleaseGroup", releaseGroup ?? string.Empty); environmentVariables.Add("Lidarr_Release_ReleaseGroup", releaseGroup ?? string.Empty);
environmentVariables.Add("Lidarr_Release_IndexerFlags", remoteAlbum.Release.IndexerFlags.ToString());
environmentVariables.Add("Lidarr_Download_Client", message.DownloadClientName ?? string.Empty); environmentVariables.Add("Lidarr_Download_Client", message.DownloadClientName ?? string.Empty);
environmentVariables.Add("Lidarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); environmentVariables.Add("Lidarr_Download_Client_Type", message.DownloadClientType ?? string.Empty);
environmentVariables.Add("Lidarr_Download_Id", message.DownloadId ?? string.Empty); environmentVariables.Add("Lidarr_Download_Id", message.DownloadId ?? string.Empty);

@ -26,6 +26,7 @@ namespace NzbDrone.Core.Parser.Model
public List<Track> Tracks { get; set; } public List<Track> Tracks { 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.
}
} }

Loading…
Cancel
Save