mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-20 21:54:58 -04:00
5f8297da6c
Closes #7552
994 lines
24 KiB
TypeScript
994 lines
24 KiB
TypeScript
import { useQueryClient } from '@tanstack/react-query';
|
|
import moment from 'moment';
|
|
import { useCallback, useMemo } from 'react';
|
|
import { FilterBuilderTag } from 'Components/Filter/Builder/FilterBuilderRowValue';
|
|
import { Filter, FilterBuilderProp } from 'Filters/Filter';
|
|
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
|
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
|
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
|
import {
|
|
filterBuilderTypes,
|
|
filterBuilderValueTypes,
|
|
sortDirections,
|
|
} from 'Helpers/Props';
|
|
import { FilterType } from 'Helpers/Props/filterTypes';
|
|
import getFilterTypePredicate from 'Helpers/Props/getFilterTypePredicate';
|
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
|
import sortByProp from 'Utilities/Array/sortByProp';
|
|
import clientSideFilterAndSort from 'Utilities/Filter/clientSideFilterAndSort';
|
|
import translate from 'Utilities/String/translate';
|
|
import Series, { Statistics } from './Series';
|
|
import { useSeriesOptions } from './seriesOptionsStore';
|
|
|
|
// Date filter predicate helper
|
|
const dateFilterPredicate = (
|
|
itemDate: string | undefined,
|
|
filterValue: string | Date,
|
|
type: FilterType
|
|
): boolean => {
|
|
if (!itemDate) return false;
|
|
const predicate = getFilterTypePredicate(type);
|
|
return predicate(itemDate, filterValue);
|
|
};
|
|
|
|
export const FILTERS: Filter[] = [
|
|
{
|
|
key: 'all',
|
|
label: () => translate('All'),
|
|
filters: [],
|
|
},
|
|
{
|
|
key: 'monitored',
|
|
label: () => translate('MonitoredOnly'),
|
|
filters: [
|
|
{
|
|
key: 'monitored',
|
|
value: [true],
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'unmonitored',
|
|
label: () => translate('UnmonitoredOnly'),
|
|
filters: [
|
|
{
|
|
key: 'monitored',
|
|
value: [false],
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'continuing',
|
|
label: () => translate('ContinuingOnly'),
|
|
filters: [
|
|
{
|
|
key: 'status',
|
|
value: 'continuing',
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'ended',
|
|
label: () => translate('EndedOnly'),
|
|
filters: [
|
|
{
|
|
key: 'status',
|
|
value: 'ended',
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'missing',
|
|
label: () => translate('MissingEpisodes'),
|
|
filters: [
|
|
{
|
|
key: 'missing',
|
|
value: [true],
|
|
type: 'equal',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const SORT_PREDICATES = {
|
|
status: (item: Series, _direction: SortDirection) => {
|
|
let result = 0;
|
|
|
|
if (item.monitored) {
|
|
result += 2;
|
|
}
|
|
|
|
if (item.status === 'continuing') {
|
|
result++;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
sizeOnDisk: (item: Series, _direction: SortDirection) => {
|
|
return item.statistics?.sizeOnDisk ?? 0;
|
|
},
|
|
|
|
network: (item: Series, _direction: SortDirection) => {
|
|
const network = item.network;
|
|
|
|
return network ? network.toLowerCase() : '';
|
|
},
|
|
|
|
nextAiring: (item: Series, direction: SortDirection) => {
|
|
const nextAiring = item.nextAiring;
|
|
|
|
if (nextAiring) {
|
|
return moment(nextAiring).unix();
|
|
}
|
|
|
|
if (direction === sortDirections.DESCENDING) {
|
|
return 0;
|
|
}
|
|
|
|
return Number.MAX_VALUE;
|
|
},
|
|
|
|
previousAiring: (item: Series, direction: SortDirection) => {
|
|
const previousAiring = item.previousAiring;
|
|
|
|
if (previousAiring) {
|
|
return moment(previousAiring).unix();
|
|
}
|
|
|
|
if (direction === sortDirections.DESCENDING) {
|
|
return -Number.MAX_VALUE;
|
|
}
|
|
|
|
return Number.MAX_VALUE;
|
|
},
|
|
|
|
episodeProgress: (item: Series, _direction: SortDirection) => {
|
|
const statistics = item.statistics;
|
|
|
|
const episodeCount = statistics?.episodeCount ?? 0;
|
|
const episodeFileCount = statistics?.episodeFileCount ?? 0;
|
|
|
|
const progress = episodeCount
|
|
? (episodeFileCount / episodeCount) * 100
|
|
: 100;
|
|
|
|
return progress + episodeCount / 1000000;
|
|
},
|
|
|
|
episodeCount: (item: Series, _direction: SortDirection) => {
|
|
return item.statistics?.totalEpisodeCount ?? 0;
|
|
},
|
|
|
|
seasonCount: (item: Series, _direction: SortDirection) => {
|
|
return item.statistics?.seasonCount ?? 0;
|
|
},
|
|
|
|
originalLanguage: (item: Series, _direction: SortDirection) => {
|
|
const { originalLanguage } = item;
|
|
|
|
return originalLanguage?.name ?? '';
|
|
},
|
|
|
|
ratings: (item: Series, _direction: SortDirection) => {
|
|
const { ratings } = item;
|
|
|
|
return ratings.value ?? 0;
|
|
},
|
|
|
|
monitorNewItems: (item: Series, _direction: SortDirection) => {
|
|
return item.monitorNewItems === 'all' ? 1 : 0;
|
|
},
|
|
} as const;
|
|
|
|
const FILTER_PREDICATES = {
|
|
episodeProgress: (item: Series, filterValue: number, type: FilterType) => {
|
|
const statistics = item.statistics;
|
|
const episodeCount = statistics?.episodeCount ?? 0;
|
|
const episodeFileCount = statistics?.episodeFileCount ?? 0;
|
|
|
|
const progress = episodeCount
|
|
? (episodeFileCount / episodeCount) * 100
|
|
: 100;
|
|
|
|
const predicate = getFilterTypePredicate(type);
|
|
return predicate(progress, filterValue);
|
|
},
|
|
|
|
missing: (item: Series, _filterValue: boolean, _type: FilterType) => {
|
|
const statistics = item.statistics;
|
|
const episodeCount = statistics?.episodeCount ?? 0;
|
|
const episodeFileCount = statistics?.episodeFileCount ?? 0;
|
|
return episodeCount - episodeFileCount > 0;
|
|
},
|
|
|
|
nextAiring: (item: Series, filterValue: string | Date, type: FilterType) => {
|
|
return dateFilterPredicate(item.nextAiring, filterValue, type);
|
|
},
|
|
|
|
previousAiring: (
|
|
item: Series,
|
|
filterValue: string | Date,
|
|
type: FilterType
|
|
) => {
|
|
return dateFilterPredicate(item.previousAiring, filterValue, type);
|
|
},
|
|
|
|
added: (item: Series, filterValue: string | Date, type: FilterType) => {
|
|
return dateFilterPredicate(item.added, filterValue, type);
|
|
},
|
|
|
|
ratings: (item: Series, filterValue: number, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const value = item.ratings.value ?? 0;
|
|
return predicate(value * 10, filterValue);
|
|
},
|
|
|
|
ratingVotes: (item: Series, filterValue: number, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const votes = item.ratings.votes ?? 0;
|
|
return predicate(votes, filterValue);
|
|
},
|
|
|
|
originalLanguage: (item: Series, filterValue: string, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const languageName = item.originalLanguage?.name ?? '';
|
|
return predicate(languageName, filterValue);
|
|
},
|
|
|
|
releaseGroups: (item: Series, filterValue: string[], type: FilterType) => {
|
|
const releaseGroups = item.statistics?.releaseGroups ?? [];
|
|
const predicate = getFilterTypePredicate(type);
|
|
return predicate(releaseGroups, filterValue);
|
|
},
|
|
|
|
seasonCount: (item: Series, filterValue: number, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const seasonCount = item.statistics?.seasonCount ?? 0;
|
|
return predicate(seasonCount, filterValue);
|
|
},
|
|
|
|
sizeOnDisk: (item: Series, filterValue: number, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const sizeOnDisk = item.statistics?.sizeOnDisk ?? 0;
|
|
return predicate(sizeOnDisk, filterValue);
|
|
},
|
|
|
|
hasMissingSeason: (item: Series, filterValue: boolean, type: FilterType) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const seasons = item.seasons ?? [];
|
|
|
|
const hasMissingSeason = seasons.some((season) => {
|
|
const { seasonNumber } = season;
|
|
const statistics = season.statistics;
|
|
const episodeFileCount = statistics?.episodeFileCount ?? 0;
|
|
const episodeCount = statistics?.episodeCount ?? 0;
|
|
const totalEpisodeCount = statistics?.totalEpisodeCount ?? 0;
|
|
|
|
return (
|
|
seasonNumber > 0 &&
|
|
totalEpisodeCount > 0 &&
|
|
episodeCount === totalEpisodeCount &&
|
|
episodeFileCount === 0
|
|
);
|
|
});
|
|
|
|
return predicate(hasMissingSeason, filterValue);
|
|
},
|
|
|
|
seasonsMonitoredStatus: (
|
|
item: Series,
|
|
filterValue: string,
|
|
type: FilterType
|
|
) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const seasons = item.seasons ?? [];
|
|
|
|
const { monitoredCount, unmonitoredCount } = seasons.reduce(
|
|
(acc, { seasonNumber, monitored }) => {
|
|
if (seasonNumber <= 0) {
|
|
return acc;
|
|
}
|
|
|
|
if (monitored) {
|
|
acc.monitoredCount++;
|
|
} else {
|
|
acc.unmonitoredCount++;
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{ monitoredCount: 0, unmonitoredCount: 0 }
|
|
);
|
|
|
|
let seasonsMonitoredStatus = 'partial';
|
|
|
|
if (monitoredCount === 0) {
|
|
seasonsMonitoredStatus = 'none';
|
|
} else if (unmonitoredCount === 0) {
|
|
seasonsMonitoredStatus = 'all';
|
|
}
|
|
|
|
return predicate(seasonsMonitoredStatus, filterValue);
|
|
},
|
|
|
|
episodesMonitoredStatus: (
|
|
item: Series,
|
|
filterValue: string,
|
|
type: FilterType
|
|
) => {
|
|
const predicate = getFilterTypePredicate(type);
|
|
const { seasons, statistics = {} as Statistics } = item;
|
|
const { monitoredEpisodeCount = 0, totalEpisodeCount = 0 } = statistics;
|
|
const specials = seasons?.find((s) => s.seasonNumber === 0);
|
|
|
|
// The monitored count and total count include specials, but those areskipped
|
|
// for seasons monitored status so we should to exclude them here too.
|
|
|
|
const monitoredCount =
|
|
monitoredEpisodeCount -
|
|
(specials?.statistics?.monitoredEpisodeCount ?? 0);
|
|
|
|
const totalCount =
|
|
totalEpisodeCount - (specials?.statistics?.totalEpisodeCount ?? 0);
|
|
|
|
let episodesMonitoredStatus = 'partial';
|
|
|
|
if (monitoredCount === 0) {
|
|
episodesMonitoredStatus = 'none';
|
|
} else if (totalCount - monitoredCount === 0) {
|
|
episodesMonitoredStatus = 'all';
|
|
}
|
|
|
|
return predicate(episodesMonitoredStatus, filterValue);
|
|
},
|
|
} as const;
|
|
|
|
export const FILTER_BUILDER: FilterBuilderProp<Series>[] = [
|
|
{
|
|
name: 'monitored',
|
|
label: () => translate('Monitored'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.BOOL,
|
|
},
|
|
{
|
|
name: 'status',
|
|
label: () => translate('Status'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.SERIES_STATUS,
|
|
},
|
|
{
|
|
name: 'seriesType',
|
|
label: () => translate('Type'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.SERIES_TYPES,
|
|
},
|
|
{
|
|
name: 'title',
|
|
label: () => translate('Title'),
|
|
type: filterBuilderTypes.STRING,
|
|
},
|
|
{
|
|
name: 'network',
|
|
label: () => translate('Network'),
|
|
type: filterBuilderTypes.ARRAY,
|
|
optionsSelector: function (items: Series[]) {
|
|
const tagList = items.reduce<FilterBuilderTag<string, string>[]>(
|
|
(acc, series) => {
|
|
if (series.network) {
|
|
acc.push({
|
|
id: series.network,
|
|
name: series.network,
|
|
});
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
[]
|
|
);
|
|
|
|
return tagList.sort(sortByProp('name'));
|
|
},
|
|
},
|
|
{
|
|
name: 'qualityProfileId',
|
|
label: () => translate('QualityProfile'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.QUALITY_PROFILE,
|
|
},
|
|
{
|
|
name: 'nextAiring',
|
|
label: () => translate('NextAiring'),
|
|
type: filterBuilderTypes.DATE,
|
|
valueType: filterBuilderValueTypes.DATE,
|
|
},
|
|
{
|
|
name: 'previousAiring',
|
|
label: () => translate('PreviousAiring'),
|
|
type: filterBuilderTypes.DATE,
|
|
valueType: filterBuilderValueTypes.DATE,
|
|
},
|
|
{
|
|
name: 'added',
|
|
label: () => translate('Added'),
|
|
type: filterBuilderTypes.DATE,
|
|
valueType: filterBuilderValueTypes.DATE,
|
|
},
|
|
{
|
|
name: 'seasonCount',
|
|
label: () => translate('SeasonCount'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
},
|
|
{
|
|
name: 'episodeProgress',
|
|
label: () => translate('EpisodeProgress'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
},
|
|
{
|
|
name: 'path',
|
|
label: () => translate('Path'),
|
|
type: filterBuilderTypes.STRING,
|
|
},
|
|
{
|
|
name: 'rootFolderPath',
|
|
label: () => translate('RootFolderPath'),
|
|
type: filterBuilderTypes.EXACT,
|
|
},
|
|
{
|
|
name: 'sizeOnDisk',
|
|
label: () => translate('SizeOnDisk'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
valueType: filterBuilderValueTypes.BYTES,
|
|
},
|
|
{
|
|
name: 'genres',
|
|
label: () => translate('Genres'),
|
|
type: filterBuilderTypes.ARRAY,
|
|
optionsSelector: function (items: Series[]) {
|
|
const tagList = items.reduce<FilterBuilderTag<string, string>[]>(
|
|
(acc, series) => {
|
|
series.genres.forEach((genre) => {
|
|
acc.push({
|
|
id: genre,
|
|
name: genre,
|
|
});
|
|
});
|
|
|
|
return acc;
|
|
},
|
|
[]
|
|
);
|
|
|
|
return tagList.sort(sortByProp('name'));
|
|
},
|
|
},
|
|
{
|
|
name: 'originalLanguage',
|
|
label: () => translate('OriginalLanguage'),
|
|
type: filterBuilderTypes.EXACT,
|
|
optionsSelector: function (items: Series[]) {
|
|
const languageList = items.reduce<FilterBuilderTag<string, string>[]>(
|
|
(acc, series) => {
|
|
if (series.originalLanguage) {
|
|
acc.push({
|
|
id: series.originalLanguage.name,
|
|
name: series.originalLanguage.name,
|
|
});
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
[]
|
|
);
|
|
|
|
return languageList.sort(sortByProp('name'));
|
|
},
|
|
},
|
|
{
|
|
name: 'releaseGroups',
|
|
label: () => translate('ReleaseGroups'),
|
|
type: filterBuilderTypes.ARRAY,
|
|
},
|
|
{
|
|
name: 'ratings',
|
|
label: () => translate('Rating'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
},
|
|
{
|
|
name: 'ratingVotes',
|
|
label: () => translate('RatingVotes'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
},
|
|
{
|
|
name: 'certification',
|
|
label: () => translate('Certification'),
|
|
type: filterBuilderTypes.EXACT,
|
|
},
|
|
{
|
|
name: 'tags',
|
|
label: () => translate('Tags'),
|
|
type: filterBuilderTypes.ARRAY,
|
|
valueType: filterBuilderValueTypes.TAG,
|
|
},
|
|
{
|
|
name: 'useSceneNumbering',
|
|
label: () => translate('SceneNumbering'),
|
|
type: filterBuilderTypes.EXACT,
|
|
},
|
|
{
|
|
name: 'hasMissingSeason',
|
|
label: () => translate('HasMissingSeason'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.BOOL,
|
|
},
|
|
{
|
|
name: 'seasonsMonitoredStatus',
|
|
label: () => translate('SeasonsMonitoredStatus'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.MONITORED_STATUS,
|
|
},
|
|
{
|
|
name: 'episodesMonitoredStatus',
|
|
label: () => translate('EpisodesMonitoredStatus'),
|
|
type: filterBuilderTypes.EXACT,
|
|
valueType: filterBuilderValueTypes.MONITORED_STATUS,
|
|
},
|
|
{
|
|
name: 'year',
|
|
label: () => translate('Year'),
|
|
type: filterBuilderTypes.NUMBER,
|
|
},
|
|
];
|
|
|
|
const DEFAULT_SERIES: Series[] = [];
|
|
|
|
const useSeries = () => {
|
|
const { data, ...result } = useApiQuery<Series[]>({
|
|
path: '/series',
|
|
queryOptions: {
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: Infinity,
|
|
},
|
|
});
|
|
|
|
const seriesMap = useMemo(() => {
|
|
if (!data) {
|
|
return new Map<number, Series>();
|
|
}
|
|
|
|
return new Map<number, Series>(data.map((series) => [series.id, series]));
|
|
}, [data]);
|
|
|
|
return {
|
|
...result,
|
|
data: data ?? DEFAULT_SERIES,
|
|
seriesMap,
|
|
};
|
|
};
|
|
|
|
export default useSeries;
|
|
|
|
export const useSeriesIndex = () => {
|
|
const { selectedFilterKey, sortKey, sortDirection } = useSeriesOptions();
|
|
const { data: seriesData = [], ...queryResult } = useSeries();
|
|
const customFilters = useCustomFiltersList('series');
|
|
|
|
const data = useMemo(() => {
|
|
return clientSideFilterAndSort<
|
|
Series,
|
|
typeof FILTER_PREDICATES,
|
|
typeof SORT_PREDICATES
|
|
>(seriesData, {
|
|
selectedFilterKey,
|
|
filters: FILTERS,
|
|
filterPredicates: FILTER_PREDICATES,
|
|
customFilters,
|
|
sortKey: sortKey as keyof Series,
|
|
sortDirection,
|
|
secondarySortKey: 'sortTitle',
|
|
secondarySortDirection: 'ascending',
|
|
sortPredicates: SORT_PREDICATES,
|
|
});
|
|
}, [customFilters, seriesData, selectedFilterKey, sortKey, sortDirection]);
|
|
|
|
return {
|
|
...queryResult,
|
|
data: data.data,
|
|
totalItems: data.totalItems,
|
|
};
|
|
};
|
|
|
|
export const useHasSeries = () => {
|
|
const { data: seriesData = [] } = useSeries();
|
|
|
|
return useMemo(() => {
|
|
return seriesData.length > 0;
|
|
}, [seriesData]);
|
|
};
|
|
|
|
export const useSingleSeries = (seriesId?: number) => {
|
|
const { seriesMap } = useSeries();
|
|
|
|
return useMemo(() => {
|
|
if (!seriesId) {
|
|
return undefined;
|
|
}
|
|
|
|
return seriesMap.get(seriesId);
|
|
}, [seriesMap, seriesId]);
|
|
};
|
|
|
|
export const useMultipleSeries = (seriesIds: number[]) => {
|
|
const { seriesMap } = useSeries();
|
|
|
|
return useMemo(() => {
|
|
if (seriesIds.length === 0) {
|
|
return DEFAULT_SERIES;
|
|
}
|
|
|
|
return seriesIds.reduce((acc: Series[], seriesId) => {
|
|
const series = seriesMap.get(seriesId);
|
|
|
|
if (series) {
|
|
acc.push(series);
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}, [seriesMap, seriesIds]);
|
|
};
|
|
|
|
interface SaveSeriesPayload extends Partial<Series> {
|
|
id: number;
|
|
}
|
|
|
|
interface DeleteSeriesPayload {
|
|
deleteFiles?: boolean;
|
|
addImportListExclusion?: boolean;
|
|
}
|
|
|
|
interface ToggleSeriesMonitoredPayload {
|
|
monitored: boolean;
|
|
}
|
|
|
|
interface ToggleSeasonMonitoredPayload {
|
|
seasonNumber: number;
|
|
monitored: boolean;
|
|
}
|
|
|
|
interface UpdateSeriesMonitorPayload {
|
|
series: {
|
|
id: number;
|
|
monitored?: boolean;
|
|
seasons?: {
|
|
seasonNumber: number;
|
|
monitored: boolean;
|
|
}[];
|
|
}[];
|
|
monitoringOptions?: {
|
|
monitor: string;
|
|
};
|
|
}
|
|
|
|
interface BulkDeleteSeriesPayload {
|
|
seriesIds: number[];
|
|
deleteFiles?: boolean;
|
|
addImportListExclusion?: boolean;
|
|
}
|
|
|
|
interface SaveSeriesEditorPayload {
|
|
seriesIds: number[];
|
|
monitored?: boolean;
|
|
qualityProfileId?: number;
|
|
seriesType?: string;
|
|
seasonFolder?: boolean;
|
|
rootFolderPath?: string;
|
|
tags?: number[];
|
|
}
|
|
|
|
export const useSaveSeries = (moveFiles?: boolean) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
Series,
|
|
SaveSeriesPayload
|
|
>({
|
|
path: '/series',
|
|
queryParams: {
|
|
moveFiles,
|
|
},
|
|
method: 'PUT',
|
|
mutationOptions: {
|
|
onSuccess: (updatedSeries) => {
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.map((series) => {
|
|
if (series.id === updatedSeries.id) {
|
|
return {
|
|
...series,
|
|
...updatedSeries,
|
|
};
|
|
}
|
|
|
|
return series;
|
|
});
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
saveSeries: mutate,
|
|
isSaving: isPending,
|
|
saveError: error,
|
|
};
|
|
};
|
|
|
|
export const useDeleteSeries = (
|
|
seriesId: number,
|
|
options: DeleteSeriesPayload
|
|
) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<unknown, void>({
|
|
path: `/series/${seriesId}`,
|
|
queryParams: {
|
|
...options,
|
|
},
|
|
method: 'DELETE',
|
|
mutationOptions: {
|
|
onSuccess: () => {
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.filter((series) => series.id !== seriesId);
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
deleteSeries: mutate,
|
|
isDeleting: isPending,
|
|
deleteError: error,
|
|
};
|
|
};
|
|
|
|
export const useToggleSeriesMonitored = (seriesId: number) => {
|
|
const queryClient = useQueryClient();
|
|
const series = useSingleSeries(seriesId);
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
Series,
|
|
ToggleSeriesMonitoredPayload
|
|
>({
|
|
path: '/series',
|
|
method: 'PUT',
|
|
mutationOptions: {
|
|
onSuccess: (updatedSeries) => {
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.map((series) =>
|
|
series.id === updatedSeries.id ? updatedSeries : series
|
|
);
|
|
});
|
|
},
|
|
},
|
|
});
|
|
const toggleSeriesMonitored = useCallback(
|
|
(payload: ToggleSeriesMonitoredPayload) => {
|
|
return mutate({ ...series, ...payload });
|
|
},
|
|
[series, mutate]
|
|
);
|
|
|
|
return {
|
|
toggleSeriesMonitored,
|
|
isTogglingSeriesMonitored: isPending,
|
|
toggleSeriesMonitoredError: error,
|
|
};
|
|
};
|
|
|
|
export const useToggleSeasonMonitored = (seriesId: number) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
Series,
|
|
ToggleSeasonMonitoredPayload
|
|
>({
|
|
path: `/series/${seriesId}/season`,
|
|
method: 'PUT',
|
|
mutationOptions: {
|
|
onSuccess: (updatedSeries) => {
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.map((series) => {
|
|
if (series.id === updatedSeries.id) {
|
|
return {
|
|
...series,
|
|
seasons: series.seasons.map((season) => {
|
|
const updatedSeason = updatedSeries.seasons.find(
|
|
(s) => s.seasonNumber === season.seasonNumber
|
|
);
|
|
|
|
if (updatedSeason) {
|
|
return {
|
|
...season,
|
|
...updatedSeason,
|
|
};
|
|
}
|
|
|
|
return season;
|
|
}),
|
|
};
|
|
}
|
|
|
|
return series;
|
|
});
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
toggleSeasonMonitored: mutate,
|
|
isTogglingSeasonMonitored: isPending,
|
|
toggleSeasonMonitoredError: error,
|
|
};
|
|
};
|
|
|
|
export const useUpdateSeriesMonitor = (
|
|
shouldFetchEpisodesAfterUpdate = false
|
|
) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
void,
|
|
UpdateSeriesMonitorPayload
|
|
>({
|
|
path: '/seasonPass',
|
|
method: 'POST',
|
|
mutationOptions: {
|
|
onSuccess: (_, variables) => {
|
|
if (shouldFetchEpisodesAfterUpdate) {
|
|
queryClient.invalidateQueries({ queryKey: ['/episode'] });
|
|
}
|
|
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.map((series) => {
|
|
const updatedSeries = variables.series.find(
|
|
(s) => s.id === series.id
|
|
);
|
|
|
|
if (!updatedSeries) {
|
|
return series;
|
|
}
|
|
|
|
return {
|
|
...series,
|
|
monitored: updatedSeries.monitored ?? series.monitored,
|
|
seasons: series.seasons.map((season) => {
|
|
const updatedSeason = updatedSeries.seasons?.find(
|
|
(s) => s.seasonNumber === season.seasonNumber
|
|
);
|
|
|
|
if (updatedSeason) {
|
|
return {
|
|
...season,
|
|
monitored: updatedSeason.monitored,
|
|
};
|
|
}
|
|
|
|
return season;
|
|
}),
|
|
};
|
|
});
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
updateSeriesMonitor: mutate,
|
|
isUpdatingSeriesMonitor: isPending,
|
|
updateSeriesMonitorError: error,
|
|
};
|
|
};
|
|
|
|
export const useSaveSeriesEditor = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
Series[],
|
|
SaveSeriesEditorPayload
|
|
>({
|
|
path: '/series/editor',
|
|
method: 'PUT',
|
|
mutationOptions: {
|
|
onSuccess: (updatedSeries) => {
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.map((series) => {
|
|
const updatedSeriesData = updatedSeries.find(
|
|
(updated) => updated.id === series.id
|
|
);
|
|
|
|
if (updatedSeriesData) {
|
|
const {
|
|
alternateTitles,
|
|
images,
|
|
rootFolderPath,
|
|
statistics,
|
|
...propsToUpdate
|
|
} = updatedSeriesData;
|
|
|
|
return { ...series, ...propsToUpdate };
|
|
}
|
|
|
|
return series;
|
|
});
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
saveSeriesEditor: mutate,
|
|
isSavingSeriesEditor: isPending,
|
|
saveSeriesEditorError: error,
|
|
};
|
|
};
|
|
|
|
export const useBulkDeleteSeries = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate, isPending, error } = useApiMutation<
|
|
void,
|
|
BulkDeleteSeriesPayload
|
|
>({
|
|
path: '/series/editor',
|
|
method: 'DELETE',
|
|
mutationOptions: {
|
|
onSuccess: (_, variables) => {
|
|
const seriesIds = new Set(variables.seriesIds);
|
|
|
|
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
|
if (!oldSeries) {
|
|
return oldSeries;
|
|
}
|
|
|
|
return oldSeries.filter((series) => !seriesIds.has(series.id));
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
bulkDeleteSeries: mutate,
|
|
isBulkDeleting: isPending,
|
|
bulkDeleteError: error,
|
|
};
|
|
};
|