1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

New: Manually import multiple items at the same time from Activity: Queue

This commit is contained in:
Mark McDowall
2025-12-22 11:13:23 -08:00
parent ec44e1c513
commit ce8a5d8a6b
9 changed files with 85 additions and 22 deletions
+40 -2
View File
@@ -26,6 +26,7 @@ import useEpisodes from 'Episode/useEpisodes';
import { useCustomFiltersList } from 'Filters/useCustomFilters'; import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections'; import { SortDirection } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import { CheckInputChanged } from 'typings/inputs'; import { CheckInputChanged } from 'typings/inputs';
import QueueModel from 'typings/Queue'; import QueueModel from 'typings/Queue';
import { TableOptionsChangePayload } from 'typings/Table'; import { TableOptionsChangePayload } from 'typings/Table';
@@ -109,6 +110,9 @@ function QueueContent() {
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false); useState(false);
const [isInteractiveImportDownloadIds, setIsInteractiveImportDownloadIds] =
useState<string[]>(() => []);
const isRefreshing = const isRefreshing =
isLoading || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting; isLoading || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
@@ -156,12 +160,30 @@ function QueueContent() {
shouldBlockRefresh.current = false; shouldBlockRefresh.current = false;
removeQueueItems({ ids: selectedIds }); removeQueueItems({ ids: selectedIds });
setIsConfirmRemoveModalOpen(false); setIsConfirmRemoveModalOpen(false);
}, [selectedIds, setIsConfirmRemoveModalOpen, removeQueueItems]); }, [selectedIds, removeQueueItems]);
const handleConfirmRemoveModalClose = useCallback(() => { const handleConfirmRemoveModalClose = useCallback(() => {
shouldBlockRefresh.current = false; shouldBlockRefresh.current = false;
setIsConfirmRemoveModalOpen(false); setIsConfirmRemoveModalOpen(false);
}, [setIsConfirmRemoveModalOpen]); }, []);
const handleImportSelectedPress = useCallback(() => {
shouldBlockRefresh.current = true;
setIsInteractiveImportDownloadIds(
selectedIds
.map((id) => {
const item = records.find((i) => i.id === id);
return item?.downloadId;
})
.filter((id): id is string => !!id)
);
}, [records, selectedIds]);
const handleImportSelectedModalClose = useCallback(() => {
shouldBlockRefresh.current = false;
setIsInteractiveImportDownloadIds([]);
}, []);
const handleFilterSelect = useCallback( const handleFilterSelect = useCallback(
(selectedFilterKey: string | number) => { (selectedFilterKey: string | number) => {
@@ -292,6 +314,15 @@ function QueueContent() {
isSpinning={isRemoving} isSpinning={isRemoving}
onPress={handleRemoveSelectedPress} onPress={handleRemoveSelectedPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton
label={translate('ImportSelected')}
iconName={icons.INTERACTIVE}
isDisabled={disableSelectedActions}
onPress={handleImportSelectedPress}
/>
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}> <PageToolbarSection alignContent={align.RIGHT}>
@@ -358,6 +389,13 @@ function QueueContent() {
onRemovePress={handleRemoveSelectedConfirmed} onRemovePress={handleRemoveSelectedConfirmed}
onModalClose={handleConfirmRemoveModalClose} onModalClose={handleConfirmRemoveModalClose}
/> />
<InteractiveImportModal
isOpen={isInteractiveImportDownloadIds.length > 0}
downloadIds={isInteractiveImportDownloadIds}
title={translate('InteractiveImportMultipleQueueItems')}
onModalClose={handleImportSelectedModalClose}
/>
</PageContent> </PageContent>
); );
} }
+2 -2
View File
@@ -45,7 +45,7 @@ interface QueueRowProps {
id: number; id: number;
seriesId?: number; seriesId?: number;
episodeIds: number[]; episodeIds: number[];
downloadId?: string; downloadId: string;
title: string; title: string;
status: string; status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus; trackedDownloadStatus?: QueueTrackedDownloadStatus;
@@ -399,7 +399,7 @@ function QueueRow(props: QueueRowProps) {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
downloadId={downloadId} downloadIds={[downloadId]}
title={title} title={title}
onModalClose={handleInteractiveImportModalClose} onModalClose={handleInteractiveImportModalClose}
/> />
@@ -202,7 +202,7 @@ function isSameEpisodeFile(
const filterExistingFilesStore = create<boolean>(() => false); const filterExistingFilesStore = create<boolean>(() => false);
export interface InteractiveImportModalContentProps { export interface InteractiveImportModalContentProps {
downloadId?: string; downloadIds?: string[];
seriesId?: number; seriesId?: number;
seasonNumber?: number; seasonNumber?: number;
showSeries?: boolean; showSeries?: boolean;
@@ -224,7 +224,7 @@ function InteractiveImportModalContentInner(
props: InteractiveImportModalContentProps props: InteractiveImportModalContentProps
) { ) {
const { const {
downloadId, downloadIds,
seriesId, seriesId,
seasonNumber, seasonNumber,
allowSeriesChange = true, allowSeriesChange = true,
@@ -252,7 +252,7 @@ function InteractiveImportModalContentInner(
data, data,
originalItems, originalItems,
} = useInteractiveImport({ } = useInteractiveImport({
downloadId, downloadIds,
seriesId, seriesId,
seasonNumber, seasonNumber,
folder, folder,
@@ -484,7 +484,8 @@ function InteractiveImportModalContentInner(
}, [setIsConfirmDeleteModalOpen]); }, [setIsConfirmDeleteModalOpen]);
const handleImportSelectedPress = useCallback(() => { const handleImportSelectedPress = useCallback(() => {
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode; const finalImportMode =
downloadIds || !showImportMode ? 'auto' : importMode;
const existingFiles: Partial<EpisodeFile>[] = []; const existingFiles: Partial<EpisodeFile>[] = [];
const files: InteractiveImportCommandOptions[] = []; const files: InteractiveImportCommandOptions[] = [];
@@ -502,6 +503,7 @@ function InteractiveImportModalContentInner(
if (isSelected) { if (isSelected) {
const { const {
downloadId,
series, series,
seasonNumber, seasonNumber,
episodes, episodes,
@@ -605,7 +607,7 @@ function InteractiveImportModalContentInner(
onModalClose(); onModalClose();
} }
}, [ }, [
downloadId, downloadIds,
showImportMode, showImportMode,
importMode, importMode,
items, items,
@@ -921,7 +923,7 @@ function InteractiveImportModalContentInner(
</SpinnerButton> </SpinnerButton>
) : null} ) : null}
{!downloadId && showImportMode ? ( {!downloadIds && showImportMode ? (
<SelectInput <SelectInput
className={styles.importMode} className={styles.importMode}
name="importMode" name="importMode"
@@ -1046,9 +1048,9 @@ function InteractiveImportModalContent(
) { ) {
const filterExistingFiles = filterExistingFilesStore((state) => state); const filterExistingFiles = filterExistingFilesStore((state) => state);
const { downloadId, seriesId, seasonNumber, folder } = props; const { downloadIds, seriesId, seasonNumber, folder } = props;
const { data } = useInteractiveImport({ const { data } = useInteractiveImport({
downloadId, downloadIds,
seriesId, seriesId,
seasonNumber, seasonNumber,
folder, folder,
@@ -12,7 +12,7 @@ interface InteractiveImportModalProps
extends Omit<InteractiveImportModalContentProps, 'modalTitle'> { extends Omit<InteractiveImportModalContentProps, 'modalTitle'> {
isOpen: boolean; isOpen: boolean;
folder?: string; folder?: string;
downloadId?: string; downloadIds?: string[];
modalTitle?: string; modalTitle?: string;
onModalClose(): void; onModalClose(): void;
} }
@@ -21,7 +21,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
const { const {
isOpen, isOpen,
folder, folder,
downloadId, downloadIds,
modalTitle = translate('ManualImport'), modalTitle = translate('ManualImport'),
onModalClose, onModalClose,
...otherProps ...otherProps
@@ -54,11 +54,11 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
{folderPath || downloadId ? ( {folderPath || downloadIds ? (
<InteractiveImportModalContent <InteractiveImportModalContent
{...otherProps} {...otherProps}
folder={folderPath} folder={folderPath}
downloadId={downloadId} downloadIds={downloadIds}
modalTitle={modalTitle} modalTitle={modalTitle}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@@ -13,7 +13,7 @@ import ReleaseType from './ReleaseType';
const DEFAULT_ITEMS: InteractiveImport[] = []; const DEFAULT_ITEMS: InteractiveImport[] = [];
interface InteractiveImportParams { interface InteractiveImportParams {
downloadId?: string; downloadIds?: string[];
seriesId?: number; seriesId?: number;
seasonNumber?: number; seasonNumber?: number;
folder?: string; folder?: string;
@@ -7,6 +7,7 @@ export interface QueryParams {
| boolean | boolean
| PropertyFilter[] | PropertyFilter[]
| number[] | number[]
| string[]
| undefined; | undefined;
} }
@@ -976,6 +976,7 @@
"ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling", "ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling",
"ImportScriptPath": "Import Script Path", "ImportScriptPath": "Import Script Path",
"ImportScriptPathHelpText": "The path to the script to use for importing", "ImportScriptPathHelpText": "The path to the script to use for importing",
"ImportSelected": "Import Selected",
"ImportSeries": "Import Series", "ImportSeries": "Import Series",
"ImportUsingScript": "Import Using Script", "ImportUsingScript": "Import Using Script",
"ImportUsingScriptHelpText": "Copy files for importing using a script (ex. for transcoding)", "ImportUsingScriptHelpText": "Copy files for importing using a script (ex. for transcoding)",
@@ -1086,6 +1087,7 @@
"InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name",
"InteractiveImport": "Interactive Import", "InteractiveImport": "Interactive Import",
"InteractiveImportLoadError": "Unable to load manual import items", "InteractiveImportLoadError": "Unable to load manual import items",
"InteractiveImportMultipleQueueItems": "Multiple Queue Items",
"InteractiveImportNoEpisode": "One or more episodes must be chosen for each selected file", "InteractiveImportNoEpisode": "One or more episodes must be chosen for each selected file",
"InteractiveImportNoFilesFound": "No video files were found in the selected folder", "InteractiveImportNoFilesFound": "No video files were found in the selected folder",
"InteractiveImportNoImportMode": "An import mode must be selected", "InteractiveImportNoImportMode": "An import mode must be selected",
@@ -20,14 +20,34 @@ public class ManualImportController : Controller
[HttpGet] [HttpGet]
[Produces("application/json")] [Produces("application/json")]
public List<ManualImportResource> GetMediaFiles(string? folder, string? downloadId, int? seriesId, int? seasonNumber, bool filterExistingFiles = true) public List<ManualImportResource> GetMediaFiles(string? folder, [FromQuery] string[]? downloadIds, int? seriesId, int? seasonNumber, bool filterExistingFiles = true)
{ {
if (seriesId.HasValue && downloadId.IsNullOrWhiteSpace()) if (seriesId.HasValue && downloadIds == null)
{ {
return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber).ToResource().Select(AddQualityWeight).ToList(); return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber)
.ToResource()
.Select(AddQualityWeight)
.ToList();
} }
return _manualImportService.GetMediaFiles(folder, downloadId, seriesId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); if (downloadIds != null && downloadIds.Any())
{
var files = new List<ManualImportItem>();
foreach (var downloadId in downloadIds.Distinct())
{
files.AddRange(_manualImportService.GetMediaFiles(null, downloadId, seriesId, filterExistingFiles));
}
return files.ToResource()
.Select(AddQualityWeight)
.ToList();
}
return _manualImportService.GetMediaFiles(folder, null, seriesId, filterExistingFiles)
.ToResource()
.Select(AddQualityWeight)
.ToList();
} }
[HttpPost] [HttpPost]
@@ -52,7 +52,7 @@ public static class ManualImportResourceMapper
Size = model.Size, Size = model.Size,
Series = model.Series?.ToResource(), Series = model.Series?.ToResource(),
SeasonNumber = model.SeasonNumber, SeasonNumber = model.SeasonNumber,
Episodes = model.Episodes.ToResource(), Episodes = model.Episodes?.ToResource() ?? [],
EpisodeFileId = model.EpisodeFileId, EpisodeFileId = model.EpisodeFileId,
ReleaseGroup = model.ReleaseGroup, ReleaseGroup = model.ReleaseGroup,
Quality = model.Quality, Quality = model.Quality,