New: Indexer flags

Closes #2782
This commit is contained in:
Bogdan 2024-02-21 06:12:45 +02:00 committed by GitHub
parent a57254640f
commit 7a768b5d0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 876 additions and 72 deletions

View File

@ -9,6 +9,7 @@ import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
@ -40,19 +41,21 @@ export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>, extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {} AppSectionSaveState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>; export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>; export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
advancedSettings: boolean; advancedSettings: boolean;
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
languages: LanguageSettingsAppState; languages: LanguageSettingsAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState; qualityProfiles: QualityProfilesAppState;
ui: UiSettingsAppState; ui: UiSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
} }
export default SettingsAppState; export default SettingsAppState;

View File

@ -11,6 +11,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 MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput'; import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
@ -71,6 +72,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;
@ -279,6 +283,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool, includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object, selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool, pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object), errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object), warnings: PropTypes.arrayOf(PropTypes.object),

View File

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

View File

@ -6,7 +6,13 @@ import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchSeries } from 'Store/Actions/seriesActions'; import { fetchSeries } from 'Store/Actions/seriesActions';
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
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 +57,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated, (state) => state.settings.languages.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 +68,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
languagesIsPopulated, languagesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated, systemStatusIsPopulated,
translationsIsPopulated translationsIsPopulated
) => { ) => {
@ -72,6 +80,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
languagesIsPopulated && languagesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated && systemStatusIsPopulated &&
translationsIsPopulated translationsIsPopulated
); );
@ -86,6 +95,7 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error, (state) => state.settings.languages.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 +106,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
) => { ) => {
@ -107,6 +118,7 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
languagesError || languagesError ||
importListsError || importListsError ||
indexerFlagsError ||
systemStatusError || systemStatusError ||
translationsError translationsError
); );
@ -120,6 +132,7 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
importListsError, importListsError,
indexerFlagsError,
systemStatusError, systemStatusError,
translationsError translationsError
}; };
@ -174,6 +187,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() { dispatchFetchImportLists() {
dispatch(fetchImportLists()); dispatch(fetchImportLists());
}, },
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() { dispatchFetchUISettings() {
dispatch(fetchUISettings()); dispatch(fetchUISettings());
}, },
@ -213,6 +229,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages(); this.props.dispatchFetchLanguages();
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();
@ -238,6 +255,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchLanguages, dispatchFetchLanguages,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations, dispatchFetchTranslations,
@ -278,6 +296,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchLanguages: 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,

View File

@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;

View File

@ -16,6 +16,7 @@ export interface EpisodeFile extends ModelBase {
languages: Language[]; languages: Language[];
quality: QualityModel; quality: QualityModel;
customFormats: CustomFormat[]; customFormats: CustomFormat[];
indexerFlags: number;
mediaInfo: MediaInfo; mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean; qualityCutoffNotMet: boolean;
} }

View File

