Add Tracklist to Album Modal

pull/94/head
Qstick 7 years ago
parent 90d9741056
commit d243a8c8c4

@ -188,7 +188,7 @@ EpisodeDetailsModalContent.propTypes = {
EpisodeDetailsModalContent.defaultProps = { EpisodeDetailsModalContent.defaultProps = {
selectedTab: 'details', selectedTab: 'details',
albumLabel: 'Unknown', albumLabel: ['Unknown'],
episodeEntity: episodeEntities.EPISODES, episodeEntity: episodeEntities.EPISODES,
startInteractiveSearch: false startInteractiveSearch: false
}; };

@ -7,6 +7,8 @@ import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';
import episodeEntities from 'Episode/episodeEntities'; import episodeEntities from 'Episode/episodeEntities';
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -34,6 +36,10 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
clearReleases, clearReleases,
fetchTracks,
clearTracks,
fetchEpisodeFiles,
clearEpisodeFiles,
toggleEpisodeMonitored toggleEpisodeMonitored
}; };
@ -41,14 +47,33 @@ class EpisodeDetailsModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() {
this._populate();
}
componentWillUnmount() { componentWillUnmount() {
// Clear pending releases here so we can reshow the search // Clear pending releases here so we can reshow the search
// results even after switching tabs. // results even after switching tabs.
this._unpopulate();
this.props.clearReleases(); this.props.clearReleases();
} }
//
// Control
_populate() {
const artistId = this.props.artistId;
const albumId = this.props.episodeId;
this.props.fetchTracks({ artistId, albumId });
// this.props.fetchEpisodeFiles({ artistId, albumId });
}
_unpopulate() {
this.props.clearTracks();
// this.props.clearEpisodeFiles();
}
// //
// Listeners // Listeners
@ -82,6 +107,10 @@ EpisodeDetailsModalContentConnector.propTypes = {
episodeId: PropTypes.number.isRequired, episodeId: PropTypes.number.isRequired,
episodeEntity: PropTypes.string.isRequired, episodeEntity: PropTypes.string.isRequired,
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
fetchTracks: PropTypes.func.isRequired,
clearTracks: PropTypes.func.isRequired,
fetchEpisodeFiles: PropTypes.func.isRequired,
clearEpisodeFiles: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired, clearReleases: PropTypes.func.isRequired,
toggleEpisodeMonitored: PropTypes.func.isRequired toggleEpisodeMonitored: PropTypes.func.isRequired
}; };

