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

View file

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

View file

@ -1,10 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React from 'react';
import { icons, scrollDirections } from 'Helpers/Props'; import { icons, scrollDirections } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Scroller from 'Components/Scroller/Scroller'; 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 TableHeader from './TableHeader';
import TableHeaderCell from './TableHeaderCell'; import TableHeaderCell from './TableHeaderCell';
import TableSelectAllHeaderCell from './TableSelectAllHeaderCell'; 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;
// return (
// Lifecycle <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 = { if (!isVisible) {
isTableOptionsModalOpen: false return null;
}; }
}
// if (
// Listeners (name === 'actions' || name === 'details') &&
onTableOptionChange
onTableOptionsPress = () => { ) {
this.setState({ isTableOptionsModalOpen: true }); return (
} <TableHeaderCell
key={name}
onTableOptionsModalClose = () => { className={styles[name]}
this.setState({ isTableOptionsModalOpen: false }); name={name}
} isSortable={false}
{...otherProps}
// >
// Render <TableOptionsModalWrapper
columns={columns}
render() { optionsComponent={optionsComponent}
const { pageSize={pageSize}
className, canModifyColumns={canModifyColumns}
selectAll, onTableOptionChange={onTableOptionChange}
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}
> >
<IconButton <IconButton
name={icons.ADVANCED_SETTINGS} name={icons.ADVANCED_SETTINGS}
onPress={this.onTableOptionsPress}
/> />
</TableHeaderCell> </TableOptionsModalWrapper>
);
}
return (
<TableHeaderCell
key={column.name}
onSortPress={onSortPress}
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
</TableHeaderCell> </TableHeaderCell>
); );
}) }
}
{ return (
!!onTableOptionChange && <TableHeaderCell
<TableOptionsModal key={column.name}
isOpen={this.state.isTableOptionsModalOpen} onSortPress={onSortPress}
columns={columns} {...getTableHeaderCellProps(otherProps)}
optionsComponent={optionsComponent} {...column}
pageSize={pageSize} >
canModifyColumns={canModifyColumns} {column.label}
onTableOptionChange={onTableOptionChange} </TableHeaderCell>
onModalClose={this.onTableOptionsModalClose} );
/> })
} }
</TableHeader> </TableHeader>
{children} {children}
</table> </table>
</Scroller> </Scroller>
); );
}
} }
Table.propTypes = { 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, faStop as fasStop,
faSync as fasSync, faSync as fasSync,
faTags as fasTags, faTags as fasTags,
faTable as fasTable,
faTh as fasTh, faTh as fasTh,
faThList as fasThList, faThList as fasThList,
faTrashAlt as fasTrashAlt, faTrashAlt as fasTrashAlt,
@ -188,6 +189,7 @@ export const SORT_DESCENDING = fasSortDown;
export const SPINNER = fasSpinner; export const SPINNER = fasSpinner;
export const SUBTRACT = fasMinus; export const SUBTRACT = fasMinus;
export const SYSTEM = fasLaptop; export const SYSTEM = fasLaptop;
export const TABLE = fasTable;
export const TAGS = fasTags; export const TAGS = fasTags;
export const TBA = fasQuestionCircle; export const TBA = fasQuestionCircle;
export const TEST = fasVial; export const TEST = fasVial;

View file

@ -1,108 +1,78 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; 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 SeriesIndexTableOptionsConnector from './SeriesIndexTableOptionsConnector';
import styles from './SeriesIndexHeader.css'; import styles from './SeriesIndexHeader.css';
class SeriesIndexHeader extends Component { function SeriesIndexHeader(props) {
const {
showBanners,
columns,
onTableOptionChange,
...otherProps
} = props;
// return (
// Lifecycle <VirtualTableHeader>
{
columns.map((column) => {
const {
name,
label,
isSortable,
isVisible
} = column;
constructor(props, context) { if (!isVisible) {
super(props, context); return null;
}
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 (name === 'actions') {
return ( return (
<VirtualTableHeaderCell <VirtualTableHeaderCell
key={name} key={name}
className={classNames( className={styles[name]}
styles[name],
name === 'sortTitle' && showBanners && styles.banner
)}
name={name} name={name}
isSortable={isSortable} isSortable={false}
{...otherProps} {...otherProps}
> >
{label}
<TableOptionsModalWrapper
columns={columns}
optionsComponent={SeriesIndexTableOptionsConnector}
onTableOptionChange={onTableOptionChange}
>
<IconButton
name={icons.ADVANCED_SETTINGS}
/>
</TableOptionsModalWrapper>
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
); );
}) }
}
<TableOptionsModal return (
isOpen={this.state.isTableOptionsModalOpen} <VirtualTableHeaderCell
columns={columns} key={name}
optionsComponent={SeriesIndexTableOptionsConnector} className={classNames(
onTableOptionChange={onTableOptionChange} styles[name],
onModalClose={this.onTableOptionsModalClose} name === 'sortTitle' && showBanners && styles.banner
/> )}
</VirtualTableHeader> name={name}
); isSortable={isSortable}
} {...otherProps}
>
{label}
</VirtualTableHeaderCell>
);
})
}
</VirtualTableHeader>
);
} }
SeriesIndexHeader.propTypes = { SeriesIndexHeader.propTypes = {

View file

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

View file

@ -10,7 +10,11 @@ function createQueueItemSelector() {
} }
return details.find((item) => { 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.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; 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) if (trackedDownload.RemoteEpisode == null)
{ {
_logger.Trace("No Episode found for download '{0}', not tracking.", trackedDownload.DownloadItem.Title); _logger.Trace("No Episode found for download '{0}'", trackedDownload.DownloadItem.Title);
return null;
} }
} }
catch (Exception e) catch (Exception e)

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles.Languages
List<LanguageProfile> All(); List<LanguageProfile> All();
LanguageProfile Get(int id); LanguageProfile Get(int id);
bool Exists(int id); bool Exists(int id);
LanguageProfile GetDefaultProfile(string name, Language cutoff = null, params Language[] allowed);
} }
public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent> public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent>
@ -66,6 +67,25 @@ namespace NzbDrone.Core.Profiles.Languages
return _profileRepository.Exists(id); 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) private LanguageProfile AddDefaultProfile(string name, Language cutoff, params Language[] allowed)
{ {
var languages = Language.All var languages = Language.All
@ -92,4 +112,4 @@ namespace NzbDrone.Core.Profiles.Languages
AddDefaultProfile("English", Language.English, Language.English); AddDefaultProfile("English", Language.English, Language.English);
} }
} }
} }

View file

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

View file

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

View file

@ -1,4 +1,3 @@
using System.Linq;
using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Languages;
using Sonarr.Http; using Sonarr.Http;
@ -6,32 +5,19 @@ namespace Sonarr.Api.V3.Profiles.Language
{ {
public class LanguageProfileSchemaModule : SonarrRestModule<LanguageProfileResource> public class LanguageProfileSchemaModule : SonarrRestModule<LanguageProfileResource>
{ {
private readonly LanguageProfileService _languageProfileService;
public LanguageProfileSchemaModule() public LanguageProfileSchemaModule(LanguageProfileService languageProfileService)
: base("/languageprofile/schema") : base("/languageprofile/schema")
{ {
_languageProfileService = languageProfileService;
GetResourceSingle = GetAll; GetResourceSingle = GetAll;
} }
private LanguageProfileResource GetAll() private LanguageProfileResource GetAll()
{ {
var orderedLanguages = NzbDrone.Core.Languages.Language.All var profile = _languageProfileService.GetDefaultProfile(string.Empty);
.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
};
return profile.ToResource(); return profile.ToResource();
} }
} }
} }

View file

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

View file

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