@ -62,6 +62,7 @@ import {
faFileExport as fasFileExport, faFileExport as fasFileExport,
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,
@ -154,6 +155,7 @@ export const FILE_MISSING = fasFileCircleQuestion;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FINALE_SEASON = fasCirclePause; export const FINALE_SEASON = fasCirclePause;
export const FINALE_SERIES = fasCircleStop; export const FINALE_SERIES = fasCircleStop;
export const FLAG = fasFlag;
export const FOOTNOTE = fasAsterisk; export const FOOTNOTE = fasAsterisk;
export const FOLDER = farFolder; export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen; export const FOLDER_OPEN = fasFolderOpen;

View File

@ -12,6 +12,7 @@ export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect'; export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect'; export const LANGUAGE_SELECT = 'languageSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';

View File

@ -0,0 +1,34 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
interface SelectIndexerFlagsModalProps {
isOpen: boolean;
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) {
const {
isOpen,
indexerFlags,
modalTitle,
onIndexerFlagsSelect,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectIndexerFlagsModalContent
indexerFlags={indexerFlags}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectIndexerFlagsModal;

View File

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

View File

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

View File

@ -0,0 +1,75 @@
import React, { useCallback, useState } 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';
interface SelectIndexerFlagsModalContentProps {
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModalContent(
props: SelectIndexerFlagsModalContentProps
) {
const { modalTitle, onIndexerFlagsSelect, onModalClose } = props;
const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags);
const onIndexerFlagsChange = useCallback(
({ value }: { value: number }) => {
setIndexerFlags(value);
},
[setIndexerFlags]
);
const onIndexerFlagsSelectWrapper = useCallback(() => {
onIndexerFlagsSelect(indexerFlags);
}, [indexerFlags, onIndexerFlagsSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SetIndexerFlagsModalTitle', { modalTitle })}
</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={onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectIndexerFlagsModalContent;

View File

@ -29,6 +29,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import ImportMode from 'InteractiveImport/ImportMode'; import ImportMode from 'InteractiveImport/ImportMode';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import InteractiveImport, { import InteractiveImport, {
InteractiveImportCommandOptions, InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport'; } from 'InteractiveImport/InteractiveImport';
@ -71,7 +72,8 @@ type SelectType =
| 'episode' | 'episode'
| 'releaseGroup' | 'releaseGroup'
| 'quality' | 'quality'
| 'language'; | 'language'
| 'indexerFlags';
type FilterExistingFiles = 'all' | 'new'; type FilterExistingFiles = 'all' | 'new';
@ -135,11 +137,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,
@ -284,8 +296,18 @@ function InteractiveImportModalContent(
} }
} }
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return result; return result;
}, [showSeries]); }, [showSeries, items]);
const selectedIds: number[] = useMemo(() => { const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState); return getSelectedIds(selectedState);
@ -343,6 +365,10 @@ function InteractiveImportModalContent(
key: 'language', key: 'language',
value: translate('SelectLanguage'), value: translate('SelectLanguage'),
}, },
{
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
]; ];
if (allowSeriesChange) { if (allowSeriesChange) {
@ -483,6 +509,7 @@ function InteractiveImportModalContent(
releaseGroup, releaseGroup,
quality, quality,
languages, languages,
indexerFlags,
episodeFileId, episodeFileId,
} = item; } = item;
@ -532,6 +559,7 @@ function InteractiveImportModalContent(
releaseGroup, releaseGroup,
quality, quality,
languages, languages,
indexerFlags,
}); });
return; return;
@ -546,6 +574,7 @@ function InteractiveImportModalContent(
releaseGroup, releaseGroup,
quality, quality,
languages, languages,
indexerFlags,
downloadId, downloadId,
episodeFileId, episodeFileId,
}); });
@ -742,6 +771,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch] [selectedIds, dispatch]
); );
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const orderedSelectedIds = items.reduce((acc: number[], file) => { const orderedSelectedIds = items.reduce((acc: number[], file) => {
if (selectedIds.includes(file.id)) { if (selectedIds.includes(file.id)) {
acc.push(file.id); acc.push(file.id);
@ -947,6 +992,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose} onModalClose={onSelectModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal <ConfirmModal
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}

View File

@ -12,9 +12,11 @@ import Episode from 'Episode/Episode';
import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeQuality from 'Episode/EpisodeQuality';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
@ -41,7 +43,8 @@ type SelectType =
| 'episode' | 'episode'
| 'releaseGroup' | 'releaseGroup'
| 'quality' | 'quality'
| 'language'; | 'language'
| 'indexerFlags';
type SelectedChangeProps = SelectStateInputProps & { type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean; hasEpisodeFileId: boolean;
@ -60,6 +63,7 @@ interface InteractiveImportRowProps {
size: number; size: number;
customFormats?: object[]; customFormats?: object[];
customFormatScore?: number; customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[]; rejections: Rejection[];
columns: Column[]; columns: Column[];
episodeFileId?: number; episodeFileId?: number;
@ -84,6 +88,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
size, size,
customFormats, customFormats,
customFormatScore, customFormatScore,
indexerFlags,
rejections, rejections,
isReprocessing, isReprocessing,
isSelected, isSelected,
@ -100,6 +105,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
() => columns.find((c) => c.name === 'series')?.isVisible ?? false, () => columns.find((c) => c.name === 'series')?.isVisible ?? false,
[columns] [columns]
); );
const isIndexerFlagsColumnVisible = useMemo(
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>( const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null null
@ -306,6 +315,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
[id, dispatch, setSelectModalOpen, selectRowAfterChange] [id, dispatch, setSelectModalOpen, selectRowAfterChange]
); );
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItem({
id,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const seriesTitle = series ? series.title : ''; const seriesTitle = series ? series.title : '';
const isAnime = series?.seriesType === 'anime'; const isAnime = series?.seriesType === 'anime';
@ -332,6 +362,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality; const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages; const showLanguagePlaceholder = isSelected && !languages;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
return ( return (
<TableRow> <TableRow>
@ -448,6 +479,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
) : null} ) : null}
</TableRowCell> </TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={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 ? (
<Popover <Popover
@ -518,6 +571,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onLanguagesSelect={onLanguagesSelect} onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose} onModalClose={onSelectModalClose}
/> />
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
</TableRow> </TableRow>
); );
} }

View File

@ -13,6 +13,7 @@ export interface InteractiveImportCommandOptions {
releaseGroup?: string; releaseGroup?: string;
quality: QualityModel; quality: QualityModel;
languages: Language[]; languages: Language[];
indexerFlags: number;
downloadId?: string; downloadId?: string;
episodeFileId?: number; episodeFileId?: number;
} }
@ -31,6 +32,7 @@ interface InteractiveImport extends ModelBase {
episodes: Episode[]; episodes: Episode[];
qualityWeight: number; qualityWeight: number;
customFormats: object[]; customFormats: object[];
indexerFlags: number;
rejections: Rejection[]; rejections: Rejection[];
episodeFileId?: number; episodeFileId?: number;
} }

View File

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

View File