@ -118,7 +118,7 @@ function EpisodeStatus(props) {
EpisodeStatus.propTypes = { EpisodeStatus.propTypes = {
airDateUtc: PropTypes.string, airDateUtc: PropTypes.string,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool,
grabbed: PropTypes.bool, grabbed: PropTypes.bool,
queueItem: PropTypes.object, queueItem: PropTypes.object,
episodeFile: PropTypes.object episodeFile: PropTypes.object

@ -7,7 +7,10 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import Label from 'Components/Label'; import Label from 'Components/Label';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeQuality from 'Episode/EpisodeQuality';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import EpisodeAiringConnector from './EpisodeAiringConnector'; import EpisodeAiringConnector from './EpisodeAiringConnector';
import TrackDetailRow from './TrackDetailRow';
import styles from './EpisodeSummary.css'; import styles from './EpisodeSummary.css';
class EpisodeSummary extends Component { class EpisodeSummary extends Component {
@ -49,9 +52,11 @@ class EpisodeSummary extends Component {
releaseDate, releaseDate,
albumLabel, albumLabel,
path, path,
items,
size, size,
quality, quality,
qualityCutoffNotMet qualityCutoffNotMet,
columns
} = this.props; } = this.props;
const hasOverview = !!overview; const hasOverview = !!overview;
@ -88,53 +93,36 @@ class EpisodeSummary extends Component {
} }
</div> </div>
{ <div>
path && {
<div className={styles.files}> <div className={styles.episodes}>
<div className={styles.filesHeader}> {
<div className={styles.path}> items.length ?
Path <Table
</div> columns={columns}
>
<div className={styles.size}> <TableBody>
Size {
</div> items.map((item) => {
return (
<div className={styles.quality}> <TrackDetailRow
Quality key={item.id}
</div> columns={columns}
{...item}
<div className={styles.actions}></div> />
</div> );
})
<div className={styles.fileRow}> }
<div </TableBody>
className={styles.path} </Table> :
title={path}
> <div className={styles.noEpisodes}>
{path} No tracks in this group
</div> </div>
}
<div className={styles.size}>
{formatBytes(size)}
</div>
<div className={styles.quality}>
<EpisodeQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</div>
<div className={styles.actions}>
<IconButton
name={icons.REMOVE}
onPress={this.onRemoveEpisodeFilePress}
/>
</div>
</div>
</div> </div>
} }
</div>
<ConfirmModal <ConfirmModal
isOpen={this.state.isRemoveEpisodeFileModalOpen} isOpen={this.state.isRemoveEpisodeFileModalOpen}
@ -155,6 +143,8 @@ EpisodeSummary.propTypes = {
overview: PropTypes.string, overview: PropTypes.string,
albumLabel: PropTypes.arrayOf(PropTypes.string), albumLabel: PropTypes.arrayOf(PropTypes.string),
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
path: PropTypes.string, path: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
quality: PropTypes.object, quality: PropTypes.object,

@ -2,42 +2,28 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteEpisodeFile } from 'Store/Actions/episodeFileActions'; import { deleteEpisodeFile } from 'Store/Actions/episodeFileActions';
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
import createTrackSelector from 'Store/Selectors/createTrackSelector';
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import EpisodeSummary from './EpisodeSummary'; import EpisodeSummary from './EpisodeSummary';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createArtistSelector(), (state, { episode }) => episode,
(state) => state.tracks,
createEpisodeSelector(), createEpisodeSelector(),
createEpisodeFileSelector(), createCommandsSelector(),
(series, episode, episodeFile) => { createDimensionsSelector(),
const { (albumId, tracks, episode, commands, dimensions) => {
qualityProfileId,
network
} = series;
const {
airDateUtc,
overview
} = episode;
const {
path,
size,
quality,
qualityCutoffNotMet
} = episodeFile || {};
return { return {
network, network: episode.label,
qualityProfileId, qualityProfileId: episode.profileId,
airDateUtc, airDateUtc: episode.releaseDate,
overview, overview: episode.overview,
path, items: tracks.items,
size, columns: tracks.columns
quality,
qualityCutoffNotMet
}; };
} }
); );

@ -0,0 +1,26 @@
.title {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.monitored {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 42px;
}
.trackNumber {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.language,
.audio,
.video,
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import styles from './TrackDetailRow.css';
class TrackDetailRow extends Component {
//
// Lifecycle
//
// Listeners
//
// Render
render() {
const {
id,
title,
trackNumber,
duration,
columns,
trackFileId
} = this.props;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'trackNumber') {
return (
<TableRowCell
key={name}
className={styles.trackNumber}
>
{trackNumber}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell
key={name}
className={styles.title}
>
{title}
</TableRowCell>
);
}
if (name === 'duration') {
return (
<TableRowCell key={name}>
{
formatTimeSpan(duration)
}
</TableRowCell>
);
}
if (name === 'audioInfo') {
return (
<TableRowCell
key={name}
className={styles.audio}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO}
episodeFileId={trackFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
episodeId={id}
episodeFileId={trackFileId}
/>
</TableRowCell>
);
}
return null;
})
}
</TableRow>
);
}
}
TrackDetailRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
duration: PropTypes.number.isRequired,
trackFileId: PropTypes.number.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
trackNumber: PropTypes.number.isRequired
};
export default TrackDetailRow;

