diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 3a7b03fb6..cfdaa3998 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -19,11 +19,11 @@ function createMapStateToProps() { } const mapDispatchToProps = { - fetchInteractiveImportItems, - setInteractiveImportSort, - setInteractiveImportMode, - clearInteractiveImport, - executeCommand + dispatchFetchInteractiveImportItems: fetchInteractiveImportItems, + dispatchSetInteractiveImportSort: setInteractiveImportSort, + dispatchSetInteractiveImportMode: setInteractiveImportMode, + dispatchClearInteractiveImport: clearInteractiveImport, + dispatchExecuteCommand: executeCommand }; class InteractiveImportModalContentConnector extends Component { @@ -50,7 +50,7 @@ class InteractiveImportModalContentConnector extends Component { filterExistingFiles } = this.state; - this.props.fetchInteractiveImportItems({ + this.props.dispatchFetchInteractiveImportItems({ downloadId, folder, filterExistingFiles @@ -68,7 +68,7 @@ class InteractiveImportModalContentConnector extends Component { folder } = this.props; - this.props.fetchInteractiveImportItems({ + this.props.dispatchFetchInteractiveImportItems({ downloadId, folder, filterExistingFiles @@ -77,14 +77,14 @@ class InteractiveImportModalContentConnector extends Component { } componentWillUnmount() { - this.props.clearInteractiveImport(); + this.props.dispatchClearInteractiveImport(); } // // Listeners onSortPress = (sortKey, sortDirection) => { - this.props.setInteractiveImportSort({ sortKey, sortDirection }); + this.props.dispatchSetInteractiveImportSort({ sortKey, sortDirection }); } onFilterExistingFilesChange = (filterExistingFiles) => { @@ -92,7 +92,7 @@ class InteractiveImportModalContentConnector extends Component { } onImportModeChange = (importMode) => { - this.props.setInteractiveImportMode({ importMode }); + this.props.dispatchSetInteractiveImportMode({ importMode }); } onImportSelectedPress = (selected, importMode) => { @@ -139,7 +139,7 @@ class InteractiveImportModalContentConnector extends Component { path: item.path, folderName: item.folderName, seriesId: series.id, - episodeIds: _.map(episodes, 'id'), + episodeIds: episodes.map((e) => e.id), quality, language, downloadId: this.props.downloadId @@ -151,7 +151,7 @@ class InteractiveImportModalContentConnector extends Component { return; } - this.props.executeCommand({ + this.props.dispatchExecuteCommand({ name: commandNames.INTERACTIVE_IMPORT, files, importMode @@ -188,11 +188,11 @@ InteractiveImportModalContentConnector.propTypes = { folder: PropTypes.string, filterExistingFiles: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchInteractiveImportItems: PropTypes.func.isRequired, - setInteractiveImportSort: PropTypes.func.isRequired, - clearInteractiveImport: PropTypes.func.isRequired, - setInteractiveImportMode: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired, + dispatchFetchInteractiveImportItems: PropTypes.func.isRequired, + dispatchSetInteractiveImportSort: PropTypes.func.isRequired, + dispatchSetInteractiveImportMode: PropTypes.func.isRequired, + dispatchClearInteractiveImport: PropTypes.func.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css index b304538b0..98ab42d4a 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -1,18 +1,25 @@ .relativePath { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; + composes: cell from "~Components/Table/Cells/TableRowCell.css"; word-break: break-all; } .quality, .language { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; + composes: cell from "~Components/Table/Cells/TableRowCell.css"; text-align: center; } .label { - composes: label from '~Components/Label.css'; + composes: label from "~Components/Label.css"; cursor: pointer; } + +.reprocessing { + composes: loading from "~Components/Loading/LoadingIndicator.css"; + + margin-top: 0; + text-align: start; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 8e3063620..59a8a7f3b 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -4,6 +4,7 @@ import formatBytes from 'Utilities/Number/formatBytes'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; @@ -172,6 +173,7 @@ class InteractiveImportRow extends Component { language, size, rejections, + isReprocessing, isSelected, onSelectedChange } = this.props; @@ -189,7 +191,7 @@ class InteractiveImportRow extends Component { .join(', '); const showSeriesPlaceholder = isSelected && !series; - const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber); + const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing; const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length; const showQualityPlaceholder = isSelected && !quality; const showLanguagePlaceholder = isSelected && !language; @@ -227,6 +229,15 @@ class InteractiveImportRow extends Component { { showSeasonNumberPlaceholder ? : seasonNumber } + + { + isReprocessing && seasonNumber == null ? + : null + } + { - const series = _.find(this.props.items, { id: seriesId }); + const { + ids, + items, + dispatchUpdateInteractiveImportItem, + dispatchReprocessInteractiveImportItems, + onModalClose + } = this.props; - this.props.ids.forEach((id) => { - this.props.updateInteractiveImportItem({ + const series = items.find((s) => s.id === seriesId); + + ids.forEach((id) => { + dispatchUpdateInteractiveImportItem({ id, series, seasonNumber: undefined, @@ -49,7 +57,9 @@ class SelectSeriesModalContentConnector extends Component { }); }); - this.props.onModalClose(true); + dispatchReprocessInteractiveImportItems({ ids }); + + onModalClose(true); } // @@ -68,7 +78,8 @@ class SelectSeriesModalContentConnector extends Component { SelectSeriesModalContentConnector.propTypes = { ids: PropTypes.arrayOf(PropTypes.number).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - updateInteractiveImportItem: PropTypes.func.isRequired, + dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired, + dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 8981b44d0..a2b4c8bfa 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -8,7 +8,7 @@ import { sortDirections } from 'Helpers/Props'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; -import { set, update } from './baseActions'; +import { set, update, updateItem } from './baseActions'; // // Variables @@ -16,6 +16,8 @@ import { set, update } from './baseActions'; export const section = 'interactiveImport'; const episodesSection = `${section}.episodes`; +let abortCurrentRequest = null; +let currentIds = []; // // State @@ -49,6 +51,7 @@ export const defaultState = { episodes: { isFetching: false, + isReprocessing: false, isPopulated: false, error: null, sortKey: 'episodeNumber', @@ -66,6 +69,7 @@ export const persistState = [ // Actions Types export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/fetchInteractiveImportItems'; +export const REPROCESS_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/reprocessInteractiveImportItems'; export const SET_INTERACTIVE_IMPORT_SORT = 'interactiveImport/setInteractiveImportSort'; export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'interactiveImport/updateInteractiveImportItem'; export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteractiveImportItems'; @@ -82,6 +86,7 @@ export const CLEAR_INTERACTIVE_IMPORT_EPISODES = 'interactiveImport/clearInterac // Action Creators export const fetchInteractiveImportItems = createThunk(FETCH_INTERACTIVE_IMPORT_ITEMS); +export const reprocessInteractiveImportItems = createThunk(REPROCESS_INTERACTIVE_IMPORT_ITEMS); export const setInteractiveImportSort = createAction(SET_INTERACTIVE_IMPORT_SORT); export const updateInteractiveImportItem = createAction(UPDATE_INTERACTIVE_IMPORT_ITEM); export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPORT_ITEMS); @@ -133,6 +138,72 @@ export const actionHandlers = handleThunks({ }); }, + [REPROCESS_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) { + if (abortCurrentRequest) { + abortCurrentRequest(); + } + + dispatch(batchActions([ + ...currentIds.map((id) => updateItem({ + section, + id, + isReprocessing: false + })), + ...payload.ids.map((id) => updateItem({ + section, + id, + isReprocessing: true + })) + ])); + + const items = getState()[section].items; + + const requestPayload = payload.ids.map((id) => { + const item = items.find((i) => i.id === id); + + return { + id, + path: item.path, + seriesId: item.series.id, + downloadId: item.downloadId + }; + }); + + const { request, abortRequest } = createAjaxRequest({ + method: 'POST', + url: '/manualimport', + contentType: 'application/json', + data: JSON.stringify(requestPayload) + }); + + abortCurrentRequest = abortRequest; + currentIds = payload.ids; + + request.done((data) => { + dispatch(batchActions( + data.map((item) => updateItem({ + section, + ...item, + isReprocessing: false + })) + )); + }); + + request.fail((xhr) => { + if (xhr.aborted) { + return; + } + + dispatch(batchActions( + payload.ids.map((id) => updateItem({ + section, + id, + isReprocessing: false + })) + )); + }); + }, + [FETCH_INTERACTIVE_IMPORT_EPISODES]: createFetchHandler('interactiveImport.episodes', '/episode') }); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 659e0aeb1..7ea6a06dd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -10,7 +10,6 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; -using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; @@ -22,6 +21,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public interface IManualImportService { List GetMediaFiles(string path, string downloadId, bool filterExistingFiles); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId); } public class ManualImportService : IExecute, IManualImportService @@ -32,7 +32,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual private readonly IMakeImportDecision _importDecisionMaker; private readonly ISeriesService _seriesService; private readonly IEpisodeService _episodeService; - private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly IAggregationService _aggregationService; private readonly ITrackedDownloadService _trackedDownloadService; @@ -46,7 +45,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual IMakeImportDecision importDecisionMaker, ISeriesService seriesService, IEpisodeService episodeService, - IVideoFileInfoReader videoFileInfoReader, IAggregationService aggregationService, IImportApprovedEpisodes importApprovedEpisodes, ITrackedDownloadService trackedDownloadService, @@ -60,7 +58,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _importDecisionMaker = importDecisionMaker; _seriesService = seriesService; _episodeService = episodeService; - _videoFileInfoReader = videoFileInfoReader; _aggregationService = aggregationService; _importApprovedEpisodes = importApprovedEpisodes; _trackedDownloadService = trackedDownloadService; @@ -97,6 +94,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return ProcessFolder(path, path, downloadId, filterExistingFiles); } + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId) + { + var rootFolder = Path.GetDirectoryName(path); + var series = _seriesService.GetSeries(seriesId); + + return ProcessFile(rootFolder, rootFolder, path, downloadId, series); + } + private List ProcessFolder(string rootFolder, string baseFolder, string downloadId, bool filterExistingFiles) { DownloadClientItem downloadClientItem = null; @@ -139,11 +144,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList(); } - private ManualImportItem ProcessFile(string rootFolder, string baseFolder, string file, string downloadId) + private ManualImportItem ProcessFile(string rootFolder, string baseFolder, string file, string downloadId, Series series = null) { DownloadClientItem downloadClientItem = null; var relativeFile = baseFolder.GetRelativePath(file); - var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + + if (series == null) + { + _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + } if (series == null) { diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs index f3b189f40..7203365b1 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; +using Nancy; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Episodes; using Sonarr.Http; using Sonarr.Http.Extensions; @@ -17,6 +19,7 @@ namespace Sonarr.Api.V3.ManualImport _manualImportService = manualImportService; GetResourceAll = GetMediaFiles; + Post["/"] = x => ReprocessItems(); } private List GetMediaFiles() @@ -28,6 +31,21 @@ namespace Sonarr.Api.V3.ManualImport return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } + private Response ReprocessItems() + { + var items = Request.Body.FromJson>(); + + foreach (var item in items) + { + var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId); + + item.SeasonNumber = processedItem.SeasonNumber; + item.Episodes = processedItem.Episodes.ToResource(); + } + + return items.AsResponse(); + } + private ManualImportResource AddQualityWeight(ManualImportResource item) { if (item.Quality != null) diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs new file mode 100644 index 000000000..e0199cadb --- /dev/null +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Sonarr.Api.V3.Episodes; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.ManualImport +{ + public class ManualImportReprocessResource : RestResource + { + public string Path { get; set; } + public int SeriesId { get; set; } + public int? SeasonNumber { get; set; } + public List Episodes { get; set; } + public string DownloadId { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 212615ecf..5f5f99b9a 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -93,6 +93,7 @@ +