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 @@
+