New: Reprocess items after selection in Manual Import

Closes #3818
This commit is contained in:
Mark McDowall 2020-10-12 10:49:35 -07:00
parent c871b3f948
commit f30ae69c10
8 changed files with 111 additions and 42 deletions

View File

@ -6,7 +6,8 @@ import {
updateInteractiveImportItem, updateInteractiveImportItem,
fetchInteractiveImportEpisodes, fetchInteractiveImportEpisodes,
setInteractiveImportEpisodesSort, setInteractiveImportEpisodesSort,
clearInteractiveImportEpisodes clearInteractiveImportEpisodes,
reprocessInteractiveImportItems
} from 'Store/Actions/interactiveImportActions'; } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import SelectEpisodeModalContent from './SelectEpisodeModalContent'; import SelectEpisodeModalContent from './SelectEpisodeModalContent';
@ -21,10 +22,11 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchInteractiveImportEpisodes, dispatchFetchInteractiveImportEpisodes: fetchInteractiveImportEpisodes,
setInteractiveImportEpisodesSort, dispatchSetInteractiveImportEpisodesSort: setInteractiveImportEpisodesSort,
clearInteractiveImportEpisodes, dispatchClearInteractiveImportEpisodes: clearInteractiveImportEpisodes,
updateInteractiveImportItem dispatchUpdateInteractiveImportItem: updateInteractiveImportItem,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
}; };
class SelectEpisodeModalContentConnector extends Component { class SelectEpisodeModalContentConnector extends Component {
@ -38,26 +40,28 @@ class SelectEpisodeModalContentConnector extends Component {
seasonNumber seasonNumber
} = this.props; } = this.props;
this.props.fetchInteractiveImportEpisodes({ seriesId, seasonNumber }); this.props.dispatchFetchInteractiveImportEpisodes({ seriesId, seasonNumber });
} }
componentWillUnmount() { componentWillUnmount() {
// This clears the episodes for the queue and hides the queue // This clears the episodes for the queue and hides the queue
// We'll need another place to store episodes for manual import // We'll need another place to store episodes for manual import
this.props.clearInteractiveImportEpisodes(); this.props.dispatchClearInteractiveImportEpisodes();
} }
// //
// Listeners // Listeners
onSortPress = (sortKey, sortDirection) => { onSortPress = (sortKey, sortDirection) => {
this.props.setInteractiveImportEpisodesSort({ sortKey, sortDirection }); this.props.dispatchSetInteractiveImportEpisodesSort({ sortKey, sortDirection });
} }
onEpisodesSelect = (episodeIds) => { onEpisodesSelect = (episodeIds) => {
const { const {
ids, ids,
items, items,
dispatchUpdateInteractiveImportItem,
dispatchReprocessInteractiveImportItems,
onModalClose onModalClose
} = this.props; } = this.props;
@ -78,12 +82,14 @@ class SelectEpisodeModalContentConnector extends Component {
const startingIndex = index * episodesPerFile; const startingIndex = index * episodesPerFile;
const episodes = sortedEpisodes.slice(startingIndex, startingIndex + episodesPerFile); const episodes = sortedEpisodes.slice(startingIndex, startingIndex + episodesPerFile);
this.props.updateInteractiveImportItem({ dispatchUpdateInteractiveImportItem({
id, id,
episodes episodes
}); });
}); });
dispatchReprocessInteractiveImportItems({ ids });
onModalClose(true); onModalClose(true);
} }
@ -106,10 +112,11 @@ SelectEpisodeModalContentConnector.propTypes = {
seriesId: PropTypes.number.isRequired, seriesId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired, seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchInteractiveImportEpisodes: PropTypes.func.isRequired, dispatchFetchInteractiveImportEpisodes: PropTypes.func.isRequired,
setInteractiveImportEpisodesSort: PropTypes.func.isRequired, dispatchSetInteractiveImportEpisodesSort: PropTypes.func.isRequired,
clearInteractiveImportEpisodes: PropTypes.func.isRequired, dispatchClearInteractiveImportEpisodes: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired, dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions'; import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions';
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectLanguageModalContent from './SelectLanguageModalContent'; import SelectLanguageModalContent from './SelectLanguageModalContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -30,7 +30,8 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchLanguageProfileSchema: fetchLanguageProfileSchema, dispatchFetchLanguageProfileSchema: fetchLanguageProfileSchema,
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
}; };
class SelectLanguageModalContentConnector extends Component { class SelectLanguageModalContentConnector extends Component {
@ -48,15 +49,23 @@ class SelectLanguageModalContentConnector extends Component {
// Listeners // Listeners
onLanguageSelect = ({ value }) => { onLanguageSelect = ({ value }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
const languageId = parseInt(value); const languageId = parseInt(value);
const language = _.find(this.props.items, const language = _.find(this.props.items,
(item) => item.language.id === languageId).language; (item) => item.language.id === languageId).language;
this.props.dispatchUpdateInteractiveImportItems({ dispatchUpdateInteractiveImportItems({
ids: this.props.ids, ids,
language language
}); });
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true); this.props.onModalClose(true);
} }
@ -81,6 +90,7 @@ SelectLanguageModalContentConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired, dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectQualityModalContent from './SelectQualityModalContent'; import SelectQualityModalContent from './SelectQualityModalContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -31,7 +31,8 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
}; };
class SelectQualityModalContentConnector extends Component { class SelectQualityModalContentConnector extends Component {
@ -49,6 +50,12 @@ class SelectQualityModalContentConnector extends Component {
// Listeners // Listeners
onQualitySelect = ({ qualityId, proper, real }) => { onQualitySelect = ({ qualityId, proper, real }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
const quality = _.find(this.props.items, const quality = _.find(this.props.items,
(item) => item.id === qualityId); (item) => item.id === qualityId);
@ -57,14 +64,16 @@ class SelectQualityModalContentConnector extends Component {
real: real ? 1 : 0 real: real ? 1 : 0
}; };
this.props.dispatchUpdateInteractiveImportItems({ dispatchUpdateInteractiveImportItems({
ids: this.props.ids, ids,
quality: { quality: {
quality, quality,
revision revision
} }
}); });
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true); this.props.onModalClose(true);
} }
@ -89,6 +98,7 @@ SelectQualityModalContentConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@ -169,6 +169,10 @@ export const actionHandlers = handleThunks({
id, id,
path: item.path, path: item.path,
seriesId: item.series.id, seriesId: item.series.id,
season: item.season,
episodeIds: item.episodes.map((e) => e.id),
quality: item.quality,
language: item.language,
downloadId: item.downloadId downloadId: item.downloadId
}; };
}); });

