Fixed: Various issues with unknown items in queue

pull/2900/head
Mark McDowall 6 years ago
parent 7e33261ccc
commit 21a92b62fd

@ -5,7 +5,7 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { icons } from 'Helpers/Props';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
@ -16,6 +16,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
@ -43,16 +44,18 @@ class Queue extends Component {
// before episodes start fetching or when episodes start fetching.
if (
(
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items)
) ||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.episodeId)
) {
return false;
}
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
return false;
}
return true;
}
@ -139,7 +142,7 @@ class Queue extends Component {
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const hasError = error || episodesError;
const selectedCount = this.getSelectedIds().length;
const disableSelectedActions = selectedCount === 0;
@ -173,6 +176,21 @@ class Queue extends Component {
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>

@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@ -71,6 +72,7 @@ class QueueRow extends Component {
errorMessage,
series,
episode,
language,
quality,
protocol,
indexer,
@ -204,6 +206,16 @@ class QueueRow extends Component {
);
}
if (name === 'language') {
return (
<TableRowCell key={name}>
<EpisodeLanguage
language={language}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
@ -340,6 +352,7 @@ QueueRow.propTypes = {
errorMessage: PropTypes.string,
series: PropTypes.object,
episode: PropTypes.object,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,

@ -1,10 +1,10 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import { icons, scrollDirections } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import Scroller from 'Components/Scroller/Scroller';
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TableHeader from './TableHeader';
import TableHeaderCell from './TableHeaderCell';
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell';
@ -25,34 +25,7 @@ function getTableHeaderCellProps(props) {
}, {});
}
class Table extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
function Table(props) {
const {
className,
selectAll,
@ -64,7 +37,7 @@ class Table extends Component {
onSortPress,
onTableOptionChange,
...otherProps
} = this.props;
} = props;
return (
<Scroller
@ -89,7 +62,10 @@ class Table extends Component {
return null;
}
if ((name === 'actions' || name === 'details') && onTableOptionChange) {
if (
(name === 'actions' || name === 'details') &&
onTableOptionChange
) {
return (
<TableHeaderCell
key={name}
@ -97,11 +73,18 @@ class Table extends Component {
name={name}
isSortable={false}
{...otherProps}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={optionsComponent}
pageSize={pageSize}
canModifyColumns={canModifyColumns}
onTableOptionChange={onTableOptionChange}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</TableOptionsModalWrapper>
</TableHeaderCell>
);
}
@ -119,25 +102,11 @@ class Table extends Component {
})
}
{
!!onTableOptionChange &&
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={optionsComponent}
pageSize={pageSize}
canModifyColumns={canModifyColumns}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
}
</TableHeader>
{children}
</table>
</Scroller>
);
}
}
Table.propTypes = {

@ -0,0 +1,61 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import TableOptionsModal from './TableOptionsModal';
class TableOptionsModalWrapper extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
const {
columns,
children,
...otherProps
} = this.props;
return (
<Fragment>
{
React.cloneElement(children, { onPress: this.onTableOptionsPress })
}
<TableOptionsModal
{...otherProps}
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
onModalClose={this.onTableOptionsModalClose}
/>
</Fragment>
);
}
}
TableOptionsModalWrapper.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
children: PropTypes.node.isRequired
};
export default TableOptionsModalWrapper;