@ -44,7 +44,8 @@
cursor: default; cursor: default;
} }
.rejected { .rejected,
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px; width: 50px;

View File

@ -6,6 +6,7 @@ interface CssExports {
'download': string; 'download': string;
'downloadIcon': string; 'downloadIcon': string;
'indexer': string; 'indexer': string;
'indexerFlags': string;
'interactiveIcon': string; 'interactiveIcon': string;
'languages': string; 'languages': string;
'manualDownloadContent': string; 'manualDownloadContent': string;

View File

@ -12,6 +12,7 @@ import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeQuality from 'Episode/EpisodeQuality';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language'; import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality'; import { QualityModel } from 'Quality/Quality';
@ -98,6 +99,7 @@ interface InteractiveSearchRowProps {
mappedEpisodeNumbers?: number[]; mappedEpisodeNumbers?: number[];
mappedAbsoluteEpisodeNumbers?: number[]; mappedAbsoluteEpisodeNumbers?: number[];
mappedEpisodeInfo: ReleaseEpisode[]; mappedEpisodeInfo: ReleaseEpisode[];
indexerFlags: number;
rejections: string[]; rejections: string[];
episodeRequested: boolean; episodeRequested: boolean;
downloadAllowed: boolean; downloadAllowed: boolean;
@ -139,6 +141,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
mappedEpisodeNumbers, mappedEpisodeNumbers,
mappedAbsoluteEpisodeNumbers, mappedAbsoluteEpisodeNumbers,
mappedEpisodeInfo, mappedEpisodeInfo,
indexerFlags = 0,
rejections = [], rejections = [],
episodeRequested, episodeRequested,
downloadAllowed, downloadAllowed,
@ -254,10 +257,21 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormats.length customFormats.length
)} )}
tooltip={<EpisodeFormats formats={customFormats} />} tooltip={<EpisodeFormats 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 ? (
<Popover <Popover

View File

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

View File

@ -6,6 +6,7 @@ interface CssExports {
'customFormatScore': string; 'customFormatScore': string;
'episodeNumber': string; 'episodeNumber': string;
'episodeNumberAnime': string; 'episodeNumberAnime': string;
'indexerFlags': string;
'languages': string; 'languages': string;
'monitored': string; 'monitored': string;
'releaseGroup': string; 'releaseGroup': string;

View File

@ -1,22 +1,26 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import 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 EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeNumber from 'Episode/EpisodeNumber'; import EpisodeNumber from 'Episode/EpisodeNumber';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import IndexerFlags from 'Episode/IndexerFlags';
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
import { tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
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 formatRuntime from 'Utilities/Number/formatRuntime'; import formatRuntime from 'Utilities/Number/formatRuntime';
import translate from 'Utilities/String/translate';
import styles from './EpisodeRow.css'; import styles from './EpisodeRow.css';
class EpisodeRow extends Component { class EpisodeRow extends Component {
@ -77,6 +81,7 @@ class EpisodeRow extends Component {
releaseGroup, releaseGroup,
customFormats, customFormats,
customFormatScore, customFormatScore,
indexerFlags,
alternateTitles, alternateTitles,
columns columns
} = this.props; } = this.props;
@ -211,7 +216,7 @@ class EpisodeRow extends Component {
customFormats.length customFormats.length
)} )}
tooltip={<EpisodeFormats formats={customFormats} />} tooltip={<EpisodeFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.LEFT}
/> />
</TableRowCell> </TableRowCell>
); );
@ -322,6 +327,24 @@ class EpisodeRow extends Component {
); );
} }
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<TableRowCell <TableRowCell
@ -381,6 +404,7 @@ EpisodeRow.propTypes = {
releaseGroup: PropTypes.string, releaseGroup: PropTypes.string,
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,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -389,7 +413,8 @@ EpisodeRow.propTypes = {
EpisodeRow.defaultProps = { EpisodeRow.defaultProps = {
alternateTitles: [], alternateTitles: [],
customFormats: [] customFormats: [],
indexerFlags: 0
}; };
export default EpisodeRow; export default EpisodeRow;

View File

@ -1,4 +1,3 @@
/* eslint max-params: 0 */
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
@ -20,6 +19,7 @@ function createMapStateToProps() {
releaseGroup: episodeFile ? episodeFile.releaseGroup : null, releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
customFormats: episodeFile ? episodeFile.customFormats : [], customFormats: episodeFile ? episodeFile.customFormats : [],
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0, customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
indexerFlags: episodeFile ? episodeFile.indexerFlags : 0,
alternateTitles: series.alternateTitles alternateTitles: series.alternateTitles
}; };
} }

View File

@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};

View File

