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:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user