@ -86,6 +86,7 @@ import {
faStop as fasStop,
faSync as fasSync,
faTags as fasTags,
faTable as fasTable,
faTh as fasTh,
faThList as fasThList,
faTrashAlt as fasTrashAlt,
@ -188,6 +189,7 @@ export const SORT_DESCENDING = fasSortDown;
export const SPINNER = fasSpinner;
export const SUBTRACT = fasMinus;
export const SYSTEM = fasLaptop;
export const TABLE = fasTable;
export const TAGS = fasTags;
export const TBA = fasQuestionCircle;
export const TEST = fasVial;

@ -1,48 +1,21 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import SeriesIndexTableOptionsConnector from './SeriesIndexTableOptionsConnector';
import styles from './SeriesIndexHeader.css';
class SeriesIndexHeader extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
function SeriesIndexHeader(props) {
const {
showBanners,
columns,
onTableOptionChange,
...otherProps
} = this.props;
} = props;
return (
<VirtualTableHeader>
@ -68,10 +41,16 @@ class SeriesIndexHeader extends Component {
isSortable={false}
{...otherProps}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={SeriesIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</TableOptionsModalWrapper>
</VirtualTableHeaderCell>
);
}
@ -92,17 +71,8 @@ class SeriesIndexHeader extends Component {
);
})
}
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={SeriesIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
</VirtualTableHeader>
);
}
}
SeriesIndexHeader.propTypes = {

@ -84,6 +84,12 @@ export const defaultState = {
isSortable: true,
isVisible: false
},
{
name: 'language',
label: 'Language',
isSortable: true,
isVisible: false
},
{
name: 'quality',
label: 'Quality',

@ -10,7 +10,11 @@ function createQueueItemSelector() {
}
return details.find((item) => {
if (item.episode) {
return item.episode.id === episodeId;
}
return false;
});
}
);

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
@ -124,11 +124,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
}
}
// Track it so it can be displayed in the queue even though we can't determine which serires it is for
if (trackedDownload.RemoteEpisode == null)
{
_logger.Trace("No Episode found for download '{0}', not tracking.", trackedDownload.DownloadItem.Title);
return null;
_logger.Trace("No Episode found for download '{0}'", trackedDownload.DownloadItem.Title);
}
}
catch (Exception e)

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Lifecycle;
@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles.Languages
List<LanguageProfile> All();
LanguageProfile Get(int id);
bool Exists(int id);
LanguageProfile GetDefaultProfile(string name, Language cutoff = null, params Language[] allowed);
}
public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent>
@ -66,6 +67,25 @@ namespace NzbDrone.Core.Profiles.Languages
return _profileRepository.Exists(id);
}
public LanguageProfile GetDefaultProfile(string name, Language cutoff = null, params Language[] allowed)
{
var orderedLanguages = Language.All
.Where(l => l != Language.Unknown)
.OrderByDescending(l => l.Name)
.ToList();
orderedLanguages.Insert(0, Language.Unknown);
var languages = orderedLanguages.Select(v => new LanguageProfileItem { Language = v, Allowed = false })
.ToList();
return new LanguageProfile
{
Cutoff = Language.Unknown,
Languages = languages
};
}
private LanguageProfile AddDefaultProfile(string name, Language cutoff, params Language[] allowed)
{
var languages = Language.All

@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
@ -13,6 +14,7 @@ namespace NzbDrone.Core.Queue
{
public Series Series { get; set; }
public Episode Episode { get; set; }
public Language Language { get; set; }
public QualityModel Quality { get; set; }
public decimal Size { get; set; }
public string Title { get; set; }

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Crypto;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Queue
@ -42,7 +44,7 @@ namespace NzbDrone.Core.Queue
private IEnumerable<Queue> MapQueue(TrackedDownload trackedDownload)
{
if (trackedDownload.RemoteEpisode.Episodes != null && trackedDownload.RemoteEpisode.Episodes.Any())
if (trackedDownload.RemoteEpisode?.Episodes != null && trackedDownload.RemoteEpisode.Episodes.Any())
{
foreach (var episode in trackedDownload.RemoteEpisode.Episodes)
{
@ -59,9 +61,10 @@ namespace NzbDrone.Core.Queue
{
var queue = new Queue
{
Series = trackedDownload.RemoteEpisode.Series,
Series = trackedDownload.RemoteEpisode?.Series,
Episode = episode,
Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality,
Language = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Language ?? Language.Unknown,
Quality = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Quality ?? new QualityModel(Quality.Unknown),
Title = Parser.Parser.RemoveFileExtension(trackedDownload.DownloadItem.Title),
Size = trackedDownload.DownloadItem.TotalSize,
Sizeleft = trackedDownload.DownloadItem.RemainingSize,

@ -1,4 +1,3 @@
using System.Linq;
using NzbDrone.Core.Profiles.Languages;
using Sonarr.Http;
@ -6,31 +5,18 @@ namespace Sonarr.Api.V3.Profiles.Language
{
public class LanguageProfileSchemaModule : SonarrRestModule<LanguageProfileResource>
{
private readonly LanguageProfileService _languageProfileService;
public LanguageProfileSchemaModule()
public LanguageProfileSchemaModule(LanguageProfileService languageProfileService)
: base("/languageprofile/schema")
{
_languageProfileService = languageProfileService;
GetResourceSingle = GetAll;
}
private LanguageProfileResource GetAll()
{
var orderedLanguages = NzbDrone.Core.Languages.Language.All
.Where(l => l != NzbDrone.Core.Languages.Language.Unknown)
.OrderByDescending(l => l.Name)
.ToList();
orderedLanguages.Insert(0, NzbDrone.Core.Languages.Language.Unknown);
var languages = orderedLanguages.Select(v => new LanguageProfileItem {Language = v, Allowed = false})
.ToList();
var profile = new LanguageProfile
{
Cutoff = NzbDrone.Core.Languages.Language.Unknown,
Languages = languages
};
var profile = _languageProfileService.GetDefaultProfile(string.Empty);
return profile.ToResource();
}
}

@ -1,11 +1,14 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles.Languages;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Queue;
using NzbDrone.SignalR;
using Sonarr.Http;
@ -18,18 +21,23 @@ namespace Sonarr.Api.V3.Queue
{
private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IConfigService _configService;
private readonly LanguageComparer LANGUAGE_COMPARER;
private readonly QualityModelComparer QUALITY_COMPARER;
public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage,
IQueueService queueService,
IPendingReleaseService pendingReleaseService,
IConfigService configService)
ILanguageProfileService languageProfileService,
QualityProfileService qualityProfileService)
: base(broadcastSignalRMessage)
{
_queueService = queueService;
_pendingReleaseService = pendingReleaseService;
_configService = configService;
GetResourcePaged = GetQueue;
LANGUAGE_COMPARER = new LanguageComparer(languageProfileService.GetDefaultProfile(string.Empty));
QUALITY_COMPARER = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty));
}
private PagingResource<QueueResource> GetQueue(PagingResource<QueueResource> pagingResource)
@ -55,38 +63,60 @@ namespace Sonarr.Api.V3.Queue
if (pagingSpec.SortKey == "episode")
{
ordered = ascending ? fullQueue.OrderBy(q => q.Episode.SeasonNumber).ThenBy(q => q.Episode.EpisodeNumber) :
fullQueue.OrderByDescending(q => q.Episode.SeasonNumber).ThenByDescending(q => q.Episode.EpisodeNumber);
ordered = ascending
? fullQueue.OrderBy(q => q.Episode?.SeasonNumber).ThenBy(q => q.Episode?.EpisodeNumber)
: fullQueue.OrderByDescending(q => q.Episode?.SeasonNumber)
.ThenByDescending(q => q.Episode?.EpisodeNumber);
}
else if (pagingSpec.SortKey == "timeleft")
{
ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) :
fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer());
ordered = ascending
? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer())
: fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer());
}
else if (pagingSpec.SortKey == "estimatedCompletionTime")
{
ordered = ascending ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) :
fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer());
ordered = ascending
? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer())
: fullQueue.OrderByDescending(q => q.EstimatedCompletionTime,
new EstimatedCompletionTimeComparer());
}
else if (pagingSpec.SortKey == "protocol")
{
ordered = ascending ? fullQueue.OrderBy(q => q.Protocol) :
fullQueue.OrderByDescending(q => q.Protocol);
ordered = ascending
? fullQueue.OrderBy(q => q.Protocol)
: fullQueue.OrderByDescending(q => q.Protocol);
}
else if (pagingSpec.SortKey == "indexer")
{
ordered = ascending ? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase) :
fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase);
ordered = ascending
? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase)
: fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase);
}
else if (pagingSpec.SortKey == "downloadClient")
{
ordered = ascending ? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase) :
fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase);
ordered = ascending
? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase)
: fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase);
}
else if (pagingSpec.SortKey == "language")
{
ordered = ascending
? fullQueue.OrderBy(q => q.Language, LANGUAGE_COMPARER)
: fullQueue.OrderByDescending(q => q.Language, LANGUAGE_COMPARER);
}
else if (pagingSpec.SortKey == "quality")
{
ordered = ascending
? fullQueue.OrderBy(q => q.Quality, QUALITY_COMPARER)
: fullQueue.OrderByDescending(q => q.Quality, QUALITY_COMPARER);
}
else
@ -113,13 +143,15 @@ namespace Sonarr.Api.V3.Queue
switch (pagingSpec.SortKey)
{
case "series.sortTitle":
return q => q.Series.SortTitle;
return q => q.Series?.SortTitle;
case "episode":
return q => q.Episode;
case "episode.airDateUtc":
return q => q.Episode.AirDateUtc;
case "episode.title":
return q => q.Episode.Title;
case "language":
return q => q.Language;
case "quality":
return q => q.Quality;
case "progress":

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.Episodes;
using Sonarr.Api.V3.Series;
@ -16,6 +17,7 @@ namespace Sonarr.Api.V3.Queue
public int? EpisodeId { get; set; }
public SeriesResource Series { get; set; }
public EpisodeResource Episode { get; set; }
public Language Language { get; set; }
public QualityModel Quality { get; set; }
public decimal Size { get; set; }
public string Title { get; set; }
@ -45,6 +47,7 @@ namespace Sonarr.Api.V3.Queue
EpisodeId = model.Episode?.Id,
Series = includeSeries && model.Series != null ? model.Series.ToResource() : null,
Episode = includeEpisode && model.Episode != null ? model.Episode.ToResource() : null,
Language = model.Language,
Quality = model.Quality,
Size = model.Size,
Title = model.Title,

Loading…
Cancel
Save