@ -129,6 +129,15 @@ export const defaultState = {
isVisible: false, isVisible: false,
isSortable: true isSortable: true
}, },
{
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'),

View File

@ -161,9 +161,12 @@ export const actionHandlers = handleThunks({
const episodeFile = data.find((f) => f.id === id); const episodeFile = data.find((f) => f.id === id);
props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet; props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet;
props.customFormats = episodeFile.customFormats;
props.customFormatScore = episodeFile.customFormatScore;
props.languages = file.languages; props.languages = file.languages;
props.quality = file.quality; props.quality = file.quality;
props.releaseGroup = file.releaseGroup; props.releaseGroup = file.releaseGroup;
props.indexerFlags = file.indexerFlags;
return updateItem({ return updateItem({
section, section,

View File

@ -162,6 +162,7 @@ export const actionHandlers = handleThunks({
quality: item.quality, quality: item.quality,
languages: item.languages, languages: item.languages,
releaseGroup: item.releaseGroup, releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId downloadId: item.downloadId
}; };
}); });

View File

@ -1,4 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import indexerFlags from 'Store/Actions/Settings/indexerFlags';
import { handleThunks } from 'Store/thunks'; import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import autoTaggings from './Settings/autoTaggings'; import autoTaggings from './Settings/autoTaggings';
@ -37,6 +38,7 @@ export * from './Settings/general';
export * from './Settings/importListOptions'; export * from './Settings/importListOptions';
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';
@ -72,6 +74,7 @@ export const defaultState = {
importLists: importLists.defaultState, importLists: importLists.defaultState,
importListExclusions: importListExclusions.defaultState, importListExclusions: importListExclusions.defaultState,
importListOptions: importListOptions.defaultState, importListOptions: importListOptions.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState, indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState, indexers: indexers.defaultState,
languages: languages.defaultState, languages: languages.defaultState,
@ -116,6 +119,7 @@ export const actionHandlers = handleThunks({
...importLists.actionHandlers, ...importLists.actionHandlers,
...importListExclusions.actionHandlers, ...importListExclusions.actionHandlers,
...importListOptions.actionHandlers, ...importListOptions.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers, ...indexerOptions.actionHandlers,
...indexers.actionHandlers, ...indexers.actionHandlers,
...languages.actionHandlers, ...languages.actionHandlers,
@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...importLists.reducers, ...importLists.reducers,
...importListExclusions.reducers, ...importListExclusions.reducers,
...importListOptions.reducers, ...importListOptions.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers, ...indexerOptions.reducers,
...indexers.reducers, ...indexers.reducers,
...languages.reducers, ...languages.reducers,

View File

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

View File

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

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
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.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
@ -66,6 +67,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>())) .Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Returns(new EpisodeFileMoveResult()); .Returns(new EpisodeFileMoveResult());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EpisodeHistory>());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew() _downloadClientItem = Builder<DownloadClientItem>.CreateNew()
.With(d => d.OutputPath = new OsPath(outputPath)) .With(d => d.OutputPath = new OsPath(outputPath))
.Build(); .Build();

View File

@ -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.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -20,6 +21,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; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }

View File

@ -174,20 +174,25 @@ namespace NzbDrone.Core.Blocklisting
public void Handle(DownloadFailedEvent message) public void Handle(DownloadFailedEvent message)
{ {
var blocklist = new Blocklist var blocklist = new Blocklist
{ {
SeriesId = message.SeriesId, SeriesId = message.SeriesId,
EpisodeIds = message.EpisodeIds, EpisodeIds = message.EpisodeIds,
SourceTitle = message.SourceTitle, SourceTitle = message.SourceTitle,
Quality = message.Quality, Quality = message.Quality,
Date = DateTime.UtcNow, Date = DateTime.UtcNow,
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")), PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")),
Size = long.Parse(message.Data.GetValueOrDefault("size", "0")), Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
Indexer = message.Data.GetValueOrDefault("indexer"), Indexer = message.Data.GetValueOrDefault("indexer"),
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
Message = message.Message, Message = message.Message,
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"),
Languages = message.Languages Languages = message.Languages
}; };
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
blocklist.IndexerFlags = flags;
}
_blocklistRepository.Insert(blocklist); _blocklistRepository.Insert(blocklist);
} }

View File

@ -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;
@ -39,7 +40,8 @@ namespace NzbDrone.Core.CustomFormats
EpisodeInfo = remoteEpisode.ParsedEpisodeInfo, EpisodeInfo = remoteEpisode.ParsedEpisodeInfo,
Series = remoteEpisode.Series, Series = remoteEpisode.Series,
Size = size, Size = size,
Languages = remoteEpisode.Languages Languages = remoteEpisode.Languages,
IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -73,7 +75,8 @@ namespace NzbDrone.Core.CustomFormats
EpisodeInfo = episodeInfo, EpisodeInfo = episodeInfo,
Series = series, Series = series,
Size = blocklist.Size ?? 0, Size = blocklist.Size ?? 0,
Languages = blocklist.Languages Languages = blocklist.Languages,
IndexerFlags = blocklist.IndexerFlags
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -84,6 +87,7 @@ namespace NzbDrone.Core.CustomFormats
var parsed = Parser.Parser.ParseTitle(history.SourceTitle); var parsed = Parser.Parser.ParseTitle(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 episodeInfo = new ParsedEpisodeInfo var episodeInfo = new ParsedEpisodeInfo
{ {
@ -99,7 +103,8 @@ namespace NzbDrone.Core.CustomFormats
EpisodeInfo = episodeInfo, EpisodeInfo = episodeInfo,
Series = series, Series = series,
Size = size, Size = size,
Languages = history.Languages Languages = history.Languages,
IndexerFlags = indexerFlags
}; };
return ParseCustomFormat(input); return ParseCustomFormat(input);
@ -122,6 +127,7 @@ namespace NzbDrone.Core.CustomFormats
Series = localEpisode.Series, Series = localEpisode.Series,
Size = localEpisode.Size, Size = localEpisode.Size,
Languages = localEpisode.Languages, Languages = localEpisode.Languages,
IndexerFlags = localEpisode.IndexerFlags,
Filename = Path.GetFileName(localEpisode.Path) Filename = Path.GetFileName(localEpisode.Path)
}; };
@ -191,6 +197,7 @@ namespace NzbDrone.Core.CustomFormats
Series = series, Series = series,
Size = episodeFile.Size, Size = episodeFile.Size,
Languages = episodeFile.Languages, Languages = episodeFile.Languages,
IndexerFlags = episodeFile.IndexerFlags,
Filename = Path.GetFileName(episodeFile.RelativePath) Filename = Path.GetFileName(episodeFile.RelativePath)
}; };

View File

