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')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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);