diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index 1c6985c99..47077c163 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -25,6 +25,7 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal' import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; +import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; @@ -51,6 +52,11 @@ const columns = [ label: 'Episode(s)', isVisible: true }, + { + name: 'releaseGroup', + label: 'Release Group', + isVisible: true + }, { name: 'quality', label: 'Quality', @@ -94,8 +100,9 @@ const SELECT = 'select'; const SERIES = 'series'; const SEASON = 'season'; const EPISODE = 'episode'; -const LANGUAGE = 'language'; +const RELEASE_GROUP = 'releaseGroup'; const QUALITY = 'quality'; +const LANGUAGE = 'language'; class InteractiveImportModalContent extends Component { @@ -231,8 +238,9 @@ class InteractiveImportModalContent extends Component { { key: SELECT, value: 'Select...', disabled: true }, { key: SEASON, value: 'Select Season' }, { key: EPISODE, value: 'Select Episode(s)' }, - { key: LANGUAGE, value: 'Select Language' }, - { key: QUALITY, value: 'Select Quality' } + { key: QUALITY, value: 'Select Quality' }, + { key: RELEASE_GROUP, value: 'Select Release Group' }, + { key: LANGUAGE, value: 'Select Language' } ]; if (allowSeriesChange) { @@ -400,6 +408,13 @@ class InteractiveImportModalContent extends Component { onModalClose={this.onSelectModalClose} /> + + e.id), + releaseGroup, quality, language, downloadId: this.props.downloadId diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index a9f244f80..7b3b1c0b4 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -16,6 +16,7 @@ import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; import styles from './InteractiveImportRow.css'; @@ -32,6 +33,7 @@ class InteractiveImportRow extends Component { isSelectSeriesModalOpen: false, isSelectSeasonModalOpen: false, isSelectEpisodeModalOpen: false, + isSelectReleaseGroupModalOpen: false, isSelectQualityModalOpen: false, isSelectLanguageModalOpen: false }; @@ -125,6 +127,10 @@ class InteractiveImportRow extends Component { this.setState({ isSelectEpisodeModalOpen: true }); } + onSelectReleaseGroupPress = () => { + this.setState({ isSelectReleaseGroupModalOpen: true }); + } + onSelectQualityPress = () => { this.setState({ isSelectQualityModalOpen: true }); } @@ -148,6 +154,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); @@ -171,6 +182,7 @@ class InteractiveImportRow extends Component { episodes, quality, language, + releaseGroup, size, rejections, isReprocessing, @@ -182,6 +194,7 @@ class InteractiveImportRow extends Component { isSelectSeriesModalOpen, isSelectSeasonModalOpen, isSelectEpisodeModalOpen, + isSelectReleaseGroupModalOpen, isSelectQualityModalOpen, isSelectLanguageModalOpen } = this.state; @@ -202,6 +215,7 @@ class InteractiveImportRow extends Component { const showSeriesPlaceholder = isSelected && !series; const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing; const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length; + const showReleaseGroupPlaceholder = isSelected && !releaseGroup; const showQualityPlaceholder = isSelected && !quality; const showLanguagePlaceholder = isSelected && !language; @@ -246,7 +260,6 @@ class InteractiveImportRow extends Component { /> : null } - + + { + showReleaseGroupPlaceholder ? + : + releaseGroup + } + + + + + + + ); + } +} + +SelectReleaseGroupModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectReleaseGroupModal; diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js new file mode 100644 index 000000000..a345e2a06 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContent.js @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +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 ( + + + Manual Import - Set Release Group + + + +
+ + Release Group + + + +
+
+ + + + + + +
+ ); + } +} + +SelectReleaseGroupModalContent.propTypes = { + releaseGroup: PropTypes.string.isRequired, + onReleaseGroupSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectReleaseGroupModalContent; diff --git a/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js new file mode 100644 index 000000000..17277bf7f --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseGroup/SelectReleaseGroupModalContentConnector.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { updateInteractiveImportItems, reprocessInteractiveImportItems } 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 ( + + ); + } +} + +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); diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index f967cae84..ee6b3f058 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -175,6 +175,7 @@ export const actionHandlers = handleThunks({ episodeIds: (item.episodes || []).map((e) => e.id), quality: item.quality, language: item.language, + releaseGroup: item.releaseGroup, downloadId: item.downloadId }; }); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index 849627d19..557276771 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } public Language Language { get; set; } + public string ReleaseGroup { get; set; } public string DownloadId { get; set; } public bool Equals(ManualImportFile other) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index d9990483d..70926e8d2 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List Episodes { get; set; } public QualityModel Quality { get; set; } public Language Language { get; set; } + public string ReleaseGroup { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 253fcbfcc..000ebdc38 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public interface IManualImportService { List GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, QualityModel quality, Language language); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, Language language); } public class ManualImportService : IExecute, IManualImportService @@ -96,7 +96,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles); } - public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, QualityModel quality, Language language) + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, Language language) { var rootFolder = Path.GetDirectoryName(path); var series = _seriesService.GetSeries(seriesId); @@ -115,6 +115,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual SceneSource = SceneSource(series, rootFolder), ExistingFile = series.Path.IsParentPath(path), Size = _diskProvider.GetFileSize(path), + ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, Language = language == Language.Unknown ? LanguageParser.ParseLanguage(path) : language, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality }; @@ -141,6 +142,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual SceneSource = SceneSource(series, rootFolder), ExistingFile = series.Path.IsParentPath(path), Size = _diskProvider.GetFileSize(path), + ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, Language = language == Language.Unknown ? LanguageParser.ParseLanguage(path) : language, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality }; @@ -250,7 +252,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { var localEpisode = new LocalEpisode(); localEpisode.Path = file; + localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); localEpisode.Quality = QualityParser.ParseQuality(file); + localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); localEpisode.Language = LanguageParser.ParseLanguage(file); localEpisode.Size = _diskProvider.GetFileSize(file); @@ -350,6 +354,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual } } + item.ReleaseGroup = decision.LocalEpisode.ReleaseGroup; item.Quality = decision.LocalEpisode.Quality; item.Language = decision.LocalEpisode.Language; item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); @@ -382,6 +387,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual Episodes = episodes, FileEpisodeInfo = fileEpisodeInfo, Path = file.Path, + ReleaseGroup = file.ReleaseGroup, Quality = file.Quality, Language = file.Language, Series = series, @@ -405,6 +411,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual // Apply the user-chosen values. localEpisode.Series = series; localEpisode.Episodes = episodes; + localEpisode.ReleaseGroup = file.ReleaseGroup; localEpisode.Quality = file.Quality; localEpisode.Language = file.Language; diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs index b560969af..ebcb1a30c 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Nancy; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Qualities; @@ -39,7 +40,7 @@ namespace Sonarr.Api.V3.ManualImport foreach (var item in items) { - var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List(), item.Quality, item.Language); + var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List(), item.ReleaseGroup, item.Quality, item.Language); item.SeasonNumber = processedItem.SeasonNumber; item.Episodes = processedItem.Episodes.ToResource(); @@ -56,6 +57,11 @@ namespace Sonarr.Api.V3.ManualImport item.Quality = processedItem.Quality; } + if (item.ReleaseGroup.IsNotNullOrWhiteSpace()) + { + item.ReleaseGroup = processedItem.ReleaseGroup; + } + // Clear episode IDs in favour of the full episode item.EpisodeIds = null; } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 0ebe3a464..bab8f5589 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -16,6 +16,7 @@ namespace Sonarr.Api.V3.ManualImport public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } public Language Language { get; set; } + public string ReleaseGroup { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index 593e98cee..ea6318545 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.ManualImport public SeriesResource Series { get; set; } public int? SeasonNumber { get; set; } public List Episodes { get; set; } + public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public Language Language { get; set; } public int QualityWeight { get; set; } @@ -45,6 +46,7 @@ namespace Sonarr.Api.V3.ManualImport Series = model.Series.ToResource(), SeasonNumber = model.SeasonNumber, Episodes = model.Episodes.ToResource(), + ReleaseGroup = model.ReleaseGroup, Quality = model.Quality, Language = model.Language, //QualityWeight