@ -10,6 +10,7 @@ namespace NzbDrone.Core.CustomFormats
public ParsedEpisodeInfo EpisodeInfo { get; set; } public ParsedEpisodeInfo EpisodeInfo { get; set; }
public Series Series { get; set; } public Series Series { get; set; }
public long Size { get; set; } public long Size { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
public string Filename { get; set; } public string Filename { get; set; }

View File

@ -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((qualityValue, context) =>
{
if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue))
{
context.AddFailure($"Invalid indexer flag condition value: {qualityValue}");
}
});
}
}
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));
}
}
}

View File

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

View File

@ -10,6 +10,7 @@ using NzbDrone.Core.Download.History;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
@ -109,10 +110,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads
try try
{ {
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId) var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId)
.OrderByDescending(h => h.Date) .OrderByDescending(h => h.Date)
.ToList(); .ToList();
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
if (parsedEpisodeInfo != null) if (parsedEpisodeInfo != null)
{ {
@ -134,12 +136,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads
var firstHistoryItem = historyItems.First(); var firstHistoryItem = historyItems.First();
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EpisodeHistoryEventType.Grabbed); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EpisodeHistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
trackedDownload.Added = grabbedEvent?.Date; trackedDownload.Added = grabbedEvent?.Date;
if (parsedEpisodeInfo == null || if (parsedEpisodeInfo == null ||
trackedDownload.RemoteEpisode == null || trackedDownload.RemoteEpisode?.Series == null ||
trackedDownload.RemoteEpisode.Series == null ||
trackedDownload.RemoteEpisode.Episodes.Empty()) trackedDownload.RemoteEpisode.Episodes.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
@ -155,6 +156,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
.Select(h => h.EpisodeId).Distinct()); .Select(h => h.EpisodeId).Distinct());
} }
} }
if (trackedDownload.RemoteEpisode != null &&
Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
trackedDownload.RemoteEpisode.Release ??= new ReleaseInfo();
trackedDownload.RemoteEpisode.Release.IndexerFlags = flags;
}
} }
// Calculate custom formats // Calculate custom formats

View File

@ -169,6 +169,7 @@ namespace NzbDrone.Core.History
history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString()); history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString());
history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString()); history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString());
history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString()); history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString());
history.Data.Add("IndexerFlags", message.Episode.Release.IndexerFlags.ToString());
if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
{ {
@ -201,16 +202,16 @@ namespace NzbDrone.Core.History
foreach (var episode in message.EpisodeInfo.Episodes) foreach (var episode in message.EpisodeInfo.Episodes)
{ {
var history = new EpisodeHistory var history = new EpisodeHistory
{ {
EventType = EpisodeHistoryEventType.DownloadFolderImported, EventType = EpisodeHistoryEventType.DownloadFolderImported,
Date = DateTime.UtcNow, Date = DateTime.UtcNow,
Quality = message.EpisodeInfo.Quality, Quality = message.EpisodeInfo.Quality,
SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path),
SeriesId = message.ImportedEpisode.SeriesId, SeriesId = message.ImportedEpisode.SeriesId,
EpisodeId = episode.Id, EpisodeId = episode.Id,
DownloadId = downloadId, DownloadId = downloadId,
Languages = message.EpisodeInfo.Languages Languages = message.EpisodeInfo.Languages
}; };
history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); history.Data.Add("FileId", message.ImportedEpisode.Id.ToString());
history.Data.Add("DroppedPath", message.EpisodeInfo.Path); history.Data.Add("DroppedPath", message.EpisodeInfo.Path);
@ -220,6 +221,7 @@ namespace NzbDrone.Core.History
history.Data.Add("ReleaseGroup", message.EpisodeInfo.ReleaseGroup); history.Data.Add("ReleaseGroup", message.EpisodeInfo.ReleaseGroup);
history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString()); history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString());
history.Data.Add("Size", message.EpisodeInfo.Size.ToString()); history.Data.Add("Size", message.EpisodeInfo.Size.ToString());
history.Data.Add("IndexerFlags", message.ImportedEpisode.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -280,6 +282,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("Size", message.EpisodeFile.Size.ToString());
history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -311,6 +314,7 @@ namespace NzbDrone.Core.History
history.Data.Add("RelativePath", relativePath); history.Data.Add("RelativePath", relativePath);
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("Size", message.EpisodeFile.Size.ToString());
history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString());
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@ -323,16 +327,16 @@ namespace NzbDrone.Core.History
foreach (var episodeId in message.EpisodeIds) foreach (var episodeId in message.EpisodeIds)
{ {
var history = new EpisodeHistory var history = new EpisodeHistory
{ {
EventType = EpisodeHistoryEventType.DownloadIgnored, EventType = EpisodeHistoryEventType.DownloadIgnored,
Date = DateTime.UtcNow, Date = DateTime.UtcNow,
Quality = message.Quality, Quality = message.Quality,
SourceTitle = message.SourceTitle, SourceTitle = message.SourceTitle,
SeriesId = message.SeriesId, SeriesId = message.SeriesId,
EpisodeId = episodeId, EpisodeId = episodeId,
DownloadId = message.DownloadId, DownloadId = message.DownloadId,
Languages = message.Languages Languages = message.Languages
}; };
history.Data.Add("DownloadClient", message.DownloadClientInfo.Type); history.Data.Add("DownloadClient", message.DownloadClientInfo.Type);
history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name); history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name);

View File

@ -81,7 +81,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
Source = torrent.Source, Source = torrent.Source,
Container = torrent.Container, Container = torrent.Container,
Codec = torrent.Codec, Codec = torrent.Codec,
Resolution = torrent.Resolution Resolution = torrent.Resolution,
IndexerFlags = GetIndexerFlags(torrent)
}; };
if (torrent.TvdbID is > 0) if (torrent.TvdbID is > 0)
@ -100,6 +101,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
return results; return results;
} }
private static IndexerFlags GetIndexerFlags(BroadcastheNetTorrent item)
{
IndexerFlags flags = 0;
flags |= IndexerFlags.Freeleech;
switch (item.Origin.ToUpperInvariant())
{
case "INTERNAL":
flags |= IndexerFlags.Internal;
break;
case "SCENE":
flags |= IndexerFlags.Scene;
break;
}
return flags;
}
private string CleanReleaseName(string releaseName) private string CleanReleaseName(string releaseName)
{ {
return releaseName.Replace("\\", ""); return releaseName.Replace("\\", "");

View File

@ -38,9 +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,
@ -50,13 +48,31 @@ namespace NzbDrone.Core.Indexers.FileList
Seeders = result.Seeders, Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders, Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate.ToUniversalTime(), PublishDate = result.UploadDate.ToUniversalTime(),
ImdbId = result.ImdbId ImdbId = result.ImdbId,
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)

View File

@ -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")]

View File

@ -49,6 +49,7 @@ namespace NzbDrone.Core.Indexers.HDBits
foreach (var result in queryResults) foreach (var result in queryResults)
{ {
var id = result.Id; var id = result.Id;
torrentInfos.Add(new TorrentInfo torrentInfos.Add(new TorrentInfo
{ {
Guid = $"HDBits-{id}", Guid = $"HDBits-{id}",
@ -59,13 +60,31 @@ namespace NzbDrone.Core.Indexers.HDBits
InfoUrl = GetInfoUrl(id), InfoUrl = GetInfoUrl(id),
Seeders = result.Seeders, Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders, Peers = result.Leechers + result.Seeders,
PublishDate = result.Added.ToUniversalTime() PublishDate = result.Added.ToUniversalTime(),
IndexerFlags = GetIndexerFlags(result)
}); });
} }
return torrentInfos.ToArray(); return torrentInfos.ToArray();
} }
private static IndexerFlags GetIndexerFlags(TorrentQueryResponse item)
{
IndexerFlags flags = 0;
if (item.FreeLeech == "yes")
{
flags |= IndexerFlags.Freeleech;
}
if (item.TypeOrigin == 1)
{
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)

View File

@ -78,8 +78,12 @@ namespace NzbDrone.Core.Indexers.Torznab
{ {
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
torrentInfo.TvdbId = GetTvdbId(item); if (torrentInfo != null)
torrentInfo.TvRageId = GetTvRageId(item); {
torrentInfo.TvdbId = GetTvdbId(item);
torrentInfo.TvRageId = GetTvRageId(item);
torrentInfo.IndexerFlags = GetFlags(item);
}
return torrentInfo; return torrentInfo;
} }
@ -214,6 +218,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));
@ -229,6 +280,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));

View File

@ -214,6 +214,7 @@
"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?",
"ClickToChangeEpisode": "Click to change episode", "ClickToChangeEpisode": "Click to change episode",
"ClickToChangeIndexerFlags": "Click to change indexer flags",
"ClickToChangeLanguage": "Click to change language", "ClickToChangeLanguage": "Click to change language",
"ClickToChangeQuality": "Click to change quality", "ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group", "ClickToChangeReleaseGroup": "Click to change release group",
@ -276,6 +277,7 @@
"CustomFormatsLoadError": "Unable to load Custom Formats", "CustomFormatsLoadError": "Unable to load Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings", "CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsSummary": "Custom Formats and Settings", "CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationLanguage": "Language", "CustomFormatsSpecificationLanguage": "Language",
"CustomFormatsSpecificationMaximumSize": "Maximum Size", "CustomFormatsSpecificationMaximumSize": "Maximum Size",
"CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size", "CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size",
@ -916,6 +918,7 @@
"Indexer": "Indexer", "Indexer": "Indexer",
"IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.", "IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.",
"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",
"IndexerHDBitsSettingsCategories": "Categories", "IndexerHDBitsSettingsCategories": "Categories",
"IndexerHDBitsSettingsCategoriesHelpText": "If unspecified, all options are used.", "IndexerHDBitsSettingsCategoriesHelpText": "If unspecified, all options are used.",
"IndexerHDBitsSettingsCodecs": "Codecs", "IndexerHDBitsSettingsCodecs": "Codecs",
@ -1749,6 +1752,7 @@
"SelectEpisodesModalTitle": "{modalTitle} - Select Episode(s)", "SelectEpisodesModalTitle": "{modalTitle} - Select Episode(s)",
"SelectFolder": "Select Folder", "SelectFolder": "Select Folder",
"SelectFolderModalTitle": "{modalTitle} - Select Folder", "SelectFolderModalTitle": "{modalTitle} - Select Folder",
"SelectIndexerFlags": "Select Indexer Flags",
"SelectLanguage": "Select Language", "SelectLanguage": "Select Language",
"SelectLanguageModalTitle": "{modalTitle} - Select Language", "SelectLanguageModalTitle": "{modalTitle} - Select Language",
"SelectLanguages": "Select Languages", "SelectLanguages": "Select Languages",
@ -1790,6 +1794,8 @@
"SeriesType": "Series Type", "SeriesType": "Series Type",
"SeriesTypes": "Series Types", "SeriesTypes": "Series Types",
"SeriesTypesHelpText": "Series type is used for renaming, parsing and searching", "SeriesTypesHelpText": "Series type is used for renaming, parsing and searching",
"SetIndexerFlags": "Set Indexer Flags",
"SetIndexerFlagsModalTitle": "{modalTitle} - 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.",

