New: Set Indexer flags in Manual Import

This commit is contained in:
Bogdan 2023-08-01 20:28:12 +03:00
parent 25ab396a2c
commit 9dd31be7b3
36 changed files with 444 additions and 97 deletions

View File

@ -282,6 +282,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),

View File

@ -4,22 +4,18 @@ import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items
.filter(
// eslint-disable-next-line no-bitwise
(item) => (selectedFlags & item.id) === item.id
)
.map(({ id }) => id);
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,
@ -33,6 +29,12 @@ const selectIndexerFlagsValues = (selectedFlags: number) =>
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;

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

@ -26,6 +26,7 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import ImportMode from 'InteractiveImport/ImportMode';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
@ -59,7 +60,13 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
type SelectType = 'select' | 'movie' | 'releaseGroup' | 'quality' | 'language';
type SelectType =
| 'select'
| 'movie'
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
type FilterExistingFiles = 'all' | 'new';
@ -113,6 +120,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, {
@ -257,8 +273,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;
}, [showMovie]);
}, [showMovie, items]);
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
@ -283,6 +309,10 @@ function InteractiveImportModalContent(
key: 'language',
value: translate('SelectLanguage'),
},
{
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
];
if (allowMovieChange) {
@ -416,7 +446,14 @@ function InteractiveImportModalContent(
const isSelected = selectedIds.indexOf(item.id) > -1;
if (isSelected) {
const { movie, releaseGroup, quality, languages, movieFileId } = item;
const {
movie,
releaseGroup,
quality,
languages,
indexerFlags,
movieFileId,
} = item;
if (!movie) {
setInteractiveImportErrorMessage(
@ -450,6 +487,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
});
return;
@ -463,6 +501,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
downloadId,
movieFileId,
});
@ -620,6 +659,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const errorMessage = getErrorMessage(
error,
translate('InteractiveImportLoadError')
@ -794,6 +849,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}

View File

@ -8,11 +8,13 @@ import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import Language from 'Language/Language';
import IndexerFlags from 'Movie/IndexerFlags';
import Movie from 'Movie/Movie';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
@ -30,7 +32,12 @@ import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
type SelectType = 'movie' | 'releaseGroup' | 'quality' | 'language';
type SelectType =
| 'movie'
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
type SelectedChangeProps = SelectStateInputProps & {
hasMovieFileId: boolean;
@ -47,6 +54,7 @@ interface InteractiveImportRowProps {
size: number;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];
columns: Column[];
movieFileId?: number;
@ -69,6 +77,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
size,
customFormats,
customFormatScore,
indexerFlags,
rejections,
isSelected,
modalTitle,
@ -84,6 +93,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
() => columns.find((c) => c.name === 'movie')?.isVisible ?? false,
[columns]
);
const isIndexerFlagsColumnVisible = useMemo(
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
@ -223,12 +236,34 @@ 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 movieTitle = movie ? movie.title : '';
const showMoviePlaceholder = isSelected && !movie;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
return (
<TableRow>
@ -311,6 +346,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
@ -361,6 +418,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
</TableRow>
);
}

View File

@ -11,6 +11,7 @@ export interface InteractiveImportCommandOptions {
releaseGroup?: string;
quality: QualityModel;
languages: Language[];
indexerFlags: number;
downloadId?: string;
movieFileId?: number;
}
@ -27,6 +28,7 @@ interface InteractiveImport extends ModelBase {
movie?: Movie;
qualityWeight: number;
customFormats: object[];
indexerFlags: number;
rejections: Rejection[];
movieFileId?: number;
}

View File

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

View File

@ -11,6 +11,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import IndexerFlags from 'Movie/IndexerFlags';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
@ -90,8 +91,8 @@ interface InteractiveSearchRowProps {
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
indexerFlags: number;
rejections: string[];
indexerFlags: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
@ -125,8 +126,8 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormatScore,
customFormats,
mappedMovieId,
indexerFlags = 0,
rejections = [],
indexerFlags = [],
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
@ -276,22 +277,16 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags.length ? (
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={
<ul>
{indexerFlags.map((flag, index) => {
return <li key={index}>{flag}</li>;
})}
</ul>
}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}

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

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

View File

@ -9,6 +9,7 @@ interface CssExports {
'dateAdded': string;
'download': string;
'formats': string;
'indexerFlags': string;
'language': string;
'languages': string;
'quality': string;

View File

@ -1,12 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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 { icons, kinds, tooltipPositions } from 'Helpers/Props';
import IndexerFlags from 'Movie/IndexerFlags';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
@ -82,6 +85,7 @@ class MovieFileEditorRow extends Component {
qualityCutoffNotMet,
customFormats,
customFormatScore,
indexerFlags,
languages,
dateAdded,
columns
@ -143,12 +147,30 @@ class MovieFileEditorRow extends Component {
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell
@ -363,6 +385,7 @@ MovieFileEditorRow.propTypes = {
releaseGroup: PropTypes.string,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
mediaInfo: PropTypes.object,
@ -372,7 +395,8 @@ MovieFileEditorRow.propTypes = {
};
MovieFileEditorRow.defaultProps = {
customFormats: []
customFormats: [],
indexerFlags: 0
};
export default MovieFileEditorRow;

View File

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

View File

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

View File

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

View File

@ -78,8 +78,8 @@ namespace NzbDrone.Core.CustomFormats
MovieInfo = movieInfo,
Movie = movie,
Size = blocklist.Size ?? 0,
IndexerFlags = blocklist.IndexerFlags,
Languages = blocklist.Languages
Languages = blocklist.Languages,
IndexerFlags = blocklist.IndexerFlags
};
return ParseCustomFormat(input);
@ -90,7 +90,7 @@ namespace NzbDrone.Core.CustomFormats
var parsed = Parser.Parser.ParseMovieTitle(history.SourceTitle);
long.TryParse(history.Data.GetValueOrDefault("size"), out var size);
Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags);
Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags);
var movieInfo = new ParsedMovieInfo
{
@ -108,8 +108,8 @@ namespace NzbDrone.Core.CustomFormats
MovieInfo = movieInfo,
Movie = movie,
Size = size,
IndexerFlags = flags,
Languages = history.Languages
Languages = history.Languages,
IndexerFlags = indexerFlags
};
return ParseCustomFormat(input);
@ -117,7 +117,7 @@ namespace NzbDrone.Core.CustomFormats
public List<CustomFormat> ParseCustomFormat(LocalMovie localMovie)
{
var episodeInfo = new ParsedMovieInfo
var movieInfo = new ParsedMovieInfo
{
MovieTitles = new List<string>() { localMovie.Movie.Title },
SimpleReleaseTitle = localMovie.SceneName.IsNotNullOrWhiteSpace() ? localMovie.SceneName.SimplifyReleaseTitle() : Path.GetFileName(localMovie.Path).SimplifyReleaseTitle(),
@ -130,10 +130,11 @@ namespace NzbDrone.Core.CustomFormats
var input = new CustomFormatInput
{
MovieInfo = episodeInfo,
MovieInfo = movieInfo,
Movie = localMovie.Movie,
Size = localMovie.Size,
Languages = localMovie.Languages,
IndexerFlags = localMovie.IndexerFlags,
Filename = Path.GetFileName(localMovie.Path)
};
@ -203,8 +204,8 @@ namespace NzbDrone.Core.CustomFormats
MovieInfo = movieInfo,
Movie = movie,
Size = movieFile.Size,
IndexerFlags = movieFile.IndexerFlags,
Languages = movieFile.Languages,
IndexerFlags = movieFile.IndexerFlags,
Filename = Path.GetFileName(movieFile.RelativePath)
};

View File

@ -15,7 +15,7 @@ namespace NzbDrone.Core.CustomFormats
{
if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue))
{
context.AddFailure(string.Format("Invalid indexer flag condition value: {0}", qualityValue));
context.AddFailure($"Invalid indexer flag condition value: {qualityValue}");
}
});
}
@ -23,17 +23,17 @@ namespace NzbDrone.Core.CustomFormats
public class IndexerFlagSpecification : CustomFormatSpecificationBase
{
private static readonly IndexerFlagSpecificationValidator Validator = new IndexerFlagSpecificationValidator();
private static readonly IndexerFlagSpecificationValidator Validator = new ();
public override int Order => 4;
public override string ImplementationName => "Indexer Flag";
[FieldDefinition(1, Label = "Flag", Type = FieldType.Select, SelectOptions = typeof(IndexerFlags))]
[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) == true;
return input.IndexerFlags.HasFlag((IndexerFlags)Value);
}
public override NzbDroneValidationResult Validate()

View File

@ -140,12 +140,11 @@ namespace NzbDrone.Core.Download.TrackedDownloads
var firstHistoryItem = historyItems.FirstOrDefault();
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == MovieHistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer");
trackedDownload.Added = grabbedEvent?.Date;
if (parsedMovieInfo == null ||
trackedDownload.RemoteMovie == null ||
trackedDownload.RemoteMovie.Movie == null)
trackedDownload.RemoteMovie?.Movie == null)
{
parsedMovieInfo = Parser.Parser.ParseMovieTitle(firstHistoryItem.SourceTitle);

View File

@ -39,18 +39,6 @@ namespace NzbDrone.Core.Indexers.FileList
{
var id = result.Id;
IndexerFlags flags = 0;
if (result.FreeLeech)
{
flags |= IndexerFlags.G_Freeleech;
}
if (result.Internal)
{
flags |= IndexerFlags.G_Internal;
}
var imdbId = 0;
if (result.ImdbId != null && result.ImdbId.Length > 2)
{
@ -68,14 +56,29 @@ namespace NzbDrone.Core.Indexers.FileList
Peers = result.Leechers + result.Seeders,
PublishDate = result.UploadDate,
ImdbId = imdbId,
IndexerFlags = flags
IndexerFlags = GetIndexerFlags(result)
});
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private static IndexerFlags GetIndexerFlags(FileListTorrent item)
{
IndexerFlags flags = 0;
if (item.FreeLeech)
{
flags |= IndexerFlags.G_Freeleech;
}
if (item.Internal)
{
flags |= IndexerFlags.G_Internal;
}
return flags;
}
private string GetDownloadUrl(string torrentId)
{
@ -95,5 +98,7 @@ namespace NzbDrone.Core.Indexers.FileList
return url.FullUri;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@ -50,19 +50,6 @@ namespace NzbDrone.Core.Indexers.HDBits
foreach (var result in queryResults)
{
var id = result.Id;
var internalRelease = result.TypeOrigin == 1;
IndexerFlags flags = 0;
if (result.FreeLeech == "yes")
{
flags |= IndexerFlags.G_Freeleech;
}
if (internalRelease)
{
flags |= IndexerFlags.G_Internal;
}
torrentInfos.Add(new HDBitsInfo
{
@ -75,16 +62,30 @@ namespace NzbDrone.Core.Indexers.HDBits
Seeders = result.Seeders,
Peers = result.Leechers + result.Seeders,
PublishDate = result.Added.ToUniversalTime(),
Internal = internalRelease,
ImdbId = result.ImdbInfo?.Id ?? 0,
IndexerFlags = flags
IndexerFlags = GetIndexerFlags(result)
});
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private static IndexerFlags GetIndexerFlags(TorrentQueryResponse item)
{
IndexerFlags flags = 0;
if (item.FreeLeech == "yes")
{
flags |= IndexerFlags.G_Freeleech;
}
if (item.TypeOrigin == 1)
{
flags |= IndexerFlags.G_Internal;
}
return flags;
}
private string GetDownloadUrl(string torrentId)
{
@ -104,5 +105,7 @@ namespace NzbDrone.Core.Indexers.HDBits
return url.FullUri;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@ -193,6 +193,7 @@
"Clear": "Clear",
"ClearBlocklist": "Clear blocklist",
"ClearBlocklistMessageText": "Are you sure you want to clear all items from the blocklist?",
"ClickToChangeIndexerFlags": "Click to change indexer flags",
"ClickToChangeLanguage": "Click to change language",
"ClickToChangeMovie": "Click to change movie",
"ClickToChangeQuality": "Click to change quality",
@ -255,6 +256,7 @@
"CustomFormatsLoadError": "Unable to load Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationRegularExpression": "Regular Expression",
"CustomFormatsSpecificationRegularExpressionHelpText": "Custom Format RegEx is Case Insensitive",
"Cutoff": "Cutoff",
@ -1483,6 +1485,7 @@
"SelectDropdown": "Select...",
"SelectFolder": "Select Folder",
"SelectFolderModalTitle": "{modalTitle} - Select Folder",
"SelectIndexerFlags": "Select Indexer Flags",
"SelectLanguage": "Select Language",
"SelectLanguageModalTitle": "{modalTitle} - Select Language",
"SelectLanguages": "Select Languages",
@ -1490,6 +1493,8 @@
"SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group",
"SendAnonymousUsageData": "Send Anonymous Usage Data",
"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.",

View File

@ -20,12 +20,12 @@ namespace NzbDrone.Core.MediaFiles
public string OriginalFilePath { get; set; }
public string SceneName { get; set; }
public string ReleaseGroup { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public IndexerFlags IndexerFlags { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public string Edition { get; set; }
public Movie Movie { get; set; }
public List<Language> Languages { get; set; }
public override string ToString()
{

View File

@ -108,6 +108,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
movieFile.IndexerFlags = flags;
}
}
else
{
movieFile.IndexerFlags = localMovie.IndexerFlags;
}
bool copyOnly;
switch (importMode)

View File

@ -13,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.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 int MovieId { get; set; }

View File

@ -20,6 +20,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.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 Movie Movie { get; set; }

View File

@ -24,7 +24,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
public interface IManualImportService
{
List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? movieId, bool filterExistingFiles);
ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages);
ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags);
}
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -97,7 +97,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
return ProcessFolder(path, path, downloadId, movieId, filterExistingFiles);
}
public ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages)
public ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages, int indexerFlags)
{
var rootFolder = Path.GetDirectoryName(path);
var movie = _movieService.GetMovie(movieId);
@ -122,9 +122,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
SceneSource = SceneSource(movie, rootFolder),
ExistingFile = movie.Path.IsParentPath(path),
Size = _diskProvider.GetFileSize(path),
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages,
Quality = (quality?.Quality ?? Quality.Unknown) == Quality.Unknown ? QualityParser.ParseQuality(path) : quality,
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
IndexerFlags = (IndexerFlags)indexerFlags
};
return MapItem(_importDecisionMaker.GetDecision(localMovie, downloadClientItem), rootFolder, downloadId, null);
@ -320,6 +321,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
item.Languages = decision.LocalMovie.Languages;
item.ReleaseGroup = decision.LocalMovie.ReleaseGroup;
item.Rejections = decision.Rejections;
item.IndexerFlags = (int)decision.LocalMovie.IndexerFlags;
return item;
}
@ -346,9 +348,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
ExistingFile = existingFile,
FileMovieInfo = fileMovieInfo,
Path = file.Path,
ReleaseGroup = file.ReleaseGroup,
Quality = file.Quality,
Languages = file.Languages,
ReleaseGroup = file.ReleaseGroup,
IndexerFlags = (IndexerFlags)file.IndexerFlags,
Movie = movie,
Size = 0
};
@ -373,9 +376,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
// Apply the user-chosen values.
localMovie.Movie = movie;
localMovie.ReleaseGroup = file.ReleaseGroup;
localMovie.Quality = file.Quality;
localMovie.Languages = file.Languages;
localMovie.ReleaseGroup = file.ReleaseGroup;
localMovie.IndexerFlags = (IndexerFlags)file.IndexerFlags;
// TODO: Cleanup non-tracked downloads
var importDecision = new ImportDecision(localMovie);

View File

@ -26,6 +26,7 @@ namespace NzbDrone.Core.Parser.Model
public List<DeletedMovieFile> 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; }

View File

@ -108,12 +108,12 @@ namespace NzbDrone.Core.Parser.Model
G_DoubleUpload = 4, // General
PTP_Golden = 8, // PTP
PTP_Approved = 16, // PTP
G_Internal = 32, // General, internal
G_Internal = 32, // General, uploader is an internal release group
[Obsolete]
AHD_Internal = 64, // AHD, internal
G_Scene = 128, // General, the torrent comes from the "scene"
G_Freeleech75 = 256, // Currently only used for AHD, signifies a torrent counts towards 75 percent of your download quota.
G_Freeleech25 = 512, // Currently only used for AHD, signifies a torrent counts towards 25 percent of your download quota.
G_Scene = 128, // General, the torrent comes from a "scene" group
G_Freeleech75 = 256, // Signifies a torrent counts towards 75 percent of your download quota.
G_Freeleech25 = 512, // Signifies a torrent counts towards 25 percent of your download quota.
[Obsolete]
AHD_UserRelease = 1024 // AHD, internal
}

View File

@ -45,7 +45,6 @@ namespace Radarr.Api.V3.Indexers
public string InfoUrl { get; set; }
public bool DownloadAllowed { get; set; }
public int ReleaseWeight { get; set; }
public IEnumerable<string> IndexerFlags { get; set; }
public string Edition { get; set; }
public string MagnetUrl { get; set; }
@ -53,6 +52,7 @@ namespace Radarr.Api.V3.Indexers
public int? Seeders { get; set; }
public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; }
public int IndexerFlags { get; set; }
// Sent when queuing an unknown release
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
@ -76,7 +76,7 @@ namespace Radarr.Api.V3.Indexers
var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo;
var remoteMovie = model.RemoteMovie;
var torrentInfo = (model.RemoteMovie.Release as TorrentInfo) ?? new TorrentInfo();
var indexerFlags = torrentInfo.IndexerFlags.ToString().Split(new string[] { ", " }, StringSplitOptions.None).Where(x => x != "0");
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
@ -118,7 +118,7 @@ namespace Radarr.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 = indexerFlags
IndexerFlags = (int)indexerFlags
};
}