@ -18,7 +18,7 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'trackNumber', name: 'trackNumber',
label: 'Track Number', label: '#',
isVisible: true isVisible: true
}, },
{ {
@ -31,6 +31,16 @@ export const defaultState = {
label: 'Duration', label: 'Duration',
isVisible: true isVisible: true
}, },
{
name: 'audioInfo',
label: 'Audio Info',
isVisible: true
},
{
name: 'status',
label: 'Status',
isVisible: true
},
{ {
name: 'actions', name: 'actions',
columnLabel: 'Actions', columnLabel: 'Actions',

@ -14,6 +14,7 @@ namespace Lidarr.Api.V3.Albums
public string Title { get; set; } public string Title { get; set; }
public int ArtistId { get; set; } public int ArtistId { get; set; }
public List<string> AlbumLabel { get; set; } public List<string> AlbumLabel { get; set; }
public string ForeignAlbumId { get; set; }
public bool Monitored { get; set; } public bool Monitored { get; set; }
public string Path { get; set; } public string Path { get; set; }
public int ProfileId { get; set; } public int ProfileId { get; set; }
@ -42,6 +43,7 @@ namespace Lidarr.Api.V3.Albums
Id = model.Id, Id = model.Id,
ArtistId = model.ArtistId, ArtistId = model.ArtistId,
AlbumLabel = model.Label, AlbumLabel = model.Label,
ForeignAlbumId = model.ForeignAlbumId,
Path = model.Path, Path = model.Path,
ProfileId = model.ProfileId, ProfileId = model.ProfileId,
Monitored = model.Monitored, Monitored = model.Monitored,

@ -24,6 +24,7 @@ namespace Lidarr.Api.V3.TrackFiles
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
private readonly Logger _logger; private readonly Logger _logger;
@ -31,6 +32,7 @@ namespace Lidarr.Api.V3.TrackFiles
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IRecycleBinProvider recycleBinProvider, IRecycleBinProvider recycleBinProvider,
IArtistService artistService, IArtistService artistService,
IAlbumService albumService,
IUpgradableSpecification upgradableSpecification, IUpgradableSpecification upgradableSpecification,
Logger logger) Logger logger)
: base(signalRBroadcaster) : base(signalRBroadcaster)
@ -38,6 +40,7 @@ namespace Lidarr.Api.V3.TrackFiles
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_artistService = artistService; _artistService = artistService;
_albumService = albumService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
_logger = logger; _logger = logger;
@ -62,10 +65,11 @@ namespace Lidarr.Api.V3.TrackFiles
{ {
var artistIdQuery = Request.Query.ArtistId; var artistIdQuery = Request.Query.ArtistId;
var trackFileIdsQuery = Request.Query.TrackFileIds; var trackFileIdsQuery = Request.Query.TrackFileIds;
var albumIdQuery = Request.Query.AlbumId;
if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue) if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue && !albumIdQuery.HasValue)
{ {
throw new BadRequestException("artistId or trackFileIds must be provided"); throw new BadRequestException("artistId, albumId, or trackFileIds must be provided");
} }
if (artistIdQuery.HasValue) if (artistIdQuery.HasValue)
@ -76,6 +80,14 @@ namespace Lidarr.Api.V3.TrackFiles
return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification));
} }
if (albumIdQuery.HasValue)
{
int albumId = Convert.ToInt32(albumIdQuery.Value);
var album = _albumService.GetAlbum(albumId);
return _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id).ConvertAll(f => f.ToResource(album.Artist, _upgradableSpecification));
}
else else
{ {
string episodeFileIdsValue = trackFileIdsQuery.Value.ToString(); string episodeFileIdsValue = trackFileIdsQuery.Value.ToString();

@ -24,26 +24,28 @@ namespace Lidarr.Api.V3.Tracks
private List<TrackResource> GetEpisodes() private List<TrackResource> GetEpisodes()
{ {
var artistIdQuery = Request.Query.ArtistId; var artistIdQuery = Request.Query.ArtistId;
var albumIdQuery = Request.Query.AlbumId;
var trackIdsQuery = Request.Query.TrackIds; var trackIdsQuery = Request.Query.TrackIds;
if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue) if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue && !albumIdQuery.HasValue)
{ {
throw new BadRequestException("artistId or trackIds must be provided"); throw new BadRequestException("artistId or trackIds must be provided");
} }
if (artistIdQuery.HasValue) if (artistIdQuery.HasValue && !albumIdQuery.HasValue)
{ {
int artistId = Convert.ToInt32(artistIdQuery.Value); int artistId = Convert.ToInt32(artistIdQuery.Value);
var albumId = Request.Query.AlbumId.HasValue ? (int)Request.Query.AlbumId : (int?)null;
if (albumId.HasValue)
{
return MapToResource(_trackService.GetTracksByAlbum(artistId, albumId.Value), false, false);
}
return MapToResource(_trackService.GetTracksByArtist(artistId), false, false); return MapToResource(_trackService.GetTracksByArtist(artistId), false, false);
} }
if (albumIdQuery.HasValue)
{
int albumId = Convert.ToInt32(albumIdQuery.Value);
return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false);
}
string trackIdsValue = trackIdsQuery.Value.ToString(); string trackIdsValue = trackIdsQuery.Value.ToString();
var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -71,7 +71,7 @@ namespace NzbDrone.Core.MediaFiles
{ {
var artist = _artistService.GetArtist(artistId); var artist = _artistService.GetArtist(artistId);
var tracks = _trackService.GetTracksByAlbum(artistId, albumId); var tracks = _trackService.GetTracksByAlbum(albumId);
var files = _mediaFileService.GetFilesByAlbum(artistId, albumId); var files = _mediaFileService.GetFilesByAlbum(artistId, albumId);
return GetPreviews(artist, tracks, files) return GetPreviews(artist, tracks, files)

@ -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;
@ -57,7 +57,7 @@ namespace NzbDrone.Core.Music
foreach (var album in albums) foreach (var album in albums)
{ {
album.Monitored = monitored; album.Monitored = monitored;
var tracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); var tracks = _trackService.GetTracksByAlbum(album.Id);
foreach (var track in tracks) foreach (var track in tracks)
{ {
track.Monitored = monitored; track.Monitored = monitored;

@ -1,4 +1,4 @@
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music.Events; using NzbDrone.Core.Music.Events;
@ -37,7 +37,7 @@ namespace NzbDrone.Core.Music
album = _albumService.FindById(album.ForeignAlbumId); album = _albumService.FindById(album.ForeignAlbumId);
var existingTracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); var existingTracks = _trackService.GetTracksByAlbum(album.Id);
var updateList = new List<Track>(); var updateList = new List<Track>();
var newList = new List<Track>(); var newList = new List<Track>();

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music
{ {
Track Find(int artistId, int albumId, int trackNumber); Track Find(int artistId, int albumId, int trackNumber);
List<Track> GetTracks(int artistId); List<Track> GetTracks(int artistId);
List<Track> GetTracks(int artistId, int albumId); List<Track> GetTracksByAlbum(int albumId);
List<Track> GetTracksByFileId(int fileId); List<Track> GetTracksByFileId(int fileId);
List<Track> TracksWithFiles(int artistId); List<Track> TracksWithFiles(int artistId);
PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec); PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec);
@ -51,10 +51,9 @@ namespace NzbDrone.Core.Music
return Query.Where(s => s.ArtistId == artistId).ToList(); return Query.Where(s => s.ArtistId == artistId).ToList();
} }
public List<Track> GetTracks(int artistId, int albumId) public List<Track> GetTracksByAlbum(int albumId)
{ {
return Query.Where(s => s.ArtistId == artistId) return Query.Where(s => s.AlbumId == albumId)
.AndWhere(s => s.AlbumId == albumId)
.ToList(); .ToList();
} }

@ -1,4 +1,4 @@
using NLog; using NLog;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music
Track FindTrack(int artistId, int albumId, int trackNumber); Track FindTrack(int artistId, int albumId, int trackNumber);
Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); Track FindTrackByTitle(int artistId, int albumId, string releaseTitle);
List<Track> GetTracksByArtist(int artistId); List<Track> GetTracksByArtist(int artistId);
List<Track> GetTracksByAlbum(int artistId, int albumId); List<Track> GetTracksByAlbum(int albumId);
//List<Track> GetTracksByAlbumTitle(string artistId, string albumTitle); //List<Track> GetTracksByAlbumTitle(string artistId, string albumTitle);
List<Track> TracksWithFiles(int artistId); List<Track> TracksWithFiles(int artistId);
//PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec); //PagingSpec<Track> TracksWithoutFiles(PagingSpec<Track> pagingSpec);
@ -70,16 +70,16 @@ namespace NzbDrone.Core.Music
return _trackRepository.GetTracks(artistId).ToList(); return _trackRepository.GetTracks(artistId).ToList();
} }
public List<Track> GetTracksByAlbum(int artistId, int albumId) public List<Track> GetTracksByAlbum(int albumId)
{ {
return _trackRepository.GetTracks(artistId, albumId); return _trackRepository.GetTracksByAlbum(albumId);
} }
public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle)
{ {
// TODO: can replace this search mechanism with something smarter/faster/better // TODO: can replace this search mechanism with something smarter/faster/better
var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " ");
var tracks = _trackRepository.GetTracks(artistId, albumId); var tracks = _trackRepository.GetTracksByAlbum(albumId);
var matches = tracks.Select( var matches = tracks.Select(
track => new track => new

Loading…
Cancel
Save