View File

@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series); List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource); List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles); List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles);
ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem);
} }
public class ImportDecisionMaker : IMakeImportDecision public class ImportDecisionMaker : IMakeImportDecision
@ -90,6 +91,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return decisions; return decisions;
} }
public ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode, downloadClientItem))
.Where(c => c != null);
return new ImportDecision(localEpisode, reasons.ToArray());
}
private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem, bool otherFiles) private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem, bool otherFiles)
{ {
ImportDecision decision = null; ImportDecision decision = null;
@ -150,14 +159,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return decision; return decision;
} }
private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode, downloadClientItem))
.Where(c => c != null);
return new ImportDecision(localEpisode, reasons.ToArray());
}
private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode, DownloadClientItem downloadClientItem) private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{ {
try try

View File

@ -9,11 +9,13 @@ using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
@ -21,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public interface IManualImportService public interface IManualImportService
{ {
List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles);
ManualImportItem ReprocessItem(string path, string downloadId, int seriesId); ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, List<int> episodeIds, QualityModel quality, Language language);
} }
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -94,11 +96,32 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles); return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles);
} }
public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId) public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, List<int> episodeIds, QualityModel quality, Language language)
{ {
var rootFolder = Path.GetDirectoryName(path); var rootFolder = Path.GetDirectoryName(path);
var series = _seriesService.GetSeries(seriesId); var series = _seriesService.GetSeries(seriesId);
if (episodeIds.Any())
{
var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem;
var localEpisode = new LocalEpisode
{
Series = series,
Episodes = _episodeService.GetEpisodes(episodeIds),
FileEpisodeInfo = Parser.Parser.ParsePath(path),
DownloadClientEpisodeInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title),
Path = path,
SceneSource = SceneSource(series, rootFolder),
ExistingFile = series.Path.IsParentPath(path),
Size = _diskProvider.GetFileSize(path),
Language = language,
Quality = quality
};
return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null);
}
return ProcessFile(rootFolder, rootFolder, path, downloadId, series); return ProcessFile(rootFolder, rootFolder, path, downloadId, series);
} }
@ -162,7 +185,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{ {
try try
{ {
DownloadClientItem downloadClientItem = null; var trackedDownload = GetTrackedDownload(downloadId);
var relativeFile = baseFolder.GetRelativePath(file); var relativeFile = baseFolder.GetRelativePath(file);
if (series == null) if (series == null)
@ -175,15 +198,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
series = _parsingService.GetSeries(relativeFile); series = _parsingService.GetSeries(relativeFile);
} }
if (downloadId.IsNotNullOrWhiteSpace()) if (trackedDownload != null && series == null)
{ {
var trackedDownload = _trackedDownloadService.Find(downloadId); series = trackedDownload?.RemoteEpisode?.Series;
downloadClientItem = trackedDownload?.DownloadItem;
if (series == null)
{
series = trackedDownload?.RemoteEpisode?.Series;
}
} }
if (series == null) if (series == null)
@ -209,7 +226,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
} }
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> {file}, series, var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> {file}, series,
downloadClientItem, null, SceneSource(series, baseFolder)); trackedDownload?.DownloadItem, null, SceneSource(series, baseFolder));
if (importDecisions.Any()) if (importDecisions.Any())
{ {
@ -236,6 +253,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder)); return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder));
} }
private TrackedDownload GetTrackedDownload(string downloadId)
{
if (downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
return trackedDownload;
}
return null;
}
private ManualImportItem MapItem(ImportDecision decision, string rootFolder, string downloadId, string folderName) private ManualImportItem MapItem(ImportDecision decision, string rootFolder, string downloadId, string folderName)
{ {
var item = new ManualImportItem(); var item = new ManualImportItem();

View File

@ -38,11 +38,14 @@ namespace Sonarr.Api.V3.ManualImport
foreach (var item in items) foreach (var item in items)
{ {
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId); var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.EpisodeIds ?? new List<int>(), item.Quality, item.Language);
item.SeasonNumber = processedItem.SeasonNumber; item.SeasonNumber = processedItem.SeasonNumber;
item.Episodes = processedItem.Episodes.ToResource(); item.Episodes = processedItem.Episodes.ToResource();
item.Rejections = processedItem.Rejections; item.Rejections = processedItem.Rejections;
// Clear episode IDs in favour of the full episode
item.EpisodeIds = null;
} }
return items; return items;

View File

@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Episodes;
using Sonarr.Http.REST; using Sonarr.Http.REST;
@ -11,6 +13,9 @@ namespace Sonarr.Api.V3.ManualImport
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
public List<EpisodeResource> Episodes { get; set; } public List<EpisodeResource> Episodes { get; set; }
public List<int> EpisodeIds { get; set; }
public QualityModel Quality { get; set; }
public Language Language { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; } public IEnumerable<Rejection> Rejections { get; set; }