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

View File

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

View File

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

View File

@ -169,6 +169,10 @@ export const actionHandlers = handleThunks({
id,
path: item.path,
seriesId: item.series.id,
season: item.season,
episodeIds: item.episodes.map((e) => e.id),
quality: item.quality,
language: item.language,
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, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource);
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
@ -90,6 +91,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
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)
{
ImportDecision decision = null;
@ -150,14 +159,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
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)
{
try

View File

@ -9,11 +9,13 @@ using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
@ -21,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
public interface IManualImportService
{
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
@ -94,11 +96,32 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
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 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);
}
@ -162,7 +185,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{
try
{
DownloadClientItem downloadClientItem = null;
var trackedDownload = GetTrackedDownload(downloadId);
var relativeFile = baseFolder.GetRelativePath(file);
if (series == null)
@ -175,15 +198,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
series = _parsingService.GetSeries(relativeFile);
}
if (downloadId.IsNotNullOrWhiteSpace())
if (trackedDownload != null && series == null)
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
downloadClientItem = trackedDownload?.DownloadItem;
if (series == null)
{
series = trackedDownload?.RemoteEpisode?.Series;
}
series = trackedDownload?.RemoteEpisode?.Series;
}
if (series == null)
@ -209,7 +226,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
}
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> {file}, series,
downloadClientItem, null, SceneSource(series, baseFolder));
trackedDownload?.DownloadItem, null, SceneSource(series, baseFolder));
if (importDecisions.Any())
{
@ -236,6 +253,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
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)
{
var item = new ManualImportItem();

View File

@ -38,11 +38,14 @@ namespace Sonarr.Api.V3.ManualImport
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.Episodes = processedItem.Episodes.ToResource();
item.Rejections = processedItem.Rejections;
// Clear episode IDs in favour of the full episode
item.EpisodeIds = null;
}
return items;

View File

@ -1,5 +1,7 @@
using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.Episodes;
using Sonarr.Http.REST;
@ -11,6 +13,9 @@ namespace Sonarr.Api.V3.ManualImport
public int SeriesId { get; set; }
public int? SeasonNumber { 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 IEnumerable<Rejection> Rejections { get; set; }