parent
a57254640f
commit
7a768b5d0f
|
@ -9,6 +9,7 @@ import DownloadClient from 'typings/DownloadClient';
|
|||
import ImportList from 'typings/ImportList';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
@ -40,19 +41,21 @@ export interface ImportListOptionsSettingsAppState
|
|||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
downloadClients: DownloadClientAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
@ -11,6 +11,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
||||
|
@ -71,6 +72,9 @@ function getComponent(type) {
|
|||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInput;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
|
@ -279,6 +283,7 @@ FormInputGroup.propTypes = {
|
|||
includeNoChange: PropTypes.bool,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
indexerFlags: PropTypes.number,
|
||||
pending: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
|
|
|
@ -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,13 @@ import { createSelector } from 'reselect';
|
|||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
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 { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
|
@ -51,6 +57,7 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.settings.importLists.isPopulated,
|
||||
(state) => state.settings.indexerFlags.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
|
@ -61,6 +68,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated,
|
||||
languagesIsPopulated,
|
||||
importListsIsPopulated,
|
||||
indexerFlagsIsPopulated,
|
||||
systemStatusIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
|
@ -72,6 +80,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
indexerFlagsIsPopulated &&
|
||||
systemStatusIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
|
@ -86,6 +95,7 @@ const selectErrors = createSelector(
|
|||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.settings.importLists.error,
|
||||
(state) => state.settings.indexerFlags.error,
|
||||
(state) => state.system.status.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
|
@ -96,6 +106,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
languagesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
) => {
|
||||
|
@ -107,6 +118,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError ||
|
||||
languagesError ||
|
||||
importListsError ||
|
||||
indexerFlagsError ||
|
||||
systemStatusError ||
|
||||
translationsError
|
||||
);
|
||||
|
@ -120,6 +132,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
languagesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
};
|
||||
|
@ -174,6 +187,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchImportLists() {
|
||||
dispatch(fetchImportLists());
|
||||
},
|
||||
dispatchFetchIndexerFlags() {
|
||||
dispatch(fetchIndexerFlags());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
|
@ -213,6 +229,7 @@ class PageConnector extends Component {
|
|||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchImportLists();
|
||||
this.props.dispatchFetchIndexerFlags();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
this.props.dispatchFetchTranslations();
|
||||
|
@ -238,6 +255,7 @@ class PageConnector extends Component {
|
|||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchIndexerFlags,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
|
@ -278,6 +296,7 @@ PageConnector.propTypes = {
|
|||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||
|
|
|
@ -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;
|
|
@ -16,6 +16,7 @@ export interface EpisodeFile extends ModelBase {
|
|||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
indexerFlags: number;
|
||||
mediaInfo: MediaInfo;
|
||||
qualityCutoffNotMet: boolean;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ import {
|
|||
faFileExport as fasFileExport,
|
||||
faFileInvoice as farFileInvoice,
|
||||
faFilter as fasFilter,
|
||||
faFlag as fasFlag,
|
||||
faFolderOpen as fasFolderOpen,
|
||||
faForward as fasForward,
|
||||
faHeart as fasHeart,
|
||||
|
@ -154,6 +155,7 @@ export const FILE_MISSING = fasFileCircleQuestion;
|
|||
export const FILTER = fasFilter;
|
||||
export const FINALE_SEASON = fasCirclePause;
|
||||
export const FINALE_SERIES = fasCircleStop;
|
||||
export const FLAG = fasFlag;
|
||||
export const FOOTNOTE = fasAsterisk;
|
||||
export const FOLDER = farFolder;
|
||||
export const FOLDER_OPEN = fasFolderOpen;
|
||||
|
|
|
@ -12,6 +12,7 @@ export const PASSWORD = 'password';
|
|||
export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||
export const LANGUAGE_SELECT = 'languageSelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
7
frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts
vendored
Normal file
7
frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'modalBody': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,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;
|
|
@ -29,6 +29,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
|||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||
import ImportMode from 'InteractiveImport/ImportMode';
|
||||
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||
import InteractiveImport, {
|
||||
InteractiveImportCommandOptions,
|
||||
} from 'InteractiveImport/InteractiveImport';
|
||||
|
@ -71,7 +72,8 @@ type SelectType =
|
|||
| 'episode'
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language';
|
||||
| 'language'
|
||||
| 'indexerFlags';
|
||||
|
||||
type FilterExistingFiles = 'all' | 'new';
|
||||
|
||||
|
@ -135,11 +137,21 @@ const COLUMNS = [
|
|||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER,
|
||||
title: () => translate('Rejections'),
|
||||
}),
|
||||
isSortable: 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;
|
||||
}, [showSeries]);
|
||||
}, [showSeries, items]);
|
||||
|
||||
const selectedIds: number[] = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
|
@ -343,6 +365,10 @@ function InteractiveImportModalContent(
|
|||
key: 'language',
|
||||
value: translate('SelectLanguage'),
|
||||
},
|
||||
{
|
||||
key: 'indexerFlags',
|
||||
value: translate('SelectIndexerFlags'),
|
||||
},
|
||||
];
|
||||
|
||||
if (allowSeriesChange) {
|
||||
|
@ -483,6 +509,7 @@ function InteractiveImportModalContent(
|
|||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
indexerFlags,
|
||||
episodeFileId,
|
||||
} = item;
|
||||
|
||||
|
@ -532,6 +559,7 @@ function InteractiveImportModalContent(
|
|||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
indexerFlags,
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -546,6 +574,7 @@ function InteractiveImportModalContent(
|
|||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
indexerFlags,
|
||||
downloadId,
|
||||
episodeFileId,
|
||||
});
|
||||
|
@ -742,6 +771,22 @@ function InteractiveImportModalContent(
|
|||
[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) => {
|
||||
if (selectedIds.includes(file.id)) {
|
||||
acc.push(file.id);
|
||||
|
@ -947,6 +992,14 @@ function InteractiveImportModalContent(
|
|||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectIndexerFlagsModal
|
||||
isOpen={selectModalOpen === 'indexerFlags'}
|
||||
indexerFlags={0}
|
||||
modalTitle={modalTitle}
|
||||
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
|
|
|
@ -12,9 +12,11 @@ import Episode from 'Episode/Episode';
|
|||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
|
@ -41,7 +43,8 @@ type SelectType =
|
|||
| 'episode'
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language';
|
||||
| 'language'
|
||||
| 'indexerFlags';
|
||||
|
||||
type SelectedChangeProps = SelectStateInputProps & {
|
||||
hasEpisodeFileId: boolean;
|
||||
|
@ -60,6 +63,7 @@ interface InteractiveImportRowProps {
|
|||
size: number;
|
||||
customFormats?: object[];
|
||||
customFormatScore?: number;
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
columns: Column[];
|
||||
episodeFileId?: number;
|
||||
|
@ -84,6 +88,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
size,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
rejections,
|
||||
isReprocessing,
|
||||
isSelected,
|
||||
|
@ -100,6 +105,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
|
||||
[columns]
|
||||
);
|
||||
const isIndexerFlagsColumnVisible = useMemo(
|
||||
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
|
||||
[columns]
|
||||
);
|
||||
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
|
@ -306,6 +315,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
[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 isAnime = series?.seriesType === 'anime';
|
||||
|
||||
|
@ -332,6 +362,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !languages;
|
||||
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
|
@ -448,6 +479,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
) : null}
|
||||
</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>
|
||||
{rejections.length ? (
|
||||
<Popover
|
||||
|
@ -518,6 +571,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
onLanguagesSelect={onLanguagesSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectIndexerFlagsModal
|
||||
isOpen={selectModalOpen === 'indexerFlags'}
|
||||
indexerFlags={indexerFlags ?? 0}
|
||||
modalTitle={modalTitle}
|
||||
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface InteractiveImportCommandOptions {
|
|||
releaseGroup?: string;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
indexerFlags: number;
|
||||
downloadId?: string;
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
@ -31,6 +32,7 @@ interface InteractiveImport extends ModelBase {
|
|||
episodes: Episode[];
|
||||
qualityWeight: number;
|
||||
customFormats: object[];
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,15 @@ const columns = [
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.rejected {
|
||||
.rejected,
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
|
|
|
@ -6,6 +6,7 @@ interface CssExports {
|
|||
'download': string;
|
||||
'downloadIcon': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'interactiveIcon': string;
|
||||
'languages': string;
|
||||
'manualDownloadContent': string;
|
||||
|
|
|
@ -12,6 +12,7 @@ import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
|
@ -98,6 +99,7 @@ interface InteractiveSearchRowProps {
|
|||
mappedEpisodeNumbers?: number[];
|
||||
mappedAbsoluteEpisodeNumbers?: number[];
|
||||
mappedEpisodeInfo: ReleaseEpisode[];
|
||||
indexerFlags: number;
|
||||
rejections: string[];
|
||||
episodeRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
|
@ -139,6 +141,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||
mappedEpisodeNumbers,
|
||||
mappedAbsoluteEpisodeNumbers,
|
||||
mappedEpisodeInfo,
|
||||
indexerFlags = 0,
|
||||
rejections = [],
|
||||
episodeRequested,
|
||||
downloadAllowed,
|
||||
|
@ -254,10 +257,21 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||
customFormats.length
|
||||
)}
|
||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{indexerFlags ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{rejections.length ? (
|
||||
<Popover
|
||||
|
|
|
@ -62,3 +62,9 @@
|
|||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ interface CssExports {
|
|||
'customFormatScore': string;
|
||||
'episodeNumber': string;
|
||||
'episodeNumberAnime': string;
|
||||
'indexerFlags': string;
|
||||
'languages': string;
|
||||
'monitored': string;
|
||||
'releaseGroup': string;
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeNumber from 'Episode/EpisodeNumber';
|
||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
||||
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
||||
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 formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import formatRuntime from 'Utilities/Number/formatRuntime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EpisodeRow.css';
|
||||
|
||||
class EpisodeRow extends Component {
|
||||
|
@ -77,6 +81,7 @@ class EpisodeRow extends Component {
|
|||
releaseGroup,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
alternateTitles,
|
||||
columns
|
||||
} = this.props;
|
||||
|
@ -211,7 +216,7 @@ class EpisodeRow extends Component {
|
|||
customFormats.length
|
||||
)}
|
||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</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') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -381,6 +404,7 @@ EpisodeRow.propTypes = {
|
|||
releaseGroup: PropTypes.string,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
@ -389,7 +413,8 @@ EpisodeRow.propTypes = {
|
|||
|
||||
EpisodeRow.defaultProps = {
|
||||
alternateTitles: [],
|
||||
customFormats: []
|
||||
customFormats: [],
|
||||
indexerFlags: 0
|
||||
};
|
||||
|
||||
export default EpisodeRow;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint max-params: 0 */
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||
|
@ -20,6 +19,7 @@ function createMapStateToProps() {
|
|||
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
||||
customFormats: episodeFile ? episodeFile.customFormats : [],
|
||||
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
|
||||
indexerFlags: episodeFile ? episodeFile.indexerFlags : 0,
|
||||
alternateTitles: series.alternateTitles
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
||||
}
|
||||
|
||||
};
|
|
@ -129,6 +129,15 @@ export const defaultState = {
|
|||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
|
|
|
@ -161,9 +161,12 @@ export const actionHandlers = handleThunks({
|
|||
const episodeFile = data.find((f) => f.id === id);
|
||||
|
||||
props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet;
|
||||
props.customFormats = episodeFile.customFormats;
|
||||
props.customFormatScore = episodeFile.customFormatScore;
|
||||
props.languages = file.languages;
|
||||
props.quality = file.quality;
|
||||
props.releaseGroup = file.releaseGroup;
|
||||
props.indexerFlags = file.indexerFlags;
|
||||
|
||||
return updateItem({
|
||||
section,
|
||||
|
|
|
@ -162,6 +162,7 @@ export const actionHandlers = handleThunks({
|
|||
quality: item.quality,
|
||||
languages: item.languages,
|
||||
releaseGroup: item.releaseGroup,
|
||||
indexerFlags: item.indexerFlags,
|
||||
downloadId: item.downloadId
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import indexerFlags from 'Store/Actions/Settings/indexerFlags';
|
||||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import autoTaggings from './Settings/autoTaggings';
|
||||
|
@ -37,6 +38,7 @@ export * from './Settings/general';
|
|||
export * from './Settings/importListOptions';
|
||||
export * from './Settings/importLists';
|
||||
export * from './Settings/importListExclusions';
|
||||
export * from './Settings/indexerFlags';
|
||||
export * from './Settings/indexerOptions';
|
||||
export * from './Settings/indexers';
|
||||
export * from './Settings/languages';
|
||||
|
@ -72,6 +74,7 @@ export const defaultState = {
|
|||
importLists: importLists.defaultState,
|
||||
importListExclusions: importListExclusions.defaultState,
|
||||
importListOptions: importListOptions.defaultState,
|
||||
indexerFlags: indexerFlags.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState,
|
||||
|
@ -116,6 +119,7 @@ export const actionHandlers = handleThunks({
|
|||
...importLists.actionHandlers,
|
||||
...importListExclusions.actionHandlers,
|
||||
...importListOptions.actionHandlers,
|
||||
...indexerFlags.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
|
@ -151,6 +155,7 @@ export const reducers = createHandleActions({
|
|||
...importLists.reducers,
|
||||
...importListExclusions.reducers,
|
||||
...importListOptions.reducers,
|
||||
...indexerFlags.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const createIndexerFlagsSelector = createSelector(
|
||||
(state: AppState) => state.settings.indexerFlags,
|
||||
(indexerFlags) => indexerFlags
|
||||
);
|
||||
|
||||
export default createIndexerFlagsSelector;
|
|
@ -0,0 +1,6 @@
|
|||
interface IndexerFlag {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default IndexerFlag;
|
|
@ -8,6 +8,7 @@ using NUnit.Framework;
|
|||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||
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>()))
|
||||
.Returns(new EpisodeFileMoveResult());
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
|
||||
.Returns(new List<EpisodeHistory>());
|
||||
|
||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
||||
.With(d => d.OutputPath = new OsPath(outputPath))
|
||||
.Build();
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
|
@ -20,6 +21,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||
public long? Size { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string TorrentInfoHash { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
|
|
|
@ -174,20 +174,25 @@ namespace NzbDrone.Core.Blocklisting
|
|||
public void Handle(DownloadFailedEvent message)
|
||||
{
|
||||
var blocklist = new Blocklist
|
||||
{
|
||||
SeriesId = message.SeriesId,
|
||||
EpisodeIds = message.EpisodeIds,
|
||||
SourceTitle = message.SourceTitle,
|
||||
Quality = message.Quality,
|
||||
Date = DateTime.UtcNow,
|
||||
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")),
|
||||
Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
|
||||
Indexer = message.Data.GetValueOrDefault("indexer"),
|
||||
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
||||
Message = message.Message,
|
||||
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"),
|
||||
Languages = message.Languages
|
||||
};
|
||||
{
|
||||
SeriesId = message.SeriesId,
|
||||
EpisodeIds = message.EpisodeIds,
|
||||
SourceTitle = message.SourceTitle,
|
||||
Quality = message.Quality,
|
||||
Date = DateTime.UtcNow,
|
||||
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")),
|
||||
Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
|
||||
Indexer = message.Data.GetValueOrDefault("indexer"),
|
||||
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
||||
Message = message.Message,
|
||||
TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"),
|
||||
Languages = message.Languages
|
||||
};
|
||||
|
||||
if (Enum.TryParse(message.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags))
|
||||
{
|
||||
blocklist.IndexerFlags = flags;
|
||||
}
|
||||
|
||||
_blocklistRepository.Insert(blocklist);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -39,7 +40,8 @@ namespace NzbDrone.Core.CustomFormats
|
|||
EpisodeInfo = remoteEpisode.ParsedEpisodeInfo,
|
||||
Series = remoteEpisode.Series,
|
||||
Size = size,
|
||||
Languages = remoteEpisode.Languages
|
||||
Languages = remoteEpisode.Languages,
|
||||
IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
@ -73,7 +75,8 @@ namespace NzbDrone.Core.CustomFormats
|
|||
EpisodeInfo = episodeInfo,
|
||||
Series = series,
|
||||
Size = blocklist.Size ?? 0,
|
||||
Languages = blocklist.Languages
|
||||
Languages = blocklist.Languages,
|
||||
IndexerFlags = blocklist.IndexerFlags
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
@ -84,6 +87,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||
var parsed = Parser.Parser.ParseTitle(history.SourceTitle);
|
||||
|
||||
long.TryParse(history.Data.GetValueOrDefault("size"), out var size);
|
||||
Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags);
|
||||
|
||||
var episodeInfo = new ParsedEpisodeInfo
|
||||
{
|
||||
|
@ -99,7 +103,8 @@ namespace NzbDrone.Core.CustomFormats
|
|||
EpisodeInfo = episodeInfo,
|
||||
Series = series,
|
||||
Size = size,
|
||||
Languages = history.Languages
|
||||
Languages = history.Languages,
|
||||
IndexerFlags = indexerFlags
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
|
@ -122,6 +127,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||
Series = localEpisode.Series,
|
||||
Size = localEpisode.Size,
|
||||
Languages = localEpisode.Languages,
|
||||
IndexerFlags = localEpisode.IndexerFlags,
|
||||
Filename = Path.GetFileName(localEpisode.Path)
|
||||
};
|
||||
|
||||
|
@ -191,6 +197,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||
Series = series,
|
||||
Size = episodeFile.Size,
|
||||
Languages = episodeFile.Languages,
|
||||
IndexerFlags = episodeFile.IndexerFlags,
|
||||
Filename = Path.GetFileName(episodeFile.RelativePath)
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace NzbDrone.Core.CustomFormats
|
|||
public ParsedEpisodeInfo EpisodeInfo { get; set; }
|
||||
public Series Series { get; set; }
|
||||
public long Size { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public string Filename { get; set; }
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ using NzbDrone.Core.Download.History;
|
|||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
|
@ -109,10 +110,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
|||
|
||||
try
|
||||
{
|
||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
|
||||
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.ToList();
|
||||
.OrderByDescending(h => h.Date)
|
||||
.ToList();
|
||||
|
||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
|
||||
|
||||
if (parsedEpisodeInfo != null)
|
||||
{
|
||||
|
@ -134,12 +136,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
|||
var firstHistoryItem = historyItems.First();
|
||||
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EpisodeHistoryEventType.Grabbed);
|
||||
|
||||
trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
|
||||
trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
|
||||
trackedDownload.Added = grabbedEvent?.Date;
|
||||
|
||||
if (parsedEpisodeInfo == null ||
|
||||
trackedDownload.RemoteEpisode == null ||
|
||||
trackedDownload.RemoteEpisode.Series == null ||
|
||||
trackedDownload.RemoteEpisode?.Series == null ||
|
||||
trackedDownload.RemoteEpisode.Episodes.Empty())
|
||||
{
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -169,6 +169,7 @@ namespace NzbDrone.Core.History
|
|||
history.Data.Add("CustomFormatScore", message.Episode.CustomFormatScore.ToString());
|
||||
history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString());
|
||||
history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString());
|
||||
history.Data.Add("IndexerFlags", message.Episode.Release.IndexerFlags.ToString());
|
||||
|
||||
if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
|
||||
{
|
||||
|
@ -201,16 +202,16 @@ namespace NzbDrone.Core.History
|
|||
foreach (var episode in message.EpisodeInfo.Episodes)
|
||||
{
|
||||
var history = new EpisodeHistory
|
||||
{
|
||||
EventType = EpisodeHistoryEventType.DownloadFolderImported,
|
||||
Date = DateTime.UtcNow,
|
||||
Quality = message.EpisodeInfo.Quality,
|
||||
SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path),
|
||||
SeriesId = message.ImportedEpisode.SeriesId,
|
||||
EpisodeId = episode.Id,
|
||||
DownloadId = downloadId,
|
||||
Languages = message.EpisodeInfo.Languages
|
||||
};
|
||||
{
|
||||
EventType = EpisodeHistoryEventType.DownloadFolderImported,
|
||||
Date = DateTime.UtcNow,
|
||||
Quality = message.EpisodeInfo.Quality,
|
||||
SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path),
|
||||
SeriesId = message.ImportedEpisode.SeriesId,
|
||||
EpisodeId = episode.Id,
|
||||
DownloadId = downloadId,
|
||||
Languages = message.EpisodeInfo.Languages
|
||||
};
|
||||
|
||||
history.Data.Add("FileId", message.ImportedEpisode.Id.ToString());
|
||||
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("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString());
|
||||
history.Data.Add("Size", message.EpisodeInfo.Size.ToString());
|
||||
history.Data.Add("IndexerFlags", message.ImportedEpisode.IndexerFlags.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
@ -280,6 +282,7 @@ namespace NzbDrone.Core.History
|
|||
history.Data.Add("Reason", message.Reason.ToString());
|
||||
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
|
||||
history.Data.Add("Size", message.EpisodeFile.Size.ToString());
|
||||
history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
@ -311,6 +314,7 @@ namespace NzbDrone.Core.History
|
|||
history.Data.Add("RelativePath", relativePath);
|
||||
history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup);
|
||||
history.Data.Add("Size", message.EpisodeFile.Size.ToString());
|
||||
history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString());
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
@ -323,16 +327,16 @@ namespace NzbDrone.Core.History
|
|||
foreach (var episodeId in message.EpisodeIds)
|
||||
{
|
||||
var history = new EpisodeHistory
|
||||
{
|
||||
EventType = EpisodeHistoryEventType.DownloadIgnored,
|
||||
Date = DateTime.UtcNow,
|
||||
Quality = message.Quality,
|
||||
SourceTitle = message.SourceTitle,
|
||||
SeriesId = message.SeriesId,
|
||||
EpisodeId = episodeId,
|
||||
DownloadId = message.DownloadId,
|
||||
Languages = message.Languages
|
||||
};
|
||||
{
|
||||
EventType = EpisodeHistoryEventType.DownloadIgnored,
|
||||
Date = DateTime.UtcNow,
|
||||
Quality = message.Quality,
|
||||
SourceTitle = message.SourceTitle,
|
||||
SeriesId = message.SeriesId,
|
||||
EpisodeId = episodeId,
|
||||
DownloadId = message.DownloadId,
|
||||
Languages = message.Languages
|
||||
};
|
||||
|
||||
history.Data.Add("DownloadClient", message.DownloadClientInfo.Type);
|
||||
history.Data.Add("DownloadClientName", message.DownloadClientInfo.Name);
|
||||
|
|
|
@ -81,7 +81,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
|||
Source = torrent.Source,
|
||||
Container = torrent.Container,
|
||||
Codec = torrent.Codec,
|
||||
Resolution = torrent.Resolution
|
||||
Resolution = torrent.Resolution,
|
||||
IndexerFlags = GetIndexerFlags(torrent)
|
||||
};
|
||||
|
||||
if (torrent.TvdbID is > 0)
|
||||
|
@ -100,6 +101,24 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
|||
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)
|
||||
{
|
||||
return releaseName.Replace("\\", "");
|
||||
|
|
|
@ -38,9 +38,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
|||
{
|
||||
var id = result.Id;
|
||||
|
||||
// if (result.FreeLeech)
|
||||
|
||||
torrentInfos.Add(new TorrentInfo()
|
||||
torrentInfos.Add(new TorrentInfo
|
||||
{
|
||||
Guid = $"FileList-{id}",
|
||||
Title = result.Name,
|
||||
|
@ -50,13 +48,31 @@ namespace NzbDrone.Core.Indexers.FileList
|
|||
Seeders = result.Seeders,
|
||||
Peers = result.Leechers + result.Seeders,
|
||||
PublishDate = result.UploadDate.ToUniversalTime(),
|
||||
ImdbId = result.ImdbId
|
||||
ImdbId = result.ImdbId,
|
||||
IndexerFlags = GetIndexerFlags(result)
|
||||
});
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
private static IndexerFlags GetIndexerFlags(FileListTorrent item)
|
||||
{
|
||||
IndexerFlags flags = 0;
|
||||
|
||||
if (item.FreeLeech)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech;
|
||||
}
|
||||
|
||||
if (item.Internal)
|
||||
{
|
||||
flags |= IndexerFlags.Internal;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private string GetDownloadUrl(string torrentId)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
|||
public uint Files { get; set; }
|
||||
[JsonProperty(PropertyName = "imdb")]
|
||||
public string ImdbId { get; set; }
|
||||
public bool Internal { get; set; }
|
||||
[JsonProperty(PropertyName = "freeleech")]
|
||||
public bool FreeLeech { get; set; }
|
||||
[JsonProperty(PropertyName = "upload_date")]
|
||||
|
|
|
@ -49,6 +49,7 @@ namespace NzbDrone.Core.Indexers.HDBits
|
|||
foreach (var result in queryResults)
|
||||
{
|
||||
var id = result.Id;
|
||||
|
||||
torrentInfos.Add(new TorrentInfo
|
||||
{
|
||||
Guid = $"HDBits-{id}",
|
||||
|
@ -59,13 +60,31 @@ namespace NzbDrone.Core.Indexers.HDBits
|
|||
InfoUrl = GetInfoUrl(id),
|
||||
Seeders = result.Seeders,
|
||||
Peers = result.Leechers + result.Seeders,
|
||||
PublishDate = result.Added.ToUniversalTime()
|
||||
PublishDate = result.Added.ToUniversalTime(),
|
||||
IndexerFlags = GetIndexerFlags(result)
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var url = new HttpUri(_settings.BaseUrl)
|
||||
|
|
|
@ -78,8 +78,12 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
{
|
||||
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
|
||||
|
||||
torrentInfo.TvdbId = GetTvdbId(item);
|
||||
torrentInfo.TvRageId = GetTvRageId(item);
|
||||
if (torrentInfo != null)
|
||||
{
|
||||
torrentInfo.TvdbId = GetTvdbId(item);
|
||||
torrentInfo.TvRageId = GetTvRageId(item);
|
||||
torrentInfo.IndexerFlags = GetFlags(item);
|
||||
}
|
||||
|
||||
return torrentInfo;
|
||||
}
|
||||
|
@ -214,6 +218,53 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
return base.GetPeers(item);
|
||||
}
|
||||
|
||||
protected IndexerFlags GetFlags(XElement item)
|
||||
{
|
||||
IndexerFlags flags = 0;
|
||||
|
||||
var downloadFactor = TryGetFloatTorznabAttribute(item, "downloadvolumefactor", 1);
|
||||
var uploadFactor = TryGetFloatTorznabAttribute(item, "uploadvolumefactor", 1);
|
||||
|
||||
if (downloadFactor == 0.5)
|
||||
{
|
||||
flags |= IndexerFlags.Halfleech;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.75)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech25;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.25)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech75;
|
||||
}
|
||||
|
||||
if (downloadFactor == 0.0)
|
||||
{
|
||||
flags |= IndexerFlags.Freeleech;
|
||||
}
|
||||
|
||||
if (uploadFactor == 2.0)
|
||||
{
|
||||
flags |= IndexerFlags.DoubleUpload;
|
||||
}
|
||||
|
||||
var tags = TryGetMultipleTorznabAttributes(item, "tag");
|
||||
|
||||
if (tags.Any(t => t.EqualsIgnoreCase("internal")))
|
||||
{
|
||||
flags |= IndexerFlags.Internal;
|
||||
}
|
||||
|
||||
if (tags.Any(t => t.EqualsIgnoreCase("scene")))
|
||||
{
|
||||
flags |= IndexerFlags.Scene;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
protected string TryGetTorznabAttribute(XElement item, string key, string defaultValue = "")
|
||||
{
|
||||
var attrElement = item.Elements(ns + "attr").FirstOrDefault(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
|
@ -229,6 +280,13 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
protected float TryGetFloatTorznabAttribute(XElement item, string key, float defaultValue = 0)
|
||||
{
|
||||
var attr = TryGetTorznabAttribute(item, key, defaultValue.ToString());
|
||||
|
||||
return float.TryParse(attr, out var result) ? result : defaultValue;
|
||||
}
|
||||
|
||||
protected List<string> TryGetMultipleTorznabAttributes(XElement item, string key)
|
||||
{
|
||||
var attrElements = item.Elements(ns + "attr").Where(e => e.Attribute("name").Value.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
|
|
|
@ -214,6 +214,7 @@
|
|||
"ClearBlocklist": "Clear blocklist",
|
||||
"ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?",
|
||||
"ClickToChangeEpisode": "Click to change episode",
|
||||
"ClickToChangeIndexerFlags": "Click to change indexer flags",
|
||||
"ClickToChangeLanguage": "Click to change language",
|
||||
"ClickToChangeQuality": "Click to change quality",
|
||||
"ClickToChangeReleaseGroup": "Click to change release group",
|
||||
|
@ -276,6 +277,7 @@
|
|||
"CustomFormatsLoadError": "Unable to load Custom Formats",
|
||||
"CustomFormatsSettings": "Custom Formats Settings",
|
||||
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
|
||||
"CustomFormatsSpecificationFlag": "Flag",
|
||||
"CustomFormatsSpecificationLanguage": "Language",
|
||||
"CustomFormatsSpecificationMaximumSize": "Maximum Size",
|
||||
"CustomFormatsSpecificationMaximumSizeHelpText": "Release must be less than or equal to this size",
|
||||
|
@ -916,6 +918,7 @@
|
|||
"Indexer": "Indexer",
|
||||
"IndexerDownloadClientHealthCheckMessage": "Indexers with invalid download clients: {indexerNames}.",
|
||||
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
|
||||
"IndexerFlags": "Indexer Flags",
|
||||
"IndexerHDBitsSettingsCategories": "Categories",
|
||||
"IndexerHDBitsSettingsCategoriesHelpText": "If unspecified, all options are used.",
|
||||
"IndexerHDBitsSettingsCodecs": "Codecs",
|
||||
|
@ -1749,6 +1752,7 @@
|
|||
"SelectEpisodesModalTitle": "{modalTitle} - Select Episode(s)",
|
||||
"SelectFolder": "Select Folder",
|
||||
"SelectFolderModalTitle": "{modalTitle} - Select Folder",
|
||||
"SelectIndexerFlags": "Select Indexer Flags",
|
||||
"SelectLanguage": "Select Language",
|
||||
"SelectLanguageModalTitle": "{modalTitle} - Select Language",
|
||||
"SelectLanguages": "Select Languages",
|
||||
|
@ -1790,6 +1794,8 @@
|
|||
"SeriesType": "Series Type",
|
||||
"SeriesTypes": "Series Types",
|
||||
"SeriesTypesHelpText": "Series type is used for renaming, parsing and searching",
|
||||
"SetIndexerFlags": "Set Indexer Flags",
|
||||
"SetIndexerFlagsModalTitle": "{modalTitle} - Set Indexer Flags",
|
||||
"SetPermissions": "Set Permissions",
|
||||
"SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?",
|
||||
"SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.",
|
||||
|
|
|
@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions;
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
|
@ -21,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
public string SceneName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public LazyLoaded<List<Episode>> Episodes { get; set; }
|
||||
public LazyLoaded<Series> Series { get; set; }
|
||||
|
|
|
@ -7,6 +7,7 @@ using NzbDrone.Common.Disk;
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Extras;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles.Commands;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
@ -28,6 +29,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||
private readonly IExtraService _extraService;
|
||||
private readonly IExistingExtraFiles _existingExtraFiles;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly Logger _logger;
|
||||
|
@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||
IExtraService extraService,
|
||||
IExistingExtraFiles existingExtraFiles,
|
||||
IDiskProvider diskProvider,
|
||||
IHistoryService historyService,
|
||||
IEventAggregator eventAggregator,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
|
@ -46,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||
_extraService = extraService;
|
||||
_existingExtraFiles = existingExtraFiles;
|
||||
_diskProvider = diskProvider;
|
||||
_historyService = historyService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
|
@ -93,6 +97,22 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||
episodeFile.ReleaseGroup = localEpisode.ReleaseGroup;
|
||||
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;
|
||||
switch (importMode)
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
|
||||
public bool Equals(ManualImportFile other)
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
public string DownloadId { get; set; }
|
||||
public List<CustomFormat> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
|
||||
public ManualImportItem()
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
{
|
||||
List<ManualImportItem> GetMediaFiles(int seriesId, int? seasonNumber);
|
||||
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
|
||||
|
@ -139,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
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 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.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode);
|
||||
localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0;
|
||||
localEpisode.IndexerFlags = (IndexerFlags)indexerFlags;
|
||||
|
||||
return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null);
|
||||
}
|
||||
|
@ -197,7 +198,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
Size = _diskProvider.GetFileSize(path),
|
||||
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
|
||||
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);
|
||||
|
@ -422,6 +424,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
item.Languages = decision.LocalEpisode.Languages;
|
||||
item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path);
|
||||
item.Rejections = decision.Rejections;
|
||||
item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
@ -440,6 +443,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
item.ReleaseGroup = episodeFile.ReleaseGroup;
|
||||
item.Quality = episodeFile.Quality;
|
||||
item.Languages = episodeFile.Languages;
|
||||
item.IndexerFlags = (int)episodeFile.IndexerFlags;
|
||||
item.Size = _diskProvider.GetFileSize(item.Path);
|
||||
item.Rejections = Enumerable.Empty<Rejection>();
|
||||
item.EpisodeFileId = episodeFile.Id;
|
||||
|
@ -476,6 +480,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
ReleaseGroup = file.ReleaseGroup,
|
||||
Quality = file.Quality,
|
||||
Languages = file.Languages,
|
||||
IndexerFlags = (IndexerFlags)file.IndexerFlags,
|
||||
Series = series,
|
||||
Size = 0
|
||||
};
|
||||
|
@ -504,6 +509,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
localEpisode.ReleaseGroup = file.ReleaseGroup;
|
||||
localEpisode.Quality = file.Quality;
|
||||
localEpisode.Languages = file.Languages;
|
||||
localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags;
|
||||
|
||||
// TODO: Cleanup non-tracked downloads
|
||||
|
||||
|
@ -520,10 +526,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||
imported.Add(importResult);
|
||||
|
||||
importedTrackedDownload.Add(new ManuallyImportedFile
|
||||
{
|
||||
TrackedDownload = trackedDownload,
|
||||
ImportResult = importResult
|
||||
});
|
||||
{
|
||||
TrackedDownload = trackedDownload,
|
||||
ImportResult = importResult
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
|||
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_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_Type", message.DownloadClientType ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty);
|
||||
|
|
|
@ -31,6 +31,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public List<DeletedEpisodeFile> OldFiles { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public bool ExistingFile { get; set; }
|
||||
public bool SceneSource { get; set; }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
@ -39,6 +39,9 @@ namespace NzbDrone.Core.Parser.Model
|
|||
|
||||
public List<Language> Languages { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IndexerFlags IndexerFlags { get; set; }
|
||||
|
||||
// Used to track pending releases that are being reprocessed
|
||||
[JsonIgnore]
|
||||
public PendingReleaseReason? PendingReleaseReason { get; set; }
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles;
|
|||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Http;
|
||||
|
@ -203,6 +204,11 @@ namespace Sonarr.Api.V3.EpisodeFiles
|
|||
{
|
||||
episodeFile.ReleaseGroup = resourceEpisodeFile.ReleaseGroup;
|
||||
}
|
||||
|
||||
if (resourceEpisodeFile.IndexerFlags.HasValue)
|
||||
{
|
||||
episodeFile.IndexerFlags = (IndexerFlags)resourceEpisodeFile.IndexerFlags;
|
||||
}
|
||||
}
|
||||
|
||||
_mediaFileService.Update(episodeFiles);
|
||||
|
|
|
@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.EpisodeFiles
|
|||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public int? IndexerFlags { get; set; }
|
||||
public MediaInfoResource MediaInfo { get; set; }
|
||||
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
|
@ -88,7 +89,8 @@ namespace Sonarr.Api.V3.EpisodeFiles
|
|||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality),
|
||||
CustomFormats = customFormats.ToResource(false),
|
||||
CustomFormatScore = customFormatScore
|
||||
CustomFormatScore = customFormatScore,
|
||||
IndexerFlags = (int)model.IndexerFlags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -65,6 +65,7 @@ namespace Sonarr.Api.V3.Indexers
|
|||
public int? Seeders { get; set; }
|
||||
public int? Leechers { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
|
||||
public bool IsDaily { get; set; }
|
||||
public bool IsAbsoluteNumbering { get; set; }
|
||||
|
@ -100,6 +101,7 @@ namespace Sonarr.Api.V3.Indexers
|
|||
var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo;
|
||||
var remoteEpisode = model.RemoteEpisode;
|
||||
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?)
|
||||
return new ReleaseResource
|
||||
|
@ -152,6 +154,7 @@ namespace Sonarr.Api.V3.Indexers
|
|||
Seeders = torrentInfo.Seeders,
|
||||
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
|
||||
Protocol = releaseInfo.DownloadProtocol,
|
||||
IndexerFlags = (int)indexerFlags,
|
||||
|
||||
IsDaily = parsedEpisodeInfo.IsDaily,
|
||||
IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering,
|
||||
|
|
|
@ -39,10 +39,11 @@ namespace Sonarr.Api.V3.ManualImport
|
|||
{
|
||||
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.Episodes = processedItem.Episodes.ToResource();
|
||||
item.IndexerFlags = processedItem.IndexerFlags;
|
||||
item.Rejections = processedItem.Rejections;
|
||||
item.CustomFormats = processedItem.CustomFormats.ToResource(false);
|
||||
item.CustomFormatScore = processedItem.CustomFormatScore;
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.ManualImport
|
|||
public string DownloadId { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ namespace Sonarr.Api.V3.ManualImport
|
|||
public string DownloadId { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public int IndexerFlags { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
}
|
||||
|
||||
|
@ -65,6 +66,7 @@ namespace Sonarr.Api.V3.ManualImport
|
|||
|
||||
// QualityWeight
|
||||
DownloadId = model.DownloadId,
|
||||
IndexerFlags = model.IndexerFlags,
|
||||
Rejections = model.Rejections
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue