1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-10-29 23:12:39 +01:00

Fixed: Various issues with unknown items in queue

This commit is contained in:
Mark McDowall 2019-01-09 18:11:37 -08:00
parent 7e33261ccc
commit 21a92b62fd
15 changed files with 330 additions and 242 deletions

View File

@ -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)
this.props.isFetching &&
nextProps.isPopulated &&
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>

View File

@ -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,

View File

@ -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,119 +25,88 @@ function getTableHeaderCellProps(props) {
}, {});
}
class Table extends Component {
function Table(props) {
const {
className,
selectAll,
columns,
optionsComponent,
pageSize,
canModifyColumns,
children,
onSortPress,
onTableOptionChange,
...otherProps
} = props;
//
// Lifecycle
return (
<Scroller
className={styles.tableContainer}
scrollDirection={scrollDirections.HORIZONTAL}
>
<table className={className}>
<TableHeader>
{
selectAll &&
<TableSelectAllHeaderCell {...otherProps} />
}
constructor(props, context) {
super(props, context);
{
columns.map((column) => {
const {
name,
isVisible
} = column;
this.state = {
isTableOptionsModalOpen: false
};
}
if (!isVisible) {
return null;
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
const {
className,
selectAll,
columns,
optionsComponent,
pageSize,
canModifyColumns,
children,
onSortPress,
onTableOptionChange,
...otherProps
} = this.props;
return (
<Scroller
className={styles.tableContainer}
scrollDirection={scrollDirections.HORIZONTAL}
>
<table className={className}>
<TableHeader>
{
selectAll &&
<TableSelectAllHeaderCell {...otherProps} />
}
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if ((name === 'actions' || name === 'details') && onTableOptionChange) {
return (
<TableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
if (
(name === 'actions' || name === 'details') &&
onTableOptionChange
) {
return (
<TableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
>
<TableOptionsModalWrapper
columns={columns}
optionsComponent={optionsComponent}
pageSize={pageSize}
canModifyColumns={canModifyColumns}
onTableOptionChange={onTableOptionChange}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</TableHeaderCell>
);
}
return (
<TableHeaderCell
key={column.name}
onSortPress={onSortPress}
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
</TableOptionsModalWrapper>
</TableHeaderCell>
);
})
}
}
{
!!onTableOptionChange &&
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={optionsComponent}
pageSize={pageSize}
canModifyColumns={canModifyColumns}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
}
return (
<TableHeaderCell
key={column.name}
onSortPress={onSortPress}
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
</TableHeaderCell>
);
})
}
</TableHeader>
{children}
</table>
</Scroller>
);
}
</TableHeader>
{children}
</table>
</Scroller>
);
}
Table.propTypes = {

View File

@ -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;

View File

@ -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;

View File

@ -1,108 +1,78 @@
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 {
function SeriesIndexHeader(props) {
const {
showBanners,
columns,
onTableOptionChange,
...otherProps
} = props;
//
// Lifecycle
return (
<VirtualTableHeader>
{
columns.map((column) => {
const {
name,
label,
isSortable,
isVisible
} = column;
constructor(props, context) {
super(props, context);
this.state = {
isTableOptionsModalOpen: false
};
}
//
// Listeners
onTableOptionsPress = () => {
this.setState({ isTableOptionsModalOpen: true });
}
onTableOptionsModalClose = () => {
this.setState({ isTableOptionsModalOpen: false });
}
//
// Render
render() {
const {
showBanners,
columns,
onTableOptionChange,
...otherProps
} = this.props;
return (
<VirtualTableHeader>
{
columns.map((column) => {
const {
name,
label,
isSortable,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={styles[name]}
name={name}
isSortable={false}
{...otherProps}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/>
</VirtualTableHeaderCell>
);
}
if (!isVisible) {
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
key={name}
className={classNames(
styles[name],
name === 'sortTitle' && showBanners && styles.banner
)}
className={styles[name]}
name={name}
isSortable={isSortable}
isSortable={false}
{...otherProps}
>
{label}
<TableOptionsModalWrapper
columns={columns}
optionsComponent={SeriesIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
/>
</TableOptionsModalWrapper>
</VirtualTableHeaderCell>
);
})
}
}
<TableOptionsModal
isOpen={this.state.isTableOptionsModalOpen}
columns={columns}
optionsComponent={SeriesIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
onModalClose={this.onTableOptionsModalClose}
/>
</VirtualTableHeader>
);
}
return (
<VirtualTableHeaderCell
key={name}
className={classNames(
styles[name],
name === 'sortTitle' && showBanners && styles.banner
)}
name={name}
isSortable={isSortable}
{...otherProps}
>
{label}
</VirtualTableHeaderCell>
);
})
}
</VirtualTableHeader>
);
}
SeriesIndexHeader.propTypes = {

View File

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

View File

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

View File

@ -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)

View File

@ -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
@ -92,4 +112,4 @@ namespace NzbDrone.Core.Profiles.Languages
AddDefaultProfile("English", Language.English, Language.English);
}
}
}
}

View File

@ -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; }

View File

@ -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,

View File

@ -1,4 +1,3 @@
using System.Linq;
using NzbDrone.Core.Profiles.Languages;
using Sonarr.Http;
@ -6,32 +5,19 @@ 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();
}
}
}
}

View File

@ -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":

View File

@ -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,