From 74cdf01e49565c5bc63b0e90890a9e170caa3db4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 5 Apr 2024 23:11:17 -0700 Subject: [PATCH] New: Set 'Release Type' during Manual Import Closes #6681 --- frontend/src/Episode/getReleaseTypeName.ts | 17 ++++ frontend/src/EpisodeFile/EpisodeFile.ts | 2 + .../InteractiveImportModalContent.tsx | 41 +++++++- .../Interactive/InteractiveImportRow.tsx | 44 ++++++++- .../InteractiveImport/InteractiveImport.ts | 1 + .../ReleaseType/SelectReleaseTypeModal.tsx | 30 ++++++ .../SelectReleaseTypeModalContent.tsx | 99 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../ManualImport/ManualImportController.cs | 1 + 10 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 frontend/src/Episode/getReleaseTypeName.ts create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx diff --git a/frontend/src/Episode/getReleaseTypeName.ts b/frontend/src/Episode/getReleaseTypeName.ts new file mode 100644 index 000000000..a2bb1af5b --- /dev/null +++ b/frontend/src/Episode/getReleaseTypeName.ts @@ -0,0 +1,17 @@ +import ReleaseType from 'InteractiveImport/ReleaseType'; +import translate from 'Utilities/String/translate'; + +export default function getReleaseTypeName( + releaseType?: ReleaseType +): string | null { + switch (releaseType) { + case 'singleEpisode': + return translate('SingleEpisode'); + case 'multiEpisode': + return translate('MultiEpisode'); + case 'seasonPack': + return translate('SeasonPack'); + default: + return translate('Unknown'); + } +} diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts index 53dd53750..da362db82 100644 --- a/frontend/src/EpisodeFile/EpisodeFile.ts +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; @@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase { quality: QualityModel; customFormats: CustomFormat[]; indexerFlags: number; + releaseType: ReleaseType; mediaInfo: MediaInfo; qualityCutoffNotMet: boolean; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index e421db602..dbcd10613 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -36,6 +36,7 @@ import InteractiveImport, { import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; +import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; import Language from 'Language/Language'; @@ -73,7 +74,8 @@ type SelectType = | 'releaseGroup' | 'quality' | 'language' - | 'indexerFlags'; + | 'indexerFlags' + | 'releaseType'; type FilterExistingFiles = 'all' | 'new'; @@ -128,6 +130,12 @@ const COLUMNS = [ isSortable: true, isVisible: true, }, + { + name: 'releaseType', + label: () => translate('ReleaseType'), + isSortable: true, + isVisible: true, + }, { name: 'customFormats', label: React.createElement(Icon, { @@ -369,6 +377,10 @@ function InteractiveImportModalContent( key: 'indexerFlags', value: translate('SelectIndexerFlags'), }, + { + key: 'releaseType', + value: translate('SelectReleaseType'), + }, ]; if (allowSeriesChange) { @@ -511,6 +523,7 @@ function InteractiveImportModalContent( languages, indexerFlags, episodeFileId, + releaseType, } = item; if (!series) { @@ -560,6 +573,7 @@ function InteractiveImportModalContent( quality, languages, indexerFlags, + releaseType, }); return; @@ -575,6 +589,7 @@ function InteractiveImportModalContent( quality, languages, indexerFlags, + releaseType, downloadId, episodeFileId, }); @@ -787,6 +802,22 @@ function InteractiveImportModalContent( [selectedIds, dispatch] ); + const onReleaseTypeSelect = useCallback( + (releaseType: string) => { + dispatch( + updateInteractiveImportItems({ + ids: selectedIds, + releaseType, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: selectedIds })); + + setSelectModalOpen(null); + }, + [selectedIds, dispatch] + ); + const orderedSelectedIds = items.reduce((acc: number[], file) => { if (selectedIds.includes(file.id)) { acc.push(file.id); @@ -1000,6 +1031,14 @@ function InteractiveImportModalContent( onModalClose={onSelectModalClose} /> + + { + setSelectModalOpen('releaseType'); + }, [setSelectModalOpen]); + + const onReleaseTypeSelect = useCallback( + (releaseType: ReleaseType) => { + dispatch( + updateInteractiveImportItem({ + id, + releaseType, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: [id] })); + + setSelectModalOpen(null); + selectRowAfterChange(); + }, + [id, dispatch, setSelectModalOpen, selectRowAfterChange] + ); + const onSelectIndexerFlagsPress = useCallback(() => { setSelectModalOpen('indexerFlags'); }, [setSelectModalOpen]); @@ -461,6 +488,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { {formatBytes(size)} + + {getReleaseTypeName(releaseType)} + + {customFormats?.length ? ( + + + + + ); +} + +export default SelectReleaseTypeModal; diff --git a/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx new file mode 100644 index 000000000..610811195 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx @@ -0,0 +1,99 @@ +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 } from 'Helpers/Props'; +import ReleaseType from 'InteractiveImport/ReleaseType'; +import translate from 'Utilities/String/translate'; + +const options = [ + { + key: 'unknown', + get value() { + return translate('Unknown'); + }, + }, + { + key: 'singleEpisode', + get value() { + return translate('SingleEpisode'); + }, + }, + { + key: 'multiEpisode', + get value() { + return translate('MultiEpisode'); + }, + }, + { + key: 'seasonPack', + get value() { + return translate('SeasonPack'); + }, + }, +]; + +interface SelectReleaseTypeModalContentProps { + releaseType: ReleaseType; + modalTitle: string; + onReleaseTypeSelect(releaseType: ReleaseType): void; + onModalClose(): void; +} + +function SelectReleaseTypeModalContent( + props: SelectReleaseTypeModalContentProps +) { + const { modalTitle, onReleaseTypeSelect, onModalClose } = props; + const [releaseType, setReleaseType] = useState(props.releaseType); + + const handleReleaseTypeChange = useCallback( + ({ value }: { value: string }) => { + setReleaseType(value as ReleaseType); + }, + [setReleaseType] + ); + + const handleReleaseTypeSelect = useCallback(() => { + onReleaseTypeSelect(releaseType); + }, [releaseType, onReleaseTypeSelect]); + + return ( + + + {modalTitle} - {translate('SelectReleaseType')} + + + +
+ + {translate('ReleaseType')} + + + +
+
+ + + + + + +
+ ); +} + +export default SelectReleaseTypeModalContent; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b41d10d93..1e47f15ea 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -218,6 +218,7 @@ "ClickToChangeLanguage": "Click to change language", "ClickToChangeQuality": "Click to change quality", "ClickToChangeReleaseGroup": "Click to change release group", + "ClickToChangeReleaseType": "Click to change release type", "ClickToChangeSeason": "Click to change season", "ClickToChangeSeries": "Click to change series", "ClientPriority": "Client Priority", @@ -1777,6 +1778,7 @@ "SelectLanguages": "Select Languages", "SelectQuality": "Select Quality", "SelectReleaseGroup": "Select Release Group", + "SelectReleaseType": "Select Release Type", "SelectSeason": "Select Season", "SelectSeasonModalTitle": "{modalTitle} - Select Season", "SelectSeries": "Select Series", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 4cdf288fd..39c3c849f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -123,6 +123,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport else { episodeFile.IndexerFlags = localEpisode.IndexerFlags; + episodeFile.ReleaseType = localEpisode.ReleaseType; } // Fall back to parsed information if history is unavailable or missing diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index eb6787c5b..46ab91a95 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -43,6 +43,7 @@ namespace Sonarr.Api.V3.ManualImport item.SeasonNumber = processedItem.SeasonNumber; item.Episodes = processedItem.Episodes.ToResource(); + item.ReleaseType = processedItem.ReleaseType; item.IndexerFlags = processedItem.IndexerFlags; item.Rejections = processedItem.Rejections; item.CustomFormats = processedItem.CustomFormats.ToResource(false);