View File

@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -21,6 +22,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 LazyLoaded<List<Episode>> Episodes { get; set; } public LazyLoaded<List<Episode>> Episodes { get; set; }
public LazyLoaded<Series> Series { get; set; } public LazyLoaded<Series> Series { get; set; }

View File

@ -7,6 +7,7 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Extras; using NzbDrone.Core.Extras;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
@ -28,6 +29,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
private readonly IExtraService _extraService; private readonly IExtraService _extraService;
private readonly IExistingExtraFiles _existingExtraFiles; private readonly IExistingExtraFiles _existingExtraFiles;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger; private readonly Logger _logger;
@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
IExtraService extraService, IExtraService extraService,
IExistingExtraFiles existingExtraFiles, IExistingExtraFiles existingExtraFiles,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IHistoryService historyService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager, IManageCommandQueue commandQueueManager,
Logger logger) Logger logger)
@ -46,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_extraService = extraService; _extraService = extraService;
_existingExtraFiles = existingExtraFiles; _existingExtraFiles = existingExtraFiles;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_historyService = historyService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager; _commandQueueManager = commandQueueManager;
_logger = logger; _logger = logger;
@ -93,6 +97,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; episodeFile.ReleaseGroup = localEpisode.ReleaseGroup;
episodeFile.Languages = localEpisode.Languages; episodeFile.Languages = localEpisode.Languages;
if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true)
{
var grabHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId)
.OrderByDescending(h => h.Date)
.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed);
if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
{
episodeFile.IndexerFlags = flags;
}
}
else
{
episodeFile.IndexerFlags = localEpisode.IndexerFlags;
}
bool copyOnly; bool copyOnly;
switch (importMode) switch (importMode)
{ {

View File

@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { 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 Equals(ManualImportFile other) public bool Equals(ManualImportFile other)

View File

@ -24,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public string DownloadId { get; set; } public string DownloadId { get; set; }
public List<CustomFormat> CustomFormats { get; set; } public List<CustomFormat> CustomFormats { get; set; }
public int CustomFormatScore { get; set; } public int CustomFormatScore { get; set; }
public int IndexerFlags { get; set; }
public IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }
public ManualImportItem() public ManualImportItem()

View File

@ -25,7 +25,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{ {
List<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber); List<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber);
List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles);
ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages); ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags);
} }
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -139,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles); return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles);
} }
public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages) public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List<int> episodeIds, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags)
{ {
var rootFolder = Path.GetDirectoryName(path); var rootFolder = Path.GetDirectoryName(path);
var series = _seriesService.GetSeries(seriesId); var series = _seriesService.GetSeries(seriesId);
@ -171,6 +171,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality;
localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode);
localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0;
localEpisode.IndexerFlags = (IndexerFlags)indexerFlags;
return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null); return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null);
} }
@ -197,7 +198,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
Size = _diskProvider.GetFileSize(path), Size = _diskProvider.GetFileSize(path),
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages,
Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality,
IndexerFlags = (IndexerFlags)indexerFlags
}; };
return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null);
@ -422,6 +424,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
item.Languages = decision.LocalEpisode.Languages; item.Languages = decision.LocalEpisode.Languages;
item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path);
item.Rejections = decision.Rejections; item.Rejections = decision.Rejections;
item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags;
return item; return item;
} }
@ -440,6 +443,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
item.ReleaseGroup = episodeFile.ReleaseGroup; item.ReleaseGroup = episodeFile.ReleaseGroup;
item.Quality = episodeFile.Quality; item.Quality = episodeFile.Quality;
item.Languages = episodeFile.Languages; item.Languages = episodeFile.Languages;
item.IndexerFlags = (int)episodeFile.IndexerFlags;
item.Size = _diskProvider.GetFileSize(item.Path); item.Size = _diskProvider.GetFileSize(item.Path);
item.Rejections = Enumerable.Empty<Rejection>(); item.Rejections = Enumerable.Empty<Rejection>();
item.EpisodeFileId = episodeFile.Id; item.EpisodeFileId = episodeFile.Id;
@ -476,6 +480,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
ReleaseGroup = file.ReleaseGroup, ReleaseGroup = file.ReleaseGroup,
Quality = file.Quality, Quality = file.Quality,
Languages = file.Languages, Languages = file.Languages,
IndexerFlags = (IndexerFlags)file.IndexerFlags,
Series = series, Series = series,
Size = 0 Size = 0
}; };
@ -504,6 +509,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
localEpisode.ReleaseGroup = file.ReleaseGroup; localEpisode.ReleaseGroup = file.ReleaseGroup;
localEpisode.Quality = file.Quality; localEpisode.Quality = file.Quality;
localEpisode.Languages = file.Languages; localEpisode.Languages = file.Languages;
localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags;
// TODO: Cleanup non-tracked downloads // TODO: Cleanup non-tracked downloads
@ -520,10 +526,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
imported.Add(importResult); imported.Add(importResult);
importedTrackedDownload.Add(new ManuallyImportedFile importedTrackedDownload.Add(new ManuallyImportedFile
{ {
TrackedDownload = trackedDownload, TrackedDownload = trackedDownload,
ImportResult = importResult ImportResult = importResult
}); });
} }
} }

