mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-01-27 11:21:43 +02:00
New: Ability to forcibly grab a release from Interactive Search
Closes #395
This commit is contained in:
parent
70fb1551af
commit
8aecec507e
@ -90,6 +90,7 @@ const columns = [
|
|||||||
|
|
||||||
function InteractiveSearch(props) {
|
function InteractiveSearch(props) {
|
||||||
const {
|
const {
|
||||||
|
searchPayload,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
@ -164,6 +165,7 @@ function InteractiveSearch(props) {
|
|||||||
<InteractiveSearchRow
|
<InteractiveSearchRow
|
||||||
key={item.guid}
|
key={item.guid}
|
||||||
{...item}
|
{...item}
|
||||||
|
searchPayload={searchPayload}
|
||||||
longDateFormat={longDateFormat}
|
longDateFormat={longDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
onGrabPress={onGrabPress}
|
onGrabPress={onGrabPress}
|
||||||
@ -186,6 +188,7 @@ function InteractiveSearch(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearch.propTypes = {
|
InteractiveSearch.propTypes = {
|
||||||
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
|
@ -41,8 +41,8 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatch(action({ selectedFilterKey }));
|
dispatch(action({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onGrabPress(guid, indexerId) {
|
onGrabPress(payload) {
|
||||||
dispatch(releaseActions.grabRelease({ guid, indexerId }));
|
dispatch(releaseActions.grabRelease(payload));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
@ -42,6 +43,17 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|||||||
|
|
||||||
class InteractiveSearchRow extends Component {
|
class InteractiveSearchRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isConfirmGrabModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
@ -50,9 +62,37 @@ class InteractiveSearchRow extends Component {
|
|||||||
guid,
|
guid,
|
||||||
indexerId,
|
indexerId,
|
||||||
onGrabPress
|
onGrabPress
|
||||||
}= this.props;
|
} = this.props;
|
||||||
|
|
||||||
onGrabPress(guid, indexerId);
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmGrabPress = () => {
|
||||||
|
this.setState({ isConfirmGrabModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onGrabConfirm = () => {
|
||||||
|
this.setState({ isConfirmGrabModalOpen: false });
|
||||||
|
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
searchPayload,
|
||||||
|
onGrabPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
...searchPayload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onGrabCancel = () => {
|
||||||
|
this.setState({ isConfirmGrabModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -165,17 +205,24 @@ class InteractiveSearchRow extends Component {
|
|||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.download}>
|
<TableRowCell className={styles.download}>
|
||||||
{
|
<SpinnerIconButton
|
||||||
downloadAllowed &&
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
<SpinnerIconButton
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
isSpinning={isGrabbing}
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||||
isSpinning={isGrabbing}
|
/>
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isConfirmGrabModalOpen}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title="Grab Release"
|
||||||
|
message={`Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '${title}'?`}
|
||||||
|
confirmLabel="Grab"
|
||||||
|
onConfirm={this.onGrabConfirm}
|
||||||
|
onCancel={this.onGrabCancel}
|
||||||
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -205,6 +252,7 @@ InteractiveSearchRow.propTypes = {
|
|||||||
grabError: PropTypes.string,
|
grabError: PropTypes.string,
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
searchPayload: PropTypes.object.isRequired,
|
||||||
onGrabPress: PropTypes.func.isRequired
|
onGrabPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,12 +5,15 @@ using Nancy;
|
|||||||
using Nancy.ModelBinding;
|
using Nancy.ModelBinding;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Exceptions;
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch;
|
using NzbDrone.Core.IndexerSearch;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
using Sonarr.Http.Extensions;
|
using Sonarr.Http.Extensions;
|
||||||
using HttpStatusCode = System.Net.HttpStatusCode;
|
using HttpStatusCode = System.Net.HttpStatusCode;
|
||||||
@ -24,6 +27,9 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
private readonly IMakeDownloadDecision _downloadDecisionMaker;
|
||||||
private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision;
|
private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision;
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
|
private readonly ISeriesService _seriesService;
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
|
private readonly IParsingService _parsingService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private readonly ICached<RemoteEpisode> _remoteEpisodeCache;
|
private readonly ICached<RemoteEpisode> _remoteEpisodeCache;
|
||||||
@ -33,6 +39,9 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
IMakeDownloadDecision downloadDecisionMaker,
|
IMakeDownloadDecision downloadDecisionMaker,
|
||||||
IPrioritizeDownloadDecision prioritizeDownloadDecision,
|
IPrioritizeDownloadDecision prioritizeDownloadDecision,
|
||||||
IDownloadService downloadService,
|
IDownloadService downloadService,
|
||||||
|
ISeriesService seriesService,
|
||||||
|
IEpisodeService episodeService,
|
||||||
|
IParsingService parsingService,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
@ -41,6 +50,9 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
_downloadDecisionMaker = downloadDecisionMaker;
|
_downloadDecisionMaker = downloadDecisionMaker;
|
||||||
_prioritizeDownloadDecision = prioritizeDownloadDecision;
|
_prioritizeDownloadDecision = prioritizeDownloadDecision;
|
||||||
_downloadService = downloadService;
|
_downloadService = downloadService;
|
||||||
|
_seriesService = seriesService;
|
||||||
|
_episodeService = episodeService;
|
||||||
|
_parsingService = parsingService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
GetResourceAll = GetReleases;
|
GetResourceAll = GetReleases;
|
||||||
@ -66,6 +78,34 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (remoteEpisode.Series == null)
|
||||||
|
{
|
||||||
|
if (release.EpisodeId.HasValue)
|
||||||
|
{
|
||||||
|
var episode = _episodeService.GetEpisode(release.EpisodeId.Value);
|
||||||
|
|
||||||
|
remoteEpisode.Series = _seriesService.GetSeries(episode.SeriesId);
|
||||||
|
remoteEpisode.Episodes = new List<Episode> { episode };
|
||||||
|
}
|
||||||
|
else if (release.SeriesId.HasValue)
|
||||||
|
{
|
||||||
|
var series = _seriesService.GetSeries(release.SeriesId.Value);
|
||||||
|
var episodes = _parsingService.GetEpisodes(remoteEpisode.ParsedEpisodeInfo, series, true);
|
||||||
|
|
||||||
|
if (episodes.Empty())
|
||||||
|
{
|
||||||
|
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to parse episodes in the release");
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteEpisode.Series = series;
|
||||||
|
remoteEpisode.Episodes = episodes;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Unable to find matching series and episodes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_downloadService.DownloadReport(remoteEpisode);
|
_downloadService.DownloadReport(remoteEpisode);
|
||||||
}
|
}
|
||||||
catch (ReleaseDownloadException ex)
|
catch (ReleaseDownloadException ex)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
@ -61,6 +62,16 @@ namespace Sonarr.Api.V3.Indexers
|
|||||||
public bool IsAbsoluteNumbering { get; set; }
|
public bool IsAbsoluteNumbering { get; set; }
|
||||||
public bool IsPossibleSpecialEpisode { get; set; }
|
public bool IsPossibleSpecialEpisode { get; set; }
|
||||||
public bool Special { get; set; }
|
public bool Special { get; set; }
|
||||||
|
|
||||||
|
// Sent when queuing an unknown release
|
||||||
|
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
// [JsonIgnore]
|
||||||
|
public int? SeriesId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
// [JsonIgnore]
|
||||||
|
public int? EpisodeId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReleaseResourceMapper
|
public static class ReleaseResourceMapper
|
||||||
|
Loading…
x
Reference in New Issue
Block a user