1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-20 21:54:58 -04:00

Use react-query for series import

This commit is contained in:
Mark McDowall
2025-12-28 08:24:34 -08:00
parent 25fb4c4d7a
commit ad57cf4b5d
17 changed files with 420 additions and 686 deletions
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router';
import {
setAddSeriesOption,
@@ -13,13 +13,12 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import useRootFolders, { useRootFolder } from 'RootFolder/useRootFolders';
import { clearImportSeries } from 'Store/Actions/importSeriesActions';
import translate from 'Utilities/String/translate';
import ImportSeriesFooter from './ImportSeriesFooter';
import { clearImportSeries } from './importSeriesStore';
import ImportSeriesTable from './ImportSeriesTable';
function ImportSeries() {
const dispatch = useDispatch();
const { rootFolderId: rootFolderIdString } = useParams<{
rootFolderId: string;
}>();
@@ -68,9 +67,9 @@ function ImportSeries() {
useEffect(() => {
return () => {
dispatch(clearImportSeries());
clearImportSeries();
};
}, [rootFolderId, dispatch]);
}, [rootFolderId]);
useEffect(() => {
if (
@@ -79,13 +78,15 @@ function ImportSeries() {
) {
setAddSeriesOption('qualityProfileId', qualityProfiles[0].id);
}
}, [defaultQualityProfileId, qualityProfiles, dispatch]);
}, [defaultQualityProfileId, qualityProfiles]);
return (
<SelectProvider items={items}>
<PageContent title={translate('ImportSeries')}>
<PageContentBody ref={scrollerRef}>
{rootFoldersFetching ? <LoadingIndicator /> : null}
{rootFoldersFetching && !rootFoldersFetched ? (
<LoadingIndicator />
) : null}
{!rootFoldersFetching && !!rootFoldersError ? (
<Alert kind={kinds.DANGER}>
@@ -103,20 +104,14 @@ function ImportSeries() {
) : null}
{!rootFoldersError &&
!rootFoldersFetching &&
rootFoldersFetched &&
!!unmappedFolders.length &&
scrollerRef.current ? (
<ImportSeriesTable
unmappedFolders={unmappedFolders}
scrollerRef={scrollerRef}
/>
<ImportSeriesTable items={items} scrollerRef={scrollerRef} />
) : null}
</PageContentBody>
{!rootFoldersError &&
!rootFoldersFetching &&
!!unmappedFolders.length ? (
{!rootFoldersError && rootFoldersFetched && !!unmappedFolders.length ? (
<ImportSeriesFooter />
) : null}
</PageContent>
@@ -1,12 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
AddSeriesOptions,
setAddSeriesOption,
useAddSeriesOptions,
} from 'AddSeries/addSeriesOptionsStore';
import { useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import CheckInput from 'Components/Form/CheckInput';
import FormInputGroup from 'Components/Form/FormInputGroup';
import Icon from 'Components/Icon';
@@ -17,20 +15,22 @@ import PageContentFooter from 'Components/Page/PageContentFooter';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import { SeriesMonitor, SeriesType } from 'Series/Series';
import {
cancelLookupSeries,
importSeries,
lookupUnsearchedSeries,
setImportSeriesValue,
} from 'Store/Actions/importSeriesActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import {
ImportSeriesItem,
startProcessing,
stopProcessing,
updateImportSeriesItem,
useImportSeriesItems,
useLookupQueueHasItems,
} from './importSeriesStore';
import { useImportSeries } from './useImportSeries';
import styles from './ImportSeriesFooter.css';
type MixedType = 'mixed';
function ImportSeriesFooter() {
const dispatch = useDispatch();
const {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
@@ -38,9 +38,8 @@ function ImportSeriesFooter() {
seasonFolder: defaultSeasonFolder,
} = useAddSeriesOptions();
const { isLookingUpSeries, isImporting, items, importError } = useSelector(
(state: AppState) => state.importSeries
);
const items = useImportSeriesItems();
const isLookingUpSeries = useLookupQueueHasItems();
const [monitor, setMonitor] = useState<SeriesMonitor | MixedType>(
defaultMonitor
@@ -55,7 +54,9 @@ function ImportSeriesFooter() {
defaultSeasonFolder
);
const { selectedCount, getSelectedIds } = useSelect();
const { selectedCount, getSelectedIds } = useSelect<ImportSeriesItem>();
const { importSeries, isImporting, importError } = useImportSeries();
const {
hasUnsearchedItems,
@@ -87,7 +88,7 @@ function ImportSeriesFooter() {
isSeasonFolderMixed = true;
}
if (!item.isPopulated) {
if (!item.hasSearched) {
hasUnsearchedItems = true;
}
});
@@ -123,29 +124,26 @@ function ImportSeriesFooter() {
setAddSeriesOption(name as keyof AddSeriesOptions, value);
getSelectedIds().forEach((id) => {
dispatch(
// @ts-expect-error - actions are not typed
setImportSeriesValue({
id,
[name]: value,
})
);
updateImportSeriesItem({
id,
[name]: value,
});
});
},
[getSelectedIds, dispatch]
[getSelectedIds]
);
const handleLookupPress = useCallback(() => {
dispatch(lookupUnsearchedSeries());
}, [dispatch]);
startProcessing();
}, []);
const handleCancelLookupPress = useCallback(() => {
dispatch(cancelLookupSeries());
}, [dispatch]);
stopProcessing();
}, []);
const handleImportPress = useCallback(() => {
dispatch(importSeries({ ids: getSelectedIds() }));
}, [getSelectedIds, dispatch]);
importSeries(getSelectedIds());
}, [importSeries, getSelectedIds]);
useEffect(() => {
if (isMonitorMixed && monitor !== 'mixed') {
@@ -286,12 +284,12 @@ function ImportSeriesFooter() {
title={translate('ImportErrors')}
body={
<ul>
{Array.isArray(importError.responseJSON) ? (
importError.responseJSON.map((error, index) => {
{Array.isArray(importError.statusBody) ? (
importError.statusBody.map((error, index) => {
return <li key={index}>{error.errorMessage}</li>;
})
) : (
<li>{JSON.stringify(importError.responseJSON)}</li>
<li>{JSON.stringify(importError.statusBody)}</li>
)}
</ul>
}
@@ -1,39 +1,29 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import { ImportSeries } from 'App/State/ImportSeriesAppState';
import FormInputGroup from 'Components/Form/FormInputGroup';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { inputTypes } from 'Helpers/Props';
import useExistingSeries from 'Series/useExistingSeries';
import { setImportSeriesValue } from 'Store/Actions/importSeriesActions';
import { InputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import {
ImportSeriesItem,
UnamppedFolderItem,
updateImportSeriesItem,
useImportSeriesItem,
} from './importSeriesStore';
import ImportSeriesSelectSeries from './SelectSeries/ImportSeriesSelectSeries';
import styles from './ImportSeriesRow.css';
function createItemSelector(id: string) {
return createSelector(
(state: AppState) => state.importSeries.items,
(items) => {
return (
items.find((item) => {
return item.id === id;
}) || ({} as ImportSeries)
);
}
);
}
interface ImportSeriesRowProps {
id: string;
unmappedFolder: UnamppedFolderItem;
}
function ImportSeriesRow({ id }: ImportSeriesRowProps) {
const dispatch = useDispatch();
function ImportSeriesRow({ unmappedFolder }: ImportSeriesRowProps) {
const id = unmappedFolder.id;
const item = useImportSeriesItem(unmappedFolder.id);
const {
relativePath,
@@ -42,24 +32,18 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
seasonFolder,
seriesType,
selectedSeries,
} = useSelector(createItemSelector(id));
} = item ?? {};
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
const { getIsSelected, toggleSelected, toggleDisabled } =
useSelect<ImportSeries>();
useSelect<ImportSeriesItem>();
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(
// @ts-expect-error - actions are not typed
setImportSeriesValue({
id,
[name]: value,
})
);
updateImportSeriesItem({ id, [name]: value });
},
[id, dispatch]
[id]
);
const handleSelectedChange = useCallback(
@@ -77,6 +61,10 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
toggleDisabled(id, !selectedSeries || isExistingSeries);
}, [id, selectedSeries, isExistingSeries, toggleDisabled]);
useEffect(() => {
toggleSelected({ id, isSelected: !!selectedSeries, shiftKey: false });
}, [id, selectedSeries, toggleSelected]);
return (
<>
<VirtualTableSelectCell<string>
@@ -1,33 +1,25 @@
import React, { RefObject, useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { RefObject, useCallback, useRef } from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
import { useAppDimension } from 'App/appStore';
import { useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import { ImportSeries } from 'App/State/ImportSeriesAppState';
import VirtualTable from 'Components/Table/VirtualTable';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSeries from 'Series/useSeries';
import {
queueLookupSeries,
setImportSeriesValue,
} from 'Store/Actions/importSeriesActions';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { UnmappedFolder } from 'typings/RootFolder';
import ImportSeriesHeader from './ImportSeriesHeader';
import ImportSeriesRow from './ImportSeriesRow';
import {
UnamppedFolderItem,
useEnsureImportSeriesItems,
} from './importSeriesStore';
import styles from './ImportSeriesTable.css';
const ROW_HEIGHT = 52;
interface RowItemData {
items: ImportSeries[];
items: UnamppedFolderItem[];
}
interface ImportSeriesTableProps {
unmappedFolders: UnmappedFolder[];
items: UnamppedFolderItem[];
scrollerRef: RefObject<HTMLElement>;
}
@@ -49,42 +41,17 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
}}
className={styles.row}
>
<ImportSeriesRow key={item.id} id={item.id} />
<ImportSeriesRow key={item.id} unmappedFolder={item} />
</div>
);
}
function ImportSeriesTable({
unmappedFolders,
scrollerRef,
}: ImportSeriesTableProps) {
const dispatch = useDispatch();
const { monitor, qualityProfileId, seriesType, seasonFolder } =
useAddSeriesOptions();
const items = useSelector((state: AppState) => state.importSeries.items);
function ImportSeriesTable({ items, scrollerRef }: ImportSeriesTableProps) {
const isSmallScreen = useAppDimension('isSmallScreen');
const { data: allSeries } = useSeries();
const {
allSelected,
allUnselected,
getIsSelected,
toggleSelected,
selectAll,
unselectAll,
} = useSelect();
const defaultValues = useRef({
monitor,
qualityProfileId,
seriesType,
seasonFolder,
});
const { allSelected, allUnselected, selectAll, unselectAll, useHasItems } =
useSelect();
const listRef = useRef<FixedSizeList<RowItemData>>(null);
const initialUnmappedFolders = useRef(unmappedFolders);
const previousItems = usePrevious(items);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
@@ -97,95 +64,11 @@ function ImportSeriesTable({
[selectAll, unselectAll]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey }: SelectStateInputProps<string>) => {
toggleSelected({
id,
isSelected: value,
shiftKey,
});
},
[toggleSelected]
);
const hasSelectItems = useHasItems();
// TODO: Check if this is still needed
const handleRemoveSelectedStateItem = useCallback((_id: string) => {
// selectDispatch({
// type: 'removeItem',
// id,
// });
}, []);
useEnsureImportSeriesItems(items);
useEffect(() => {
initialUnmappedFolders.current.forEach(({ name, path, relativePath }) => {
dispatch(
queueLookupSeries({
name,
path,
relativePath,
term: name,
})
);
dispatch(
// @ts-expect-error - actions are not typed
setImportSeriesValue({
id: name,
...defaultValues.current,
})
);
});
}, [dispatch]);
useEffect(() => {
previousItems?.forEach((prevItem) => {
const { id } = prevItem;
const item = items.find((i) => i.id === id);
if (!item) {
handleRemoveSelectedStateItem(id);
return;
}
const selectedSeries = item.selectedSeries;
const isSelected = getIsSelected(id);
const isExistingSeries =
!!selectedSeries &&
allSeries.some((s) => s.tvdbId === selectedSeries.tvdbId);
if (
(!selectedSeries && prevItem.selectedSeries) ||
(isExistingSeries && !prevItem.selectedSeries)
) {
handleSelectedChange({ id, value: false, shiftKey: false });
return;
}
if (isSelected && (!selectedSeries || isExistingSeries)) {
handleSelectedChange({ id, value: false, shiftKey: false });
return;
}
if (selectedSeries && selectedSeries !== prevItem.selectedSeries) {
handleSelectedChange({ id, value: true, shiftKey: false });
return;
}
});
}, [
allSeries,
items,
previousItems,
handleRemoveSelectedStateItem,
handleSelectedChange,
getIsSelected,
]);
if (!items.length) {
if (!items.length || !hasSelectItems) {
return null;
}
@@ -7,23 +7,27 @@ import {
useFloating,
useInteractions,
} from '@floating-ui/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import useImportSeriesItem from 'AddSeries/ImportSeries/Import/useImportSeriesItem';
import AppState from 'App/State/AppState';
import React, { useCallback, useEffect, useState } from 'react';
import { useLookupSeries } from 'AddSeries/AddNewSeries/useAddSeries';
import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useDebounce from 'Helpers/Hooks/useDebounce';
import { icons, kinds } from 'Helpers/Props';
import {
queueLookupSeries,
setImportSeriesValue,
} from 'Store/Actions/importSeriesActions';
import useExistingSeries from 'Series/useExistingSeries';
import { InputChanged } from 'typings/inputs';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import {
addToLookupQueue,
removeFromLookupQueue,
updateImportSeriesItem,
useImportSeriesItem,
useIsCurrentedItemQueued,
useIsCurrentLookupQueueItem,
} from '../importSeriesStore';
import ImportSeriesSearchResult from './ImportSeriesSearchResult';
import ImportSeriesTitle from './ImportSeriesTitle';
import styles from './ImportSeriesSelectSeries.css';
@@ -37,28 +41,23 @@ function ImportSeriesSelectSeries({
id,
onInputChange,
}: ImportSeriesSelectSeriesProps) {
const dispatch = useDispatch();
const isLookingUpSeries = useSelector(
(state: AppState) => state.importSeries.isLookingUpSeries
const importSeriesItem = useImportSeriesItem(id);
const { selectedSeries, name } = importSeriesItem ?? {};
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
const [term, setTerm] = useState(name);
const [isOpen, setIsOpen] = useState(false);
const query = useDebounce(term, term ? 300 : 0);
const isCurrentLookupQueueItem = useIsCurrentLookupQueueItem(id);
const isQueued = useIsCurrentedItemQueued(id);
const { isFetching, isFetched, error, data, refetch } = useLookupSeries(
query,
isCurrentLookupQueueItem
);
const {
error,
isFetching = true,
isPopulated = false,
items = [],
isQueued = true,
selectedSeries,
isExistingSeries,
term: itemTerm,
} = useImportSeriesItem(id);
const seriesLookupTimeout = useRef<ReturnType<typeof setTimeout>>();
const [term, setTerm] = useState('');
const [isOpen, setIsOpen] = useState(false);
const errorMessage = getErrorMessage(error);
const isLookingUpSeries = isFetching || isQueued;
const handlePress = useCallback(() => {
setIsOpen((prevIsOpen) => !prevIsOpen);
@@ -66,48 +65,26 @@ function ImportSeriesSelectSeries({
const handleSearchInputChange = useCallback(
({ value }: InputChanged<string>) => {
if (seriesLookupTimeout.current) {
clearTimeout(seriesLookupTimeout.current);
}
setTerm(value);
seriesLookupTimeout.current = setTimeout(() => {
dispatch(
queueLookupSeries({
name: id,
term: value,
topOfQueue: true,
})
);
}, 200);
addToLookupQueue(id);
},
[id, dispatch]
[id]
);
const handleRefreshPress = useCallback(() => {
dispatch(
queueLookupSeries({
name: id,
term,
topOfQueue: true,
})
);
}, [id, term, dispatch]);
refetch();
}, [refetch]);
const handleSeriesSelect = useCallback(
(tvdbId: number) => {
setIsOpen(false);
const selectedSeries = items.find((item) => item.tvdbId === tvdbId)!;
const selectedSeries = data.find((item) => item.tvdbId === tvdbId)!;
dispatch(
// @ts-expect-error - actions are not typed
setImportSeriesValue({
id,
selectedSeries,
})
);
updateImportSeriesItem({
id,
selectedSeries,
});
if (selectedSeries.seriesType !== 'standard') {
onInputChange({
@@ -116,12 +93,24 @@ function ImportSeriesSelectSeries({
});
}
},
[id, items, dispatch, onInputChange]
[id, data, onInputChange]
);
useEffect(() => {
setTerm(itemTerm);
}, [itemTerm]);
if (isFetched) {
updateImportSeriesItem({
id,
hasSearched: isFetched,
selectedSeries: data[0],
});
removeFromLookupQueue(id);
}
}, [id, isFetched, data]);
useEffect(() => {
setTerm(name);
}, [name]);
const { refs, context, floatingStyles } = useFloating({
middleware: [
@@ -148,11 +137,11 @@ function ImportSeriesSelectSeries({
<>
<div ref={refs.setReference} {...getReferenceProps()}>
<Link className={styles.button} component="div" onPress={handlePress}>
{isLookingUpSeries && isQueued && !isPopulated ? (
{isLookingUpSeries && isQueued && !isFetched ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
{isPopulated && selectedSeries && isExistingSeries ? (
{isFetched && selectedSeries && isExistingSeries ? (
<Icon
className={styles.warningIcon}
name={icons.WARNING}
@@ -160,7 +149,7 @@ function ImportSeriesSelectSeries({
/>
) : null}
{isPopulated && selectedSeries ? (
{isFetched && selectedSeries ? (
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
@@ -169,7 +158,7 @@ function ImportSeriesSelectSeries({
/>
) : null}
{isPopulated && !selectedSeries ? (
{isFetched && !selectedSeries ? (
<div>
<Icon
className={styles.warningIcon}
@@ -199,6 +188,7 @@ function ImportSeriesSelectSeries({
</div>
</Link>
</div>
{isOpen ? (
<FloatingPortal id="portal-root">
<div
@@ -233,7 +223,7 @@ function ImportSeriesSelectSeries({
</div>
<div className={styles.results}>
{items.map((item) => {
{data.map((item) => {
return (
<ImportSeriesSearchResult
key={item.tvdbId}
@@ -0,0 +1,177 @@
import { useEffect } from 'react';
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
import { UnmappedFolder } from 'RootFolder/useRootFolders';
import Series, { SeriesMonitor, SeriesType } from 'Series/Series';
export interface UnamppedFolderItem extends UnmappedFolder {
id: string;
}
export interface ImportSeriesItem {
id: string;
monitor: SeriesMonitor;
path: string;
qualityProfileId: number;
relativePath: string;
seasonFolder: boolean;
selectedSeries?: Series;
seriesType: SeriesType;
name: string;
hasSearched: boolean;
}
interface ImportSeriesState {
items: Record<string, ImportSeriesItem>;
lookupQueue: string[];
isProcessing: boolean;
}
const defaultState: ImportSeriesState = {
items: {},
lookupQueue: [],
isProcessing: false,
};
const importSeriesStore = create<ImportSeriesState>()(() => defaultState);
export const useEnsureImportSeriesItems = (
unmappedFolders: UnamppedFolderItem[]
) => {
const { monitor, qualityProfileId, seriesType, seasonFolder } =
useAddSeriesOptions();
useEffect(() => {
unmappedFolders.forEach((unmappedFolder) => {
const existingItem =
importSeriesStore.getState().items[unmappedFolder.id];
if (existingItem) {
return;
}
const newItem: ImportSeriesItem = {
...unmappedFolder,
monitor,
qualityProfileId,
seriesType,
seasonFolder,
hasSearched: false,
};
importSeriesStore.setState((state) => ({
items: {
...state.items,
[unmappedFolder.id]: newItem,
},
}));
});
}, [unmappedFolders, monitor, qualityProfileId, seriesType, seasonFolder]);
};
export const updateImportSeriesItem = (
itemData: Partial<ImportSeriesItem> & Pick<ImportSeriesItem, 'id'>
) => {
console.info('\x1b[36m[MarkTest] updating item\x1b[0m', itemData);
importSeriesStore.setState((state) => {
const existingItem = state.items[itemData.id];
if (existingItem) {
return {
items: {
...state.items,
[itemData.id]: {
...existingItem,
...itemData,
},
},
};
}
return state;
});
};
export const removeImportSeriesItemByPath = (path: string) => {
importSeriesStore.setState((state) => {
const item = Object.values(state.items).find((i) => i.path === path);
if (!item) {
return state;
}
const { [item.id]: removed, ...items } = state.items;
return { items };
});
};
export const clearImportSeries = () => {
importSeriesStore.setState(defaultState);
};
export const startProcessing = () => {
importSeriesStore.setState((state) => {
const items = Object.values(state.items).reduce<string[]>((acc, item) => {
if (!item.hasSearched) {
acc.push(item.id);
}
return acc;
}, []);
return { isProcessing: true, lookupQueue: items };
});
};
export const stopProcessing = () => {
importSeriesStore.setState({ isProcessing: false, lookupQueue: [] });
};
export const addToLookupQueue = (id: string) => {
importSeriesStore.setState((state) => ({
lookupQueue: [...state.lookupQueue, id],
}));
};
export const removeFromLookupQueue = (id: string) => {
importSeriesStore.setState((state) => ({
lookupQueue: state.lookupQueue.filter((queuedId) => queuedId !== id),
}));
};
export const useIsCurrentLookupQueueItem = (id: string) => {
return importSeriesStore((state) => state.lookupQueue[0] === id);
};
export const useIsCurrentedItemQueued = (id: string) => {
return importSeriesStore((state) => state.lookupQueue.includes(id));
};
export const useLookupQueueHasItems = () => {
return importSeriesStore((state) => state.lookupQueue.length > 0);
};
export const useImportSeriesItem = (id: string) => {
return importSeriesStore((state) => state.items[id]);
};
export const useImportSeriesItems = () => {
return importSeriesStore(useShallow((state) => Object.values(state.items)));
};
export const getImportSeriesItems = (ids: string[]) => {
const state = importSeriesStore.getState();
return ids.reduce<ImportSeriesItem[]>((acc, id) => {
const item = state.items[id];
if (item != null) {
acc.push(item);
}
return acc;
}, []);
};
@@ -0,0 +1,85 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import Series from 'Series/Series';
import {
getImportSeriesItems,
removeImportSeriesItemByPath,
} from './importSeriesStore';
export const useImportSeries = () => {
const queryClient = useQueryClient();
const { isPending, error, mutate } = useApiMutation<Series[], Series[]>({
path: '/series/import',
method: 'POST',
mutationOptions: {
onSuccess: (data, newSeries) => {
queryClient.invalidateQueries({ queryKey: ['/rootFolder'] });
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
if (!oldSeries) {
return data;
}
return [...oldSeries, ...data];
});
newSeries.forEach((series) => {
removeImportSeriesItemByPath(series.path);
});
},
},
});
const importSeries = useCallback(
(ids: string[]) => {
const items = getImportSeriesItems(ids);
const addedIds: string[] = [];
const allNewSeries = ids.reduce<Series[]>((acc, id) => {
const item = items.find((i) => i.id === id);
const selectedSeries = item?.selectedSeries;
// Make sure we have a selected series and the same series hasn't been added yet.
if (
selectedSeries &&
!acc.some((a) => a.tvdbId === selectedSeries.tvdbId)
) {
const newSeries: Series = {
...selectedSeries,
monitored: true,
monitorNewItems: 'all',
qualityProfileId: item.qualityProfileId,
path: item.path,
seriesType: item.seriesType,
seasonFolder: item.seasonFolder,
addOptions: {
monitor: item.monitor,
searchForMissingEpisodes: false,
searchForCutoffUnmetEpisodes: false,
},
tags: [],
};
newSeries.path = item.path;
addedIds.push(id);
acc.push(newSeries);
}
return acc;
}, []);
if (allNewSeries.length > 0) {
mutate(allNewSeries);
}
},
[mutate]
);
return {
isImporting: isPending,
importError: error,
importSeries,
};
};
@@ -1,31 +0,0 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { ImportSeries } from 'App/State/ImportSeriesAppState';
import useSeries from 'Series/useSeries';
function useImportSeriesItem(id: string) {
const { data: series = [] } = useSeries();
const importSeries = useSelector((state: AppState) => state.importSeries);
return useMemo(() => {
const item =
importSeries.items.find((item) => {
return item.id === id;
}) ?? ({} as ImportSeries);
const selectedSeries = item && item.selectedSeries;
const isExistingSeries =
!!selectedSeries &&
series.some((s) => {
return s.tvdbId === selectedSeries.tvdbId;
});
return {
...item,
isExistingSeries,
};
}, [id, importSeries.items, series]);
}
export default useImportSeriesItem;