Fixed: Manual Import language handling

This commit is contained in:
Qstick 2021-12-24 11:04:27 -06:00
parent 974e44ce48
commit d1fa92bc6c
15 changed files with 328 additions and 50 deletions

View File

@ -19,6 +19,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
@ -40,6 +41,11 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
isVisible: true
},
{
name: 'quality',
label: translate('Quality'),
@ -83,6 +89,7 @@ const SELECT = 'select';
const MOVIE = 'movie';
const LANGUAGE = 'language';
const QUALITY = 'quality';
const RELEASE_GROUP = 'releaseGroup';
class InteractiveImportModalContent extends Component {
@ -202,10 +209,11 @@ class InteractiveImportModalContent extends Component {
const errorMessage = getErrorMessage(error, translate('UnableToLoadManualImportItems'));
const bulkSelectOptions = [
{
key: SELECT, value: translate('SelectDotDot'), disabled: true },
{ key: SELECT, value: translate('SelectDotDot'), disabled: true },
{ key: LANGUAGE, value: translate('SelectLanguage') },
{ key: QUALITY, value: translate('SelectQuality') }];
{ key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
];
if (allowMovieChange) {
bulkSelectOptions.splice(1, 0, {
@ -372,6 +380,13 @@ class InteractiveImportModalContent extends Component {
real={false}
onModalClose={this.onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === RELEASE_GROUP}
ids={selectedIds}
releaseGroup=""
onModalClose={this.onSelectModalClose}
/>
</ModalContent>
);
}

View File

@ -110,7 +110,8 @@ class InteractiveImportModalContentConnector extends Component {
const {
movie,
quality,
languages
languages,
releaseGroup
} = item;
if (!movie) {
@ -132,6 +133,7 @@ class InteractiveImportModalContentConnector extends Component {
path: item.path,
folderName: item.folderName,
movieId: movie.id,
releaseGroup,
quality,
languages,
downloadId: this.props.downloadId

View File

@ -11,6 +11,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatBytes from 'Utilities/Number/formatBytes';
@ -28,6 +29,7 @@ class InteractiveImportRow extends Component {
this.state = {
isSelectMovieModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false
};
@ -103,6 +105,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectMovieModalOpen: true });
}
onSelectReleaseGroupPress = () => {
this.setState({ isSelectReleaseGroupModalOpen: true });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
@ -116,6 +122,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
}
onSelectReleaseGroupModalClose = (changed) => {
this.setState({ isSelectReleaseGroupModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
@ -137,6 +148,7 @@ class InteractiveImportRow extends Component {
movie,
quality,
languages,
releaseGroup,
size,
rejections,
isReprocessing,
@ -147,7 +159,8 @@ class InteractiveImportRow extends Component {
const {
isSelectMovieModalOpen,
isSelectQualityModalOpen,
isSelectLanguageModalOpen
isSelectLanguageModalOpen,
isSelectReleaseGroupModalOpen
} = this.state;
const movieTitle = movie ? movie.title + ( movie.year > 0 ? ` (${movie.year})` : '') : '';
@ -155,6 +168,7 @@ class InteractiveImportRow extends Component {
const showMoviePlaceholder = isSelected && !movie;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages && !isReprocessing;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
return (
<TableRow>
@ -181,6 +195,17 @@ class InteractiveImportRow extends Component {
}
</TableRowCellButton>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={this.onSelectReleaseGroupPress}
>
{
showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> :
releaseGroup
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
@ -268,6 +293,13 @@ class InteractiveImportRow extends Component {
onModalClose={this.onSelectMovieModalClose}
/>
<SelectReleaseGroupModal
isOpen={isSelectReleaseGroupModalOpen}
ids={[id]}
releaseGroup={releaseGroup ?? ''}
onModalClose={this.onSelectReleaseGroupModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
@ -296,6 +328,7 @@ InteractiveImportRow.propTypes = {
movie: PropTypes.object,
quality: PropTypes.object,
languages: PropTypes.arrayOf(PropTypes.object),
releaseGroup: PropTypes.string,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isReprocessing: PropTypes.bool,

View File

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector';
class SelectReleaseGroupModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectReleaseGroupModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectReleaseGroupModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModal;

View File

@ -0,0 +1,99 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class SelectReleaseGroupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
releaseGroup
} = props;
this.state = {
releaseGroup
};
}
//
// Listeners
onReleaseGroupChange = ({ value }) => {
this.setState({ releaseGroup: value });
}
onReleaseGroupSelect = () => {
this.props.onReleaseGroupSelect(this.state);
}
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
releaseGroup
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImportSetReleaseGroup')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
onChange={this.onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onReleaseGroupSelect}
>
Set Release Group
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectReleaseGroupModalContent.propTypes = {
releaseGroup: PropTypes.string.isRequired,
onReleaseGroupSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModalContent;

View File

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
};
class SelectReleaseGroupModalContentConnector extends Component {
//
// Listeners
onReleaseGroupSelect = ({ releaseGroup }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
releaseGroup
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectReleaseGroupModalContent
{...this.props}
onReleaseGroupSelect={this.onReleaseGroupSelect}
/>
);
}
}
SelectReleaseGroupModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);

View File

@ -148,9 +148,10 @@ export const actionHandlers = handleThunks({
return {
id,
path: item.path,
movieId: item.movie.id,
movieId: item.movie ? item.movie.id : undefined,
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
downloadId: item.downloadId
};
});

View File

@ -128,6 +128,7 @@
"ClickToChangeLanguage": "Click to change language",
"ClickToChangeMovie": "Click to change movie",
"ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group",
"ClientPriority": "Client Priority",
"CloneCustomFormat": "Clone Custom Format",
"CloneFormatTag": "Clone Format Tag",
@ -491,6 +492,7 @@
"ManualImportSelectLanguage": "Manual Import - Select Language",
"ManualImportSelectMovie": "Manual Import - Select Movie",
"ManualImportSelectQuality": " Manual Import - Select Quality",
"ManualImportSetReleaseGroup": "Manual Import - Set Release Group",
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
"MarkAsFailed": "Mark as Failed",
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
@ -881,6 +883,7 @@
"SelectLanguges": "Select Languages",
"SelectMovie": "Select Movie",
"SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group",
"SendAnonymousUsageData": "Send Anonymous Usage Data",
"SetPermissions": "Set Permissions",
"SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?",

View File

@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Aggregation.Aggregators
public LocalMovie Aggregate(LocalMovie localMovie, DownloadClientItem downloadClientItem, bool otherFiles)
{
var languages = new List<Language> { localMovie.Movie.OriginalLanguage ?? Language.Unknown };
var languages = new List<Language> { localMovie.Movie?.OriginalLanguage ?? Language.Unknown };
var languagesConfidence = Confidence.Default;
foreach (var augmentLanguage in _augmentLanguages)

View File

@ -12,6 +12,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
public string FolderName { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public string DownloadId { get; set; }
public int MovieId { get; set; }

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
public long Size { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
public Movie Movie { get; set; }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -22,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
public interface IManualImportService
{
List<ManualImportItem> GetMediaFiles(string path, string downloadId, int? movieId, bool filterExistingFiles);
ManualImportItem ReprocessItem(string path, string downloadId, int movieId, QualityModel quality, List<Language> languages);
ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages);
}
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -92,13 +93,21 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
return ProcessFolder(path, path, downloadId, movieId, filterExistingFiles);
}
public ManualImportItem ReprocessItem(string path, string downloadId, int movieId, QualityModel quality, List<Language> languages)
public ManualImportItem ReprocessItem(string path, string downloadId, int movieId, string releaseGroup, QualityModel quality, List<Language> languages)
{
var rootFolder = Path.GetDirectoryName(path);
var movie = _movieService.GetMovie(movieId);
var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem;
var languageParse = LanguageParser.ParseLanguages(path);
if (languageParse.Count <= 1 && languageParse.First() == Language.Unknown && movie != null)
{
languageParse = new List<Language> { movie.OriginalLanguage };
_logger.Debug("Language couldn't be parsed from release, fallback to movie original language: {0}", movie.OriginalLanguage.Name);
}
var localEpisode = new LocalMovie
{
Movie = movie,
@ -108,8 +117,9 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
SceneSource = SceneSource(movie, rootFolder),
ExistingFile = movie.Path.IsParentPath(path),
Size = _diskProvider.GetFileSize(path),
Languages = (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages,
Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality
Languages = (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages,
Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality,
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
};
return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null);
@ -118,6 +128,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolder, string downloadId, int? movieId, bool filterExistingFiles)
{
DownloadClientItem downloadClientItem = null;
var directoryInfo = new DirectoryInfo(baseFolder);
var movie = movieId.HasValue ?
@ -165,50 +176,58 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
private ManualImportItem ProcessFile(string rootFolder, string baseFolder, string file, string downloadId, Movie movie = null)
{
var trackedDownload = GetTrackedDownload(downloadId);
var relativeFile = baseFolder.GetRelativePath(file);
if (movie == null)
try
{
movie = _parsingService.GetMovie(relativeFile.Split('\\', '/')[0]);
}
var trackedDownload = GetTrackedDownload(downloadId);
var relativeFile = baseFolder.GetRelativePath(file);
if (movie == null)
{
movie = _parsingService.GetMovie(relativeFile);
}
if (trackedDownload != null && movie == null)
{
movie = trackedDownload?.RemoteMovie?.Movie;
}
if (movie == null)
{
var relativeParseInfo = Parser.Parser.ParseMoviePath(relativeFile);
if (relativeParseInfo != null)
if (movie == null)
{
movie = _movieService.FindByTitle(relativeParseInfo.PrimaryMovieTitle, relativeParseInfo.Year);
movie = _parsingService.GetMovie(relativeFile.Split('\\', '/')[0]);
}
if (movie == null)
{
movie = _parsingService.GetMovie(relativeFile);
}
if (trackedDownload != null && movie == null)
{
movie = trackedDownload?.RemoteMovie?.Movie;
}
if (movie == null)
{
var relativeParseInfo = Parser.Parser.ParseMoviePath(relativeFile);
if (relativeParseInfo != null)
{
movie = _movieService.FindByTitle(relativeParseInfo.PrimaryMovieTitle, relativeParseInfo.Year);
}
}
if (movie == null)
{
var localMovie = new LocalMovie();
localMovie.Path = file;
localMovie.FileMovieInfo = Parser.Parser.ParseMoviePath(file);
localMovie.DownloadClientMovieInfo = trackedDownload?.RemoteMovie?.ParsedMovieInfo;
localMovie = _aggregationService.Augment(localMovie, null, false);
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), rootFolder, downloadId, null);
}
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file }, movie, trackedDownload?.DownloadItem, null, SceneSource(movie, baseFolder));
if (importDecisions.Any())
{
return MapItem(importDecisions.First(), rootFolder, downloadId, null);
}
}
if (movie == null)
catch (Exception ex)
{
var localMovie = new LocalMovie();
localMovie.Path = file;
localMovie.Quality = QualityParser.ParseQuality(file);
localMovie.Languages = LanguageParser.ParseLanguages(file);
localMovie.Size = _diskProvider.GetFileSize(file);
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), rootFolder, downloadId, null);
}
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file }, movie, trackedDownload?.DownloadItem, null, SceneSource(movie, baseFolder));
if (importDecisions.Any())
{
return MapItem(importDecisions.First(), rootFolder, downloadId, null);
_logger.Warn(ex, "Failed to process file: {0}", file);
}
return new ManualImportItem
@ -231,6 +250,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
localEpisode.Path = file;
localEpisode.Quality = new QualityModel(Quality.Unknown);
localEpisode.Languages = new List<Language> { Language.Unknown };
localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file);
localEpisode.Size = _diskProvider.GetFileSize(file);
items.Add(MapItem(new ImportDecision(localEpisode), rootFolder, null, null));
@ -274,6 +294,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
item.Quality = decision.LocalMovie.Quality;
item.Size = _diskProvider.GetFileSize(decision.LocalMovie.Path);
item.Languages = decision.LocalMovie.Languages;
item.ReleaseGroup = decision.LocalMovie.ReleaseGroup;
item.Rejections = decision.Rejections;
return item;
@ -303,6 +324,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
Path = file.Path,
Quality = file.Quality,
Languages = file.Languages,
ReleaseGroup = file.ReleaseGroup,
Movie = movie,
Size = 0
};
@ -325,6 +347,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
localMovie.Movie = movie;
localMovie.Quality = file.Quality;
localMovie.Languages = file.Languages;
localMovie.ReleaseGroup = file.ReleaseGroup;
//TODO: Cleanup non-tracked downloads
var importDecision = new ImportDecision(localMovie);

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles.MovieImport.Manual;
using NzbDrone.Core.Qualities;
@ -30,7 +31,7 @@ namespace Radarr.Api.V3.ManualImport
{
foreach (var item in items)
{
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.MovieId, item.Quality, item.Languages);
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.MovieId, item.ReleaseGroup, item.Quality, item.Languages);
item.Movie = processedItem.Movie.ToResource(0);
item.Rejections = processedItem.Rejections;
@ -43,6 +44,11 @@ namespace Radarr.Api.V3.ManualImport
{
item.Quality = processedItem.Quality;
}
if (item.ReleaseGroup.IsNotNullOrWhiteSpace())
{
item.ReleaseGroup = processedItem.ReleaseGroup;
}
}
return items;

View File

@ -14,6 +14,7 @@ namespace Radarr.Api.V3.ManualImport
public MovieResource Movie { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }

View File

@ -20,6 +20,7 @@ namespace Radarr.Api.V3.ManualImport
public MovieResource Movie { get; set; }
public QualityModel Quality { get; set; }
public List<Language> Languages { get; set; }
public string ReleaseGroup { get; set; }
public int QualityWeight { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
@ -45,6 +46,7 @@ namespace Radarr.Api.V3.ManualImport
Movie = model.Movie.ToResource(0),
Quality = model.Quality,
Languages = model.Languages,
ReleaseGroup = model.ReleaseGroup,
//QualityWeight
DownloadId = model.DownloadId,