View File

@ -34,9 +34,10 @@ namespace Radarr.Api.V3.ManualImport
{
foreach (var item in items)
{
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.MovieId, item.ReleaseGroup, item.Quality, item.Languages);
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.MovieId, item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags);
item.Movie = processedItem.Movie.ToResource(0);
item.IndexerFlags = processedItem.IndexerFlags;
item.Rejections = processedItem.Rejections;
item.CustomFormats = processedItem.CustomFormats.ToResource(false);
item.CustomFormatScore = processedItem.CustomFormatScore;

View File

@ -19,6 +19,7 @@ namespace Radarr.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; }
}
}

View File

@ -26,6 +26,7 @@ namespace Radarr.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; }
}
@ -58,6 +59,7 @@ namespace Radarr.Api.V3.ManualImport
// QualityWeight
DownloadId = model.DownloadId,
IndexerFlags = model.IndexerFlags,
Rejections = model.Rejections
};
}

View File

@ -19,16 +19,17 @@ namespace Radarr.Api.V3.MovieFiles
public long Size { get; set; }
public DateTime DateAdded { get; set; }
public string SceneName { get; set; }
public int IndexerFlags { get; set; }
public string ReleaseGroup { get; set; }
public string Edition { get; set; }
public List<Language> Languages { get; set; }
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 string OriginalFilePath { get; set; }
public bool QualityCutoffNotMet { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public string Edition { get; set; }
}
public static class MovieFileResourceMapper
@ -78,14 +79,14 @@ namespace Radarr.Api.V3.MovieFiles
Size = model.Size,
DateAdded = model.DateAdded,
SceneName = model.SceneName,
IndexerFlags = (int)model.IndexerFlags,
Quality = model.Quality,
Languages = model.Languages,
Edition = model.Edition,
ReleaseGroup = model.ReleaseGroup,
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
QualityCutoffNotMet = upgradableSpecification?.QualityCutoffNotMet(movie.QualityProfile, model.Quality) ?? false,
OriginalFilePath = model.OriginalFilePath
OriginalFilePath = model.OriginalFilePath,
IndexerFlags = (int)model.IndexerFlags
};
if (formatCalculationService != null)