From d243a8c8c413e2d2b9c72eb94138d2d0a3189a11 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 24 Sep 2017 15:44:25 -0400 Subject: [PATCH] Add Tracklist to Album Modal --- .../src/Episode/EpisodeDetailsModalContent.js | 2 +- .../EpisodeDetailsModalContentConnector.js | 31 ++++- frontend/src/Episode/EpisodeStatus.js | 2 +- .../src/Episode/Summary/EpisodeSummary.js | 84 ++++++------ .../Summary/EpisodeSummaryConnector.js | 42 ++---- .../src/Episode/Summary/TrackDetailRow.css | 26 ++++ .../src/Episode/Summary/TrackDetailRow.js | 123 ++++++++++++++++++ frontend/src/Store/Reducers/trackReducers.js | 12 +- src/Lidarr.Api.V3/Albums/AlbumResource.cs | 2 + .../TrackFiles/TrackFileModule.cs | 16 ++- src/Lidarr.Api.V3/Tracks/TrackModule.cs | 18 +-- .../MediaFiles/RenameTrackFileService.cs | 4 +- .../Music/AlbumMonitoredService.cs | 4 +- .../Music/RefreshTrackService.cs | 4 +- src/NzbDrone.Core/Music/TrackRepository.cs | 9 +- src/NzbDrone.Core/Music/TrackService.cs | 10 +- 16 files changed, 284 insertions(+), 105 deletions(-) create mode 100644 frontend/src/Episode/Summary/TrackDetailRow.css create mode 100644 frontend/src/Episode/Summary/TrackDetailRow.js diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.js b/frontend/src/Episode/EpisodeDetailsModalContent.js index 15381fa20..45215cac0 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContent.js +++ b/frontend/src/Episode/EpisodeDetailsModalContent.js @@ -188,7 +188,7 @@ EpisodeDetailsModalContent.propTypes = { EpisodeDetailsModalContent.defaultProps = { selectedTab: 'details', - albumLabel: 'Unknown', + albumLabel: ['Unknown'], episodeEntity: episodeEntities.EPISODES, startInteractiveSearch: false }; diff --git a/frontend/src/Episode/EpisodeDetailsModalContentConnector.js b/frontend/src/Episode/EpisodeDetailsModalContentConnector.js index fb41d023b..86479b0b4 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContentConnector.js +++ b/frontend/src/Episode/EpisodeDetailsModalContentConnector.js @@ -7,6 +7,8 @@ import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions'; import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import episodeEntities from 'Episode/episodeEntities'; +import { fetchTracks, clearTracks } from 'Store/Actions/trackActions'; +import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; import EpisodeDetailsModalContent from './EpisodeDetailsModalContent'; function createMapStateToProps() { @@ -34,6 +36,10 @@ function createMapStateToProps() { const mapDispatchToProps = { clearReleases, + fetchTracks, + clearTracks, + fetchEpisodeFiles, + clearEpisodeFiles, toggleEpisodeMonitored }; @@ -41,14 +47,33 @@ class EpisodeDetailsModalContentConnector extends Component { // // Lifecycle + componentDidMount() { + this._populate(); + } componentWillUnmount() { // Clear pending releases here so we can reshow the search // results even after switching tabs. - + this._unpopulate(); 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 @@ -82,6 +107,10 @@ EpisodeDetailsModalContentConnector.propTypes = { episodeId: PropTypes.number.isRequired, episodeEntity: PropTypes.string.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, toggleEpisodeMonitored: PropTypes.func.isRequired }; diff --git a/frontend/src/Episode/EpisodeStatus.js b/frontend/src/Episode/EpisodeStatus.js index aa0d923bd..7bf28a1f5 100644 --- a/frontend/src/Episode/EpisodeStatus.js +++ b/frontend/src/Episode/EpisodeStatus.js @@ -118,7 +118,7 @@ function EpisodeStatus(props) { EpisodeStatus.propTypes = { airDateUtc: PropTypes.string, - monitored: PropTypes.bool.isRequired, + monitored: PropTypes.bool, grabbed: PropTypes.bool, queueItem: PropTypes.object, episodeFile: PropTypes.object diff --git a/frontend/src/Episode/Summary/EpisodeSummary.js b/frontend/src/Episode/Summary/EpisodeSummary.js index 2b51ba7cf..695f08462 100644 --- a/frontend/src/Episode/Summary/EpisodeSummary.js +++ b/frontend/src/Episode/Summary/EpisodeSummary.js @@ -7,7 +7,10 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import Label from 'Components/Label'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import EpisodeQuality from 'Episode/EpisodeQuality'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; import EpisodeAiringConnector from './EpisodeAiringConnector'; +import TrackDetailRow from './TrackDetailRow'; import styles from './EpisodeSummary.css'; class EpisodeSummary extends Component { @@ -49,9 +52,11 @@ class EpisodeSummary extends Component { releaseDate, albumLabel, path, + items, size, quality, - qualityCutoffNotMet + qualityCutoffNotMet, + columns } = this.props; const hasOverview = !!overview; @@ -88,53 +93,36 @@ class EpisodeSummary extends Component { } - { - path && -
-
-
- Path -
- -
- Size -
- -
- Quality -
- -
-
- -
-
- {path} -
- -
- {formatBytes(size)} -
- -
- -
- -
- -
-
+
+ { +
+ { + items.length ? + + + { + items.map((item) => { + return ( + + ); + }) + } + +
: + +
+ No tracks in this group +
+ }
- } + } +
episode, + (state) => state.tracks, createEpisodeSelector(), - createEpisodeFileSelector(), - (series, episode, episodeFile) => { - const { - qualityProfileId, - network - } = series; - - const { - airDateUtc, - overview - } = episode; - - const { - path, - size, - quality, - qualityCutoffNotMet - } = episodeFile || {}; - + createCommandsSelector(), + createDimensionsSelector(), + (albumId, tracks, episode, commands, dimensions) => { return { - network, - qualityProfileId, - airDateUtc, - overview, - path, - size, - quality, - qualityCutoffNotMet + network: episode.label, + qualityProfileId: episode.profileId, + airDateUtc: episode.releaseDate, + overview: episode.overview, + items: tracks.items, + columns: tracks.columns }; } ); diff --git a/frontend/src/Episode/Summary/TrackDetailRow.css b/frontend/src/Episode/Summary/TrackDetailRow.css new file mode 100644 index 000000000..7dacb6a1e --- /dev/null +++ b/frontend/src/Episode/Summary/TrackDetailRow.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/Episode/Summary/TrackDetailRow.js b/frontend/src/Episode/Summary/TrackDetailRow.js new file mode 100644 index 000000000..3db53e693 --- /dev/null +++ b/frontend/src/Episode/Summary/TrackDetailRow.js @@ -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 ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'trackNumber') { + return ( + + {trackNumber} + + ); + } + + if (name === 'title') { + return ( + + {title} + + ); + } + + if (name === 'duration') { + return ( + + { + formatTimeSpan(duration) + } + + ); + } + + if (name === 'audioInfo') { + return ( + + + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + return null; + }) + } + + ); + } +} + +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; diff --git a/frontend/src/Store/Reducers/trackReducers.js b/frontend/src/Store/Reducers/trackReducers.js index bf17b4d5d..3f684fd9d 100644 --- a/frontend/src/Store/Reducers/trackReducers.js +++ b/frontend/src/Store/Reducers/trackReducers.js @@ -18,7 +18,7 @@ export const defaultState = { columns: [ { name: 'trackNumber', - label: 'Track Number', + label: '#', isVisible: true }, { @@ -31,6 +31,16 @@ export const defaultState = { label: 'Duration', isVisible: true }, + { + name: 'audioInfo', + label: 'Audio Info', + isVisible: true + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, { name: 'actions', columnLabel: 'Actions', diff --git a/src/Lidarr.Api.V3/Albums/AlbumResource.cs b/src/Lidarr.Api.V3/Albums/AlbumResource.cs index 3d13d276a..ddfc8c5b1 100644 --- a/src/Lidarr.Api.V3/Albums/AlbumResource.cs +++ b/src/Lidarr.Api.V3/Albums/AlbumResource.cs @@ -14,6 +14,7 @@ namespace Lidarr.Api.V3.Albums public string Title { get; set; } public int ArtistId { get; set; } public List AlbumLabel { get; set; } + public string ForeignAlbumId { get; set; } public bool Monitored { get; set; } public string Path { get; set; } public int ProfileId { get; set; } @@ -42,6 +43,7 @@ namespace Lidarr.Api.V3.Albums Id = model.Id, ArtistId = model.ArtistId, AlbumLabel = model.Label, + ForeignAlbumId = model.ForeignAlbumId, Path = model.Path, ProfileId = model.ProfileId, Monitored = model.Monitored, diff --git a/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs b/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs index ab0ea125f..eab63474c 100644 --- a/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs +++ b/src/Lidarr.Api.V3/TrackFiles/TrackFileModule.cs @@ -24,6 +24,7 @@ namespace Lidarr.Api.V3.TrackFiles private readonly IMediaFileService _mediaFileService; private readonly IRecycleBinProvider _recycleBinProvider; private readonly IArtistService _artistService; + private readonly IAlbumService _albumService; private readonly IUpgradableSpecification _upgradableSpecification; private readonly Logger _logger; @@ -31,6 +32,7 @@ namespace Lidarr.Api.V3.TrackFiles IMediaFileService mediaFileService, IRecycleBinProvider recycleBinProvider, IArtistService artistService, + IAlbumService albumService, IUpgradableSpecification upgradableSpecification, Logger logger) : base(signalRBroadcaster) @@ -38,6 +40,7 @@ namespace Lidarr.Api.V3.TrackFiles _mediaFileService = mediaFileService; _recycleBinProvider = recycleBinProvider; _artistService = artistService; + _albumService = albumService; _upgradableSpecification = upgradableSpecification; _logger = logger; @@ -62,10 +65,11 @@ namespace Lidarr.Api.V3.TrackFiles { var artistIdQuery = Request.Query.ArtistId; 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) @@ -76,6 +80,14 @@ namespace Lidarr.Api.V3.TrackFiles 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 { string episodeFileIdsValue = trackFileIdsQuery.Value.ToString(); diff --git a/src/Lidarr.Api.V3/Tracks/TrackModule.cs b/src/Lidarr.Api.V3/Tracks/TrackModule.cs index ad3120515..2e67a0921 100644 --- a/src/Lidarr.Api.V3/Tracks/TrackModule.cs +++ b/src/Lidarr.Api.V3/Tracks/TrackModule.cs @@ -24,26 +24,28 @@ namespace Lidarr.Api.V3.Tracks private List GetEpisodes() { var artistIdQuery = Request.Query.ArtistId; + var albumIdQuery = Request.Query.AlbumId; 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"); } - if (artistIdQuery.HasValue) + if (artistIdQuery.HasValue && !albumIdQuery.HasValue) { 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); } + if (albumIdQuery.HasValue) + { + int albumId = Convert.ToInt32(albumIdQuery.Value); + + return MapToResource(_trackService.GetTracksByAlbum(albumId), false, false); + } + string trackIdsValue = trackIdsQuery.Value.ToString(); var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs index bea565262..8b1352d0d 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -71,7 +71,7 @@ namespace NzbDrone.Core.MediaFiles { var artist = _artistService.GetArtist(artistId); - var tracks = _trackService.GetTracksByAlbum(artistId, albumId); + var tracks = _trackService.GetTracksByAlbum(albumId); var files = _mediaFileService.GetFilesByAlbum(artistId, albumId); return GetPreviews(artist, tracks, files) diff --git a/src/NzbDrone.Core/Music/AlbumMonitoredService.cs b/src/NzbDrone.Core/Music/AlbumMonitoredService.cs index c19965f41..6ffb30971 100644 --- a/src/NzbDrone.Core/Music/AlbumMonitoredService.cs +++ b/src/NzbDrone.Core/Music/AlbumMonitoredService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Music foreach (var album in albums) { album.Monitored = monitored; - var tracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); + var tracks = _trackService.GetTracksByAlbum(album.Id); foreach (var track in tracks) { track.Monitored = monitored; diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs index e84ba318a..6e024c880 100644 --- a/src/NzbDrone.Core/Music/RefreshTrackService.cs +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Music album = _albumService.FindById(album.ForeignAlbumId); - var existingTracks = _trackService.GetTracksByAlbum(album.ArtistId, album.Id); + var existingTracks = _trackService.GetTracksByAlbum(album.Id); var updateList = new List(); var newList = new List(); diff --git a/src/NzbDrone.Core/Music/TrackRepository.cs b/src/NzbDrone.Core/Music/TrackRepository.cs index b260056fe..408c01598 100644 --- a/src/NzbDrone.Core/Music/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/TrackRepository.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; using System.Collections.Generic; using System.Linq; using NLog; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music { Track Find(int artistId, int albumId, int trackNumber); List GetTracks(int artistId); - List GetTracks(int artistId, int albumId); + List GetTracksByAlbum(int albumId); List GetTracksByFileId(int fileId); List TracksWithFiles(int artistId); PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); @@ -51,10 +51,9 @@ namespace NzbDrone.Core.Music return Query.Where(s => s.ArtistId == artistId).ToList(); } - public List GetTracks(int artistId, int albumId) + public List GetTracksByAlbum(int albumId) { - return Query.Where(s => s.ArtistId == artistId) - .AndWhere(s => s.AlbumId == albumId) + return Query.Where(s => s.AlbumId == albumId) .ToList(); } diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs index 76fc7a5ca..d58eb6fbf 100644 --- a/src/NzbDrone.Core/Music/TrackService.cs +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music Track FindTrack(int artistId, int albumId, int trackNumber); Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); List GetTracksByArtist(int artistId); - List GetTracksByAlbum(int artistId, int albumId); + List GetTracksByAlbum(int albumId); //List GetTracksByAlbumTitle(string artistId, string albumTitle); List TracksWithFiles(int artistId); //PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); @@ -70,16 +70,16 @@ namespace NzbDrone.Core.Music return _trackRepository.GetTracks(artistId).ToList(); } - public List GetTracksByAlbum(int artistId, int albumId) + public List GetTracksByAlbum(int albumId) { - return _trackRepository.GetTracks(artistId, albumId); + return _trackRepository.GetTracksByAlbum(albumId); } public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better var normalizedReleaseTitle = Parser.Parser.NormalizeEpisodeTitle(releaseTitle).Replace(".", " "); - var tracks = _trackRepository.GetTracks(artistId, albumId); + var tracks = _trackRepository.GetTracksByAlbum(albumId); var matches = tracks.Select( track => new