diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 6d52ef135..874e42356 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -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), diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx index fd3520ceb..8dbd27a70 100644 --- a/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx +++ b/frontend/src/Components/Form/IndexerFlagsSelectInput.tsx @@ -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; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx new file mode 100644 index 000000000..9136554cc --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModal.tsx @@ -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 ( + + + + ); +} + +export default SelectIndexerFlagsModal; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css new file mode 100644 index 000000000..72dfb1cb6 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css @@ -0,0 +1,7 @@ +.modalBody { + composes: modalBody from '~Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts new file mode 100644 index 000000000..3fc49a060 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'modalBody': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx new file mode 100644 index 000000000..f36f46602 --- /dev/null +++ b/frontend/src/InteractiveImport/IndexerFlags/SelectIndexerFlagsModalContent.tsx @@ -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 ( + + + {translate('SetIndexerFlagsModalTitle', { modalTitle })} + + + +
+ + {translate('IndexerFlags')} + + + +
+
+ + + + + + +
+ ); +} + +export default SelectIndexerFlagsModalContent; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 0732171c3..0eea66249 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -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} /> + + 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( 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 ( @@ -311,6 +346,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { ) : null} + {isIndexerFlagsColumnVisible ? ( + + {showIndexerFlagsPlaceholder ? ( + + ) : ( + <> + {indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={} + position={tooltipPositions.LEFT} + /> + ) : null} + + )} + + ) : null} + {rejections.length ? ( + + ); } diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 000ca1a2d..4e876f852 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -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; } diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index 97ac6807a..2268139a7 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -39,8 +39,7 @@ } .rejected, -.indexerFlags, -.download { +.indexerFlags { composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 50px; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx index 8489ac63d..41b009de3 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.tsx @@ -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={} - position={tooltipPositions.TOP} + position={tooltipPositions.LEFT} /> - {indexerFlags.length ? ( + {indexerFlags ? ( } title={translate('IndexerFlags')} - body={ -
    - {indexerFlags.map((flag, index) => { - return
  • {flag}
  • ; - })} -
- } + body={} position={tooltipPositions.LEFT} /> ) : null} diff --git a/frontend/src/Movie/IndexerFlags.tsx b/frontend/src/Movie/IndexerFlags.tsx new file mode 100644 index 000000000..74e2e033c --- /dev/null +++ b/frontend/src/Movie/IndexerFlags.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector'; + +interface IndexerFlagsProps { + indexerFlags: number; +} + +function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) { + const allIndexerFlags = useSelector(createIndexerFlagsSelector); + + const flags = allIndexerFlags.items.filter( + // eslint-disable-next-line no-bitwise + (item) => (indexerFlags & item.id) === item.id + ); + + return flags.length ? ( +
    + {flags.map((flag, index) => { + return
  • {flag.name}
  • ; + })} +
+ ) : null; +} + +export default IndexerFlags; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.css b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css index a68d41a6f..ae729b9f2 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorRow.css +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css @@ -57,3 +57,9 @@ width: 55px; } + +.indexerFlags { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.css.d.ts b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css.d.ts index d7eafc0fd..a15d0ed8c 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorRow.css.d.ts +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css.d.ts @@ -9,6 +9,7 @@ interface CssExports { 'dateAdded': string; 'download': string; 'formats': string; + 'indexerFlags': string; 'language': string; 'languages': string; 'quality': string; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js index f7de755ad..5c0604efa 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js @@ -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={} - position={tooltipPositions.TOP} + position={tooltipPositions.LEFT} />
); } + if (name === 'indexerFlags') { + return ( + + {indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={} + position={tooltipPositions.LEFT} + /> + ) : null} + + ); + } + if (name === 'languages') { return ( translate('IndexerFlags'), + label: React.createElement(Icon, { + name: icons.FLAG, + title: () => translate('IndexerFlags') + }), + isVisible: false + }, { name: 'dateAdded', label: () => translate('Added'), diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index 6257ae0b8..9d1f6f1a4 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -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 ParseCustomFormat(LocalMovie localMovie) { - var episodeInfo = new ParsedMovieInfo + var movieInfo = new ParsedMovieInfo { MovieTitles = new List() { 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) }; diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs index ccd0abe1c..56f73f8b9 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -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() diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 3c395484d..05b3e25c0 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -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); diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs index 7adda8192..1b4bbbae7 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListParser.cs @@ -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, 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, DateTime?> CookiesUpdater { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index b14fac3aa..91d35114b 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -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, 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, DateTime?> CookiesUpdater { get; set; } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 92411ea1c..4b591948e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -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.", diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs index 7e885a5cb..ede888f8c 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFile.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -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 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 Languages { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs index 6c151a563..56016dda6 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs @@ -108,6 +108,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport movieFile.IndexerFlags = flags; } } + else + { + movieFile.IndexerFlags = localMovie.IndexerFlags; + } bool copyOnly; switch (importMode) diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportFile.cs index 87c500de1..443b24cc6 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportFile.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual public QualityModel Quality { get; set; } public List Languages { get; set; } public string ReleaseGroup { get; set; } + public int IndexerFlags { get; set; } public string DownloadId { get; set; } public int MovieId { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportItem.cs index 516c0dd5e..a809868fb 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportItem.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual public string DownloadId { get; set; } public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } public Movie Movie { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs index b93b5bd24..3a257aa8a 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Manual/ManualImportService.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual public interface IManualImportService { List GetMediaFiles(string path, string downloadId, int? movieId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List languages); + ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List languages, int indexerFlags); } public class ManualImportService : IExecute, 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 languages) + public ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List 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); diff --git a/src/NzbDrone.Core/Parser/Model/LocalMovie.cs b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs index 392833a6a..3d89d349e 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalMovie.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.Parser.Model public List OldFiles { get; set; } public QualityModel Quality { get; set; } public List Languages { get; set; } + public IndexerFlags IndexerFlags { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } public bool SceneSource { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 788ce9a10..d21188c85 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -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 } diff --git a/src/Radarr.Api.V3/Indexers/ReleaseResource.cs b/src/Radarr.Api.V3/Indexers/ReleaseResource.cs index 89d155570..fb2807424 100644 --- a/src/Radarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Radarr.Api.V3/Indexers/ReleaseResource.cs @@ -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 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 }; } diff --git a/src/Radarr.Api.V3/ManualImport/ManualImportController.cs b/src/Radarr.Api.V3/ManualImport/ManualImportController.cs index 3a270f35a..40f877d7f 100644 --- a/src/Radarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Radarr.Api.V3/ManualImport/ManualImportController.cs @@ -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; diff --git a/src/Radarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Radarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 5733f145e..659fea56f 100644 --- a/src/Radarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Radarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -19,6 +19,7 @@ namespace Radarr.Api.V3.ManualImport public string DownloadId { get; set; } public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } } } diff --git a/src/Radarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Radarr.Api.V3/ManualImport/ManualImportResource.cs index c75d3ae9b..b89561403 100644 --- a/src/Radarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Radarr.Api.V3/ManualImport/ManualImportResource.cs @@ -26,6 +26,7 @@ namespace Radarr.Api.V3.ManualImport public string DownloadId { get; set; } public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } + public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } } @@ -58,6 +59,7 @@ namespace Radarr.Api.V3.ManualImport // QualityWeight DownloadId = model.DownloadId, + IndexerFlags = model.IndexerFlags, Rejections = model.Rejections }; } diff --git a/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs b/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs index 15b19ab7d..9d798d38d 100644 --- a/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs +++ b/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs @@ -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 Languages { get; set; } public QualityModel Quality { get; set; } public List 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 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)