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

View file

@ -26,6 +26,7 @@ import useEpisodes from 'Episode/useEpisodes';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import { CheckInputChanged } from 'typings/inputs';
import QueueModel from 'typings/Queue';
import { TableOptionsChangePayload } from 'typings/Table';
@ -109,6 +110,9 @@ function QueueContent() {
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
const [isInteractiveImportDownloadIds, setIsInteractiveImportDownloadIds] =
useState<string[]>(() => []);
const isRefreshing =
isLoading || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
@ -156,12 +160,30 @@ function QueueContent() {
shouldBlockRefresh.current = false;
removeQueueItems({ ids: selectedIds });
setIsConfirmRemoveModalOpen(false);
}, [selectedIds, setIsConfirmRemoveModalOpen, removeQueueItems]);
}, [selectedIds, removeQueueItems]);
const handleConfirmRemoveModalClose = useCallback(() => {
shouldBlockRefresh.current = 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(
(selectedFilterKey: string | number) => {
@ -292,6 +314,15 @@ function QueueContent() {
isSpinning={isRemoving}
onPress={handleRemoveSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('ImportSelected')}
iconName={icons.INTERACTIVE}
isDisabled={disableSelectedActions}
onPress={handleImportSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
@ -358,6 +389,13 @@ function QueueContent() {
onRemovePress={handleRemoveSelectedConfirmed}
onModalClose={handleConfirmRemoveModalClose}
/>
<InteractiveImportModal
isOpen={isInteractiveImportDownloadIds.length > 0}
downloadIds={isInteractiveImportDownloadIds}
title={translate('InteractiveImportMultipleQueueItems')}
onModalClose={handleImportSelectedModalClose}
/>
</PageContent>
);
}

View file

@ -45,7 +45,7 @@ interface QueueRowProps {
id: number;
seriesId?: number;
episodeIds: number[];
downloadId?: string;
downloadId: string;
title: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
@ -399,7 +399,7 @@ function QueueRow(props: QueueRowProps) {
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
downloadIds={[downloadId]}
title={title}
onModalClose={handleInteractiveImportModalClose}
/>

View file

@ -202,7 +202,7 @@ function isSameEpisodeFile(
const filterExistingFilesStore = create<boolean>(() => false);
export interface InteractiveImportModalContentProps {
downloadId?: string;
downloadIds?: string[];
seriesId?: number;
seasonNumber?: number;
showSeries?: boolean;
@ -224,7 +224,7 @@ function InteractiveImportModalContentInner(
props: InteractiveImportModalContentProps
) {
const {
downloadId,
downloadIds,
seriesId,
seasonNumber,
allowSeriesChange = true,
@ -252,7 +252,7 @@ function InteractiveImportModalContentInner(
data,
originalItems,
} = useInteractiveImport({
downloadId,
downloadIds,
seriesId,
seasonNumber,
folder,
@ -484,7 +484,8 @@ function InteractiveImportModalContentInner(
}, [setIsConfirmDeleteModalOpen]);
const handleImportSelectedPress = useCallback(() => {
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
const finalImportMode =
downloadIds || !showImportMode ? 'auto' : importMode;
const existingFiles: Partial<EpisodeFile>[] = [];
const files: InteractiveImportCommandOptions[] = [];
@ -502,6 +503,7 @@ function InteractiveImportModalContentInner(
if (isSelected) {
const {
downloadId,
series,
seasonNumber,
episodes,
@ -605,7 +607,7 @@ function InteractiveImportModalContentInner(
onModalClose();
}
}, [
downloadId,
downloadIds,
showImportMode,
importMode,
items,
@ -921,7 +923,7 @@ function InteractiveImportModalContentInner(
</SpinnerButton>
) : null}
{!downloadId && showImportMode ? (
{!downloadIds && showImportMode ? (
<SelectInput
className={styles.importMode}
name="importMode"
@ -1046,9 +1048,9 @@ function InteractiveImportModalContent(
) {
const filterExistingFiles = filterExistingFilesStore((state) => state);
const { downloadId, seriesId, seasonNumber, folder } = props;
const { downloadIds, seriesId, seasonNumber, folder } = props;
const { data } = useInteractiveImport({
downloadId,
downloadIds,
seriesId,
seasonNumber,
folder,

View file

@ -12,7 +12,7 @@ interface InteractiveImportModalProps
extends Omit<InteractiveImportModalContentProps, 'modalTitle'> {
isOpen: boolean;
folder?: string;
downloadId?: string;
downloadIds?: string[];
modalTitle?: string;
onModalClose(): void;
}
@ -21,7 +21,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
const {
isOpen,
folder,
downloadId,
downloadIds,
modalTitle = translate('ManualImport'),
onModalClose,
...otherProps
@ -54,11 +54,11 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
{folderPath || downloadId ? (
{folderPath || downloadIds ? (
<InteractiveImportModalContent
{...otherProps}
folder={folderPath}
downloadId={downloadId}
downloadIds={downloadIds}
modalTitle={modalTitle}
onModalClose={onModalClose}
/>

View file

@ -13,7 +13,7 @@ import ReleaseType from './ReleaseType';
const DEFAULT_ITEMS: InteractiveImport[] = [];
interface InteractiveImportParams {
downloadId?: string;
downloadIds?: string[];
seriesId?: number;
seasonNumber?: number;
folder?: string;

View file

@ -7,6 +7,7 @@ export interface QueryParams {
| boolean
| PropertyFilter[]
| number[]
| string[]
| undefined;
}

View file

@ -976,6 +976,7 @@
"ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling",
"ImportScriptPath": "Import Script Path",
"ImportScriptPathHelpText": "The path to the script to use for importing",
"ImportSelected": "Import Selected",
"ImportSeries": "Import Series",
"ImportUsingScript": "Import Using Script",
"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",
"InteractiveImport": "Interactive Import",
"InteractiveImportLoadError": "Unable to load manual import items",
"InteractiveImportMultipleQueueItems": "Multiple Queue Items",
"InteractiveImportNoEpisode": "One or more episodes must be chosen for each selected file",
"InteractiveImportNoFilesFound": "No video files were found in the selected folder",
"InteractiveImportNoImportMode": "An import mode must be selected",

View file

@ -20,14 +20,34 @@ public ManualImportController(IManualImportService manualImportService)
[HttpGet]
[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]

View file

@ -52,7 +52,7 @@ public static ManualImportResource ToResource(this ManualImportItem model)
Size = model.Size,
Series = model.Series?.ToResource(),
SeasonNumber = model.SeasonNumber,
Episodes = model.Episodes.ToResource(),
Episodes = model.Episodes?.ToResource() ?? [],
EpisodeFileId = model.EpisodeFileId,
ReleaseGroup = model.ReleaseGroup,
Quality = model.Quality,