View File

@ -89,6 +89,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Release_Quality", remoteEpisode.ParsedEpisodeInfo.Quality.Quality.Name); environmentVariables.Add("Sonarr_Release_Quality", remoteEpisode.ParsedEpisodeInfo.Quality.Quality.Name);
environmentVariables.Add("Sonarr_Release_QualityVersion", remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version.ToString()); environmentVariables.Add("Sonarr_Release_QualityVersion", remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version.ToString());
environmentVariables.Add("Sonarr_Release_ReleaseGroup", releaseGroup ?? string.Empty); environmentVariables.Add("Sonarr_Release_ReleaseGroup", releaseGroup ?? string.Empty);
environmentVariables.Add("Sonarr_Release_IndexerFlags", remoteEpisode.Release.IndexerFlags.ToString());
environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientName ?? string.Empty); environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientName ?? string.Empty);
environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientType ?? string.Empty); environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientType ?? string.Empty);
environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty); environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty);

View File

@ -31,6 +31,7 @@ namespace NzbDrone.Core.Parser.Model
public List<DeletedEpisodeFile> OldFiles { get; set; } public List<DeletedEpisodeFile> OldFiles { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public MediaInfoModel MediaInfo { get; set; } public MediaInfoModel MediaInfo { get; set; }
public bool ExistingFile { get; set; } public bool ExistingFile { get; set; }
public bool SceneSource { get; set; } public bool SceneSource { get; set; }

View File

@ -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;
@ -39,6 +39,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; }
@ -107,4 +110,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.
}
} }

View File

@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Sonarr.Http; using Sonarr.Http;
@ -203,6 +204,11 @@ namespace Sonarr.Api.V3.EpisodeFiles
{ {
episodeFile.ReleaseGroup = resourceEpisodeFile.ReleaseGroup; episodeFile.ReleaseGroup = resourceEpisodeFile.ReleaseGroup;
} }
if (resourceEpisodeFile.IndexerFlags.HasValue)
{
episodeFile.IndexerFlags = (IndexerFlags)resourceEpisodeFile.IndexerFlags;
}
} }
_mediaFileService.Update(episodeFiles); _mediaFileService.Update(episodeFiles);

View File

@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.EpisodeFiles
public QualityModel Quality { get; set; } public QualityModel Quality { 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; }
@ -88,7 +89,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
MediaInfo = model.MediaInfo.ToResource(model.SceneName), MediaInfo = model.MediaInfo.ToResource(model.SceneName),
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality),
CustomFormats = customFormats.ToResource(false), CustomFormats = customFormats.ToResource(false),
CustomFormatScore = customFormatScore CustomFormatScore = customFormatScore,
IndexerFlags = (int)model.IndexerFlags
}; };
} }
} }

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Parser.Model;
using Sonarr.Http;
namespace Sonarr.Api.V3.Indexers
{
[V3ApiController]
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();
}
}
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.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();
}
}

View File

@ -65,6 +65,7 @@ namespace Sonarr.Api.V3.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; }
public bool IsDaily { get; set; } public bool IsDaily { get; set; }
public bool IsAbsoluteNumbering { get; set; } public bool IsAbsoluteNumbering { get; set; }
@ -100,6 +101,7 @@ namespace Sonarr.Api.V3.Indexers
var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo;
var remoteEpisode = model.RemoteEpisode; var remoteEpisode = model.RemoteEpisode;
var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); var torrentInfo = (model.RemoteEpisode.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
@ -152,6 +154,7 @@ namespace Sonarr.Api.V3.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,
IsDaily = parsedEpisodeInfo.IsDaily, IsDaily = parsedEpisodeInfo.IsDaily,
IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering, IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering,

View File

@ -39,10 +39,11 @@ namespace Sonarr.Api.V3.ManualImport
{ {
foreach (var item in items) foreach (var item in items)
{ {
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages); var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags);
item.SeasonNumber = processedItem.SeasonNumber; item.SeasonNumber = processedItem.SeasonNumber;
item.Episodes = processedItem.Episodes.ToResource(); item.Episodes = processedItem.Episodes.ToResource();
item.IndexerFlags = processedItem.IndexerFlags;
item.Rejections = processedItem.Rejections; item.Rejections = processedItem.Rejections;
item.CustomFormats = processedItem.CustomFormats.ToResource(false); item.CustomFormats = processedItem.CustomFormats.ToResource(false);
item.CustomFormatScore = processedItem.CustomFormatScore; item.CustomFormatScore = processedItem.CustomFormatScore;

View File

@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.ManualImport
public string DownloadId { get; set; } public string DownloadId { 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 IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }
} }
} }

View File

@ -30,6 +30,7 @@ namespace Sonarr.Api.V3.ManualImport
public string DownloadId { get; set; } public string DownloadId { 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 IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }
} }
@ -65,6 +66,7 @@ namespace Sonarr.Api.V3.ManualImport
// QualityWeight // QualityWeight
DownloadId = model.DownloadId, DownloadId = model.DownloadId,
IndexerFlags = model.IndexerFlags,
Rejections = model.Rejections Rejections = model.Rejections
}; };
} }