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

Use react-query for renaming

This commit is contained in:
Mark McDowall
2025-12-20 21:18:39 -08:00
parent bc099f27cb
commit 10c0e18a42
10 changed files with 98 additions and 157 deletions
-2
View File
@@ -3,7 +3,6 @@ import CaptchaAppState from './CaptchaAppState';
import ImportSeriesAppState from './ImportSeriesAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import OAuthAppState from './OAuthAppState';
import OrganizePreviewAppState from './OrganizePreviewAppState';
import ProviderOptionsAppState from './ProviderOptionsAppState';
import SettingsAppState from './SettingsAppState';
@@ -13,7 +12,6 @@ interface AppState {
importSeries: ImportSeriesAppState;
interactiveImport: InteractiveImportAppState;
oAuth: OAuthAppState;
organizePreview: OrganizePreviewAppState;
providerOptions: ProviderOptionsAppState;
settings: SettingsAppState;
}
@@ -1,15 +0,0 @@
import ModelBase from 'App/ModelBase';
import AppSectionState from 'App/State/AppSectionState';
export interface OrganizePreviewModel extends ModelBase {
seriesId: number;
seasonNumber: number;
episodeNumbers: number[];
episodeFileId: number;
existingPath: string;
newPath: string;
}
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
export default OrganizePreviewAppState;
+3 -12
View File
@@ -1,7 +1,5 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
import OrganizePreviewModalContent, {
OrganizePreviewModalContentProps,
} from './OrganizePreviewModalContent';
@@ -16,19 +14,12 @@ function OrganizePreviewModal({
onModalClose,
...otherProps
}: OrganizePreviewModalProps) {
const dispatch = useDispatch();
const handleOnModalClose = useCallback(() => {
dispatch(clearOrganizePreview());
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleOnModalClose}>
<Modal isOpen={isOpen} onModalClose={onModalClose}>
{isOpen ? (
<OrganizePreviewModalContent
{...otherProps}
onModalClose={handleOnModalClose}
onModalClose={onModalClose}
/>
) : null}
</Modal>
@@ -2,7 +2,6 @@ import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import { OrganizePreviewModel } from 'App/State/OrganizePreviewAppState';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
@@ -17,11 +16,11 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import formatSeason from 'Season/formatSeason';
import { useSingleSeries } from 'Series/useSeries';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import OrganizePreviewRow from './OrganizePreviewRow';
import useOrganizePreview, { OrganizePreviewModel } from './useOrganizePreview';
import styles from './OrganizePreviewModalContent.css';
function getValue(allSelected: boolean, allUnselected: boolean) {
@@ -50,9 +49,9 @@ function OrganizePreviewModalContentInner({
const {
items,
isFetching: isPreviewFetching,
isPopulated: isPreviewPopulated,
isFetched: isPreviewFetched,
error: previewError,
} = useSelector((state: AppState) => state.organizePreview);
} = useOrganizePreview(seriesId, seasonNumber);
const {
isFetching: isNamingFetching,
@@ -67,7 +66,7 @@ function OrganizePreviewModalContentInner({
useSelect<OrganizePreviewModel>();
const isFetching = isPreviewFetching || isNamingFetching;
const isPopulated = isPreviewPopulated && isNamingPopulated;
const isPopulated = isPreviewFetched && isNamingPopulated;
const error = previewError || namingError;
const { renameEpisodes } = naming;
const episodeFormat = naming[`${series.seriesType}EpisodeFormat`];
@@ -98,9 +97,8 @@ function OrganizePreviewModalContentInner({
}, [seriesId, getSelectedIds, executeCommand, onModalClose]);
useEffect(() => {
dispatch(fetchOrganizePreview({ seriesId, seasonNumber }));
dispatch(fetchNamingSettings());
}, [seriesId, seasonNumber, dispatch]);
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
@@ -191,7 +189,7 @@ function OrganizePreviewModalContent({
seasonNumber,
onModalClose,
}: OrganizePreviewModalContentProps) {
const { items } = useSelector((state: AppState) => state.organizePreview);
const { items } = useOrganizePreview(seriesId, seasonNumber);
return (
<SelectProvider<OrganizePreviewModel> items={items}>
+1 -1
View File
@@ -1,10 +1,10 @@
import React, { useCallback, useEffect } from 'react';
import { useSelect } from 'App/Select/SelectContext';
import { OrganizePreviewModel } from 'App/State/OrganizePreviewAppState';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import { OrganizePreviewModel } from './useOrganizePreview';
import styles from './OrganizePreviewRow.css';
interface OrganizePreviewRowProps {
@@ -0,0 +1,33 @@
import ModelBase from 'App/ModelBase';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
export interface OrganizePreviewModel extends ModelBase {
seriesId: number;
seasonNumber: number;
episodeNumbers: number[];
episodeFileId: number;
existingPath: string;
newPath: string;
}
const DEFAULT_ORGANIZE_PREVIEW: OrganizePreviewModel[] = [];
const useOrganizePreview = (seriesId: number, seasonNumber?: number) => {
const queryParams: { seriesId: number; seasonNumber?: number } = { seriesId };
if (seasonNumber != null) {
queryParams.seasonNumber = seasonNumber;
}
const { data, ...result } = useApiQuery<OrganizePreviewModel[]>({
path: '/rename',
queryParams,
});
return {
items: data ?? DEFAULT_ORGANIZE_PREVIEW,
...result,
};
};
export default useOrganizePreview;
-2
View File
@@ -2,7 +2,6 @@ import * as captcha from './captchaActions';
import * as importSeries from './importSeriesActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions';
import * as providerOptions from './providerOptionActions';
import * as settings from './settingsActions';
@@ -11,7 +10,6 @@ export default [
importSeries,
interactiveImportActions,
oAuth,
organizePreview,
providerOptions,
settings
];
@@ -1,51 +0,0 @@
import { createAction } from 'redux-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'organizePreview';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_ORGANIZE_PREVIEW = 'organizePreview/fetchOrganizePreview';
export const CLEAR_ORGANIZE_PREVIEW = 'organizePreview/clearOrganizePreview';
//
// Action Creators
export const fetchOrganizePreview = createThunk(FETCH_ORGANIZE_PREVIEW);
export const clearOrganizePreview = createAction(CLEAR_ORGANIZE_PREVIEW);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_ORGANIZE_PREVIEW]: createFetchHandler('organizePreview', '/rename')
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_ORGANIZE_PREVIEW]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);
@@ -1,49 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles;
using Sonarr.Http;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Episodes
namespace Sonarr.Api.V5.Episodes;
[V5ApiController("rename")]
public class RenameEpisodeController : Controller
{
[V3ApiController("rename")]
public class RenameEpisodeController : Controller
private readonly IRenameEpisodeFileService _renameEpisodeFileService;
public RenameEpisodeController(IRenameEpisodeFileService renameEpisodeFileService)
{
private readonly IRenameEpisodeFileService _renameEpisodeFileService;
_renameEpisodeFileService = renameEpisodeFileService;
}
public RenameEpisodeController(IRenameEpisodeFileService renameEpisodeFileService)
[HttpGet]
[Produces("application/json")]
public List<RenameEpisodeResource> GetEpisodes(int seriesId, int? seasonNumber)
{
if (seasonNumber.HasValue)
{
_renameEpisodeFileService = renameEpisodeFileService;
return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber.Value).ToResource();
}
[HttpGet]
[Produces("application/json")]
public List<RenameEpisodeResource> GetEpisodes(int seriesId, int? seasonNumber)
{
if (seasonNumber.HasValue)
{
return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber.Value).ToResource();
}
return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource();
}
return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource();
[HttpGet("bulk")]
[Produces("application/json")]
public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds)
{
if (seriesIds is { Count: 0 })
{
throw new BadRequestException("seriesIds must be provided");
}
[HttpGet("bulk")]
[Produces("application/json")]
public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds)
if (seriesIds.Any(seriesId => seriesId <= 0))
{
if (seriesIds is { Count: 0 })
{
throw new BadRequestException("seriesIds must be provided");
}
if (seriesIds.Any(seriesId => seriesId <= 0))
{
throw new BadRequestException("seriesIds must be positive integers");
}
return _renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource();
throw new BadRequestException("seriesIds must be positive integers");
}
return _renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource();
}
}
@@ -1,43 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using Sonarr.Http.REST;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Episodes
namespace Sonarr.Api.V5.Episodes;
public class RenameEpisodeResource : RestResource
{
public class RenameEpisodeResource : RestResource
public int SeriesId { get; set; }
public int SeasonNumber { get; set; }
public List<int> EpisodeNumbers { get; set; } = [];
public int EpisodeFileId { get; set; }
public string? ExistingPath { get; set; }
public string? NewPath { get; set; }
}
public static class RenameEpisodeResourceMapper
{
public static RenameEpisodeResource ToResource(this NzbDrone.Core.MediaFiles.RenameEpisodeFilePreview model)
{
public int SeriesId { get; set; }
public int SeasonNumber { get; set; }
public List<int> EpisodeNumbers { get; set; }
public int EpisodeFileId { get; set; }
public string ExistingPath { get; set; }
public string NewPath { get; set; }
return new RenameEpisodeResource
{
Id = model.EpisodeFileId,
SeriesId = model.SeriesId,
SeasonNumber = model.SeasonNumber,
EpisodeNumbers = model.EpisodeNumbers.ToList(),
EpisodeFileId = model.EpisodeFileId,
ExistingPath = model.ExistingPath,
NewPath = model.NewPath
};
}
public static class RenameEpisodeResourceMapper
public static List<RenameEpisodeResource> ToResource(this IEnumerable<NzbDrone.Core.MediaFiles.RenameEpisodeFilePreview> models)
{
public static RenameEpisodeResource ToResource(this NzbDrone.Core.MediaFiles.RenameEpisodeFilePreview model)
{
if (model == null)
{
return null;
}
return new RenameEpisodeResource
{
Id = model.EpisodeFileId,
SeriesId = model.SeriesId,
SeasonNumber = model.SeasonNumber,
EpisodeNumbers = model.EpisodeNumbers.ToList(),
EpisodeFileId = model.EpisodeFileId,
ExistingPath = model.ExistingPath,
NewPath = model.NewPath
};
}
public static List<RenameEpisodeResource> ToResource(this IEnumerable<NzbDrone.Core.MediaFiles.RenameEpisodeFilePreview> models)
{
return models.Select(ToResource).ToList();
}
return models.Select(ToResource).ToList();
}
}