mirror of https://github.com/lidarr/Lidarr
New: Manually Edit/Override Album Release (#181)
* New: Manually Edit/Override Album Release * !fixup for comments, loading all albums instead of only artist albums * fixup! UI Cleanup lint issues * fixup! Remove AddAlbum service for now, fix refresh override selected release * fixup! Last one... to fix updating albums with custom release set Closes #109 Closes #129 Closes #128
This commit is contained in:
parent
74f433d4f0
commit
26ef43f302
|
@ -68,7 +68,7 @@ class BlacklistRow extends Component {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -103,7 +103,7 @@ class HistoryRow extends Component {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -134,7 +134,7 @@ class QueueRow extends Component {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
@ -145,7 +145,7 @@ class QueueRow extends Component {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -99,7 +99,7 @@ class AddNewArtist extends Component {
|
|||
className={styles.searchInput}
|
||||
name="artistLookup"
|
||||
value={term}
|
||||
placeholder="eg. Breaking Benjamin, lidarr:####"
|
||||
placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
|
@ -144,7 +144,7 @@ class AddNewArtist extends Component {
|
|||
!isFetching && !error && !items.length && !!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
||||
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
<div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
<div>
|
||||
<Link to="https://github.com/Lidarr/Lidarr/wiki/FAQ#why-cant-i-add-a-new-artist-when-i-know-the-musicbrainz-id">
|
||||
Why can't I find my artist?
|
||||
|
@ -157,7 +157,7 @@ class AddNewArtist extends Component {
|
|||
!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>It's easy to add a new artist, just start typing the name the artist you want to add.</div>
|
||||
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
<div>You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
|
||||
const coverPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
|
||||
|
||||
function findCover(images) {
|
||||
return _.find(images, { coverType: 'cover' });
|
||||
}
|
||||
|
||||
function getCoverUrl(cover, size) {
|
||||
if (cover) {
|
||||
if (cover.url.contains('lastWrite=') || (/^https?:/).test(cover.url)) {
|
||||
// Remove protocol
|
||||
let url = cover.url.replace(/^https?:/, '');
|
||||
url = url.replace('cover.jpg', `cover-${size}.jpg`);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumCover extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
||||
|
||||
const {
|
||||
images,
|
||||
size
|
||||
} = props;
|
||||
|
||||
const cover = findCover(images);
|
||||
|
||||
this.state = {
|
||||
pixelRatio,
|
||||
cover,
|
||||
coverUrl: getCoverUrl(cover, pixelRatio * size),
|
||||
isLoaded: false,
|
||||
hasError: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
images,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
cover,
|
||||
pixelRatio
|
||||
} = this.state;
|
||||
|
||||
const nextcover = findCover(images);
|
||||
|
||||
if (nextcover && (!cover || nextcover.url !== cover.url)) {
|
||||
this.setState({
|
||||
cover: nextcover,
|
||||
coverUrl: getCoverUrl(nextcover, pixelRatio * size),
|
||||
hasError: false
|
||||
// Don't reset isLoaded, as we want to immediately try to
|
||||
// show the new image, whether an image was shown previously
|
||||
// or the placeholder was shown.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onError = () => {
|
||||
this.setState({ hasError: true });
|
||||
}
|
||||
|
||||
onLoad = () => {
|
||||
this.setState({
|
||||
isLoaded: true,
|
||||
hasError: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
size,
|
||||
lazy,
|
||||
overflow
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
coverUrl,
|
||||
hasError,
|
||||
isLoaded
|
||||
} = this.state;
|
||||
|
||||
if (hasError || !coverUrl) {
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={coverPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
return (
|
||||
<LazyLoad
|
||||
height={size}
|
||||
offset={100}
|
||||
overflow={overflow}
|
||||
placeholder={
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={coverPlaceholder}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={coverUrl}
|
||||
onError={this.onError}
|
||||
/>
|
||||
</LazyLoad>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? coverUrl : coverPlaceholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCover.propTypes = {
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
lazy: PropTypes.bool.isRequired,
|
||||
overflow: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AlbumCover.defaultProps = {
|
||||
size: 250,
|
||||
lazy: true,
|
||||
overflow: false
|
||||
};
|
||||
|
||||
export default AlbumCover;
|
|
@ -37,7 +37,7 @@
|
|||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.openSeriesButton {
|
||||
.openArtistButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
|
|
|
@ -8,13 +8,11 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import AlbumSummaryConnector from './Summary/AlbumSummaryConnector';
|
||||
import AlbumHistoryConnector from './History/AlbumHistoryConnector';
|
||||
import AlbumSearchConnector from './Search/AlbumSearchConnector';
|
||||
import styles from './AlbumDetailsModalContent.css';
|
||||
|
||||
const tabs = [
|
||||
'details',
|
||||
'history',
|
||||
'search'
|
||||
];
|
||||
|
@ -45,14 +43,10 @@ class AlbumDetailsModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
albumId,
|
||||
albumEntity,
|
||||
artistId,
|
||||
artistName,
|
||||
nameSlug,
|
||||
albumLabel,
|
||||
foreignArtistId,
|
||||
artistMonitored,
|
||||
albumTitle,
|
||||
releaseDate,
|
||||
monitored,
|
||||
isSaving,
|
||||
showOpenArtistButton,
|
||||
|
@ -61,7 +55,7 @@ class AlbumDetailsModalContent extends Component {
|
|||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const artistLink = `/artist/${nameSlug}`;
|
||||
const artistLink = `/artist/${foreignArtistId}`;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
|
@ -96,13 +90,6 @@ class AlbumDetailsModalContent extends Component {
|
|||
<TabList
|
||||
className={styles.tabList}
|
||||
>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Details
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
|
@ -118,16 +105,6 @@ class AlbumDetailsModalContent extends Component {
|
|||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel className={styles.tabPanel}>
|
||||
<AlbumSummaryConnector
|
||||
albumId={albumId}
|
||||
albumEntity={albumEntity}
|
||||
releaseDate={releaseDate}
|
||||
albumLabel={albumLabel}
|
||||
artistId={artistId}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel className={styles.tabPanel}>
|
||||
<AlbumHistoryConnector
|
||||
albumId={albumId}
|
||||
|
@ -148,7 +125,7 @@ class AlbumDetailsModalContent extends Component {
|
|||
{
|
||||
showOpenArtistButton &&
|
||||
<Button
|
||||
className={styles.openSeriesButton}
|
||||
className={styles.openArtistButton}
|
||||
to={artistLink}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
|
@ -172,7 +149,7 @@ AlbumDetailsModalContent.propTypes = {
|
|||
albumEntity: PropTypes.string.isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistMonitored: PropTypes.bool.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
|
@ -187,7 +164,7 @@ AlbumDetailsModalContent.propTypes = {
|
|||
};
|
||||
|
||||
AlbumDetailsModalContent.defaultProps = {
|
||||
selectedTab: 'details',
|
||||
selectedTab: 'history',
|
||||
albumLabel: ['Unknown'],
|
||||
albumEntity: albumEntities.ALBUMS,
|
||||
startInteractiveSearch: false
|
||||
|
|
|
@ -17,13 +17,13 @@ function createMapStateToProps() {
|
|||
(album, artist) => {
|
||||
const {
|
||||
artistName,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
monitored: artistMonitored
|
||||
} = artist;
|
||||
|
||||
return {
|
||||
artistName,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
artistMonitored,
|
||||
...album
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import AlbumDetailsModal from './AlbumDetailsModal';
|
||||
import EditAlbumModalConnector from './Edit/EditAlbumModalConnector';
|
||||
import styles from './AlbumSearchCell.css';
|
||||
|
||||
class AlbumSearchCell extends Component {
|
||||
|
@ -16,7 +17,8 @@ class AlbumSearchCell extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
isDetailsModalOpen: false,
|
||||
isEditAlbumModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -31,6 +33,14 @@ class AlbumSearchCell extends Component {
|
|||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
onEditAlbumPress = () => {
|
||||
this.setState({ isEditAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAlbumModalClose = () => {
|
||||
this.setState({ isEditAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -57,6 +67,12 @@ class AlbumSearchCell extends Component {
|
|||
onPress={this.onManualSearchPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title="Edit Album"
|
||||
onPress={this.onEditAlbumPress}
|
||||
/>
|
||||
|
||||
<AlbumDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
albumId={albumId}
|
||||
|
@ -67,6 +83,13 @@ class AlbumSearchCell extends Component {
|
|||
onModalClose={this.onDetailsModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<EditAlbumModalConnector
|
||||
isOpen={this.state.isEditAlbumModalOpen}
|
||||
albumId={albumId}
|
||||
artistId={artistId}
|
||||
onModalClose={this.onEditAlbumModalClose}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
function AlbumTitleDetailLink({ foreignAlbumId, title }) {
|
||||
const link = `/album/${foreignAlbumId}`;
|
||||
|
||||
return (
|
||||
<Link to={link}>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
AlbumTitleDetailLink.propTypes = {
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default AlbumTitleDetailLink;
|
|
@ -0,0 +1,137 @@
|
|||
.innerContentBody {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.backdropOverlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $black;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
width: 250px;
|
||||
height: 97px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 5px;
|
||||
font-weight: 300;
|
||||
font-size: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.alternateTitlesIconContainer {
|
||||
margin-left: 20px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.artistNavigationButtons {
|
||||
white-space: no-wrap;
|
||||
}
|
||||
|
||||
.artistNavigationButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
margin-left: 5px;
|
||||
color: #e1e2e3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.runtime {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.sizeOnDisk,
|
||||
.qualityProfileName,
|
||||
.tags {
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex: 1 0 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.contentContainer {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
.cover {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,422 @@
|
|||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { align, icons, sizes } from 'Helpers/Props';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
import AlbumCover from 'Album/AlbumCover';
|
||||
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||
|
||||
import styles from './AlbumDetails.css';
|
||||
|
||||
function getFanartUrl(images) {
|
||||
const fanartImage = _.find(images, { coverType: 'fanart' });
|
||||
if (fanartImage) {
|
||||
// Remove protocol
|
||||
return fanartImage.url.replace(/^https?:/, '');
|
||||
}
|
||||
}
|
||||
|
||||
function getExpandedState(newState) {
|
||||
return {
|
||||
allExpanded: newState.allSelected,
|
||||
allCollapsed: newState.allUnselected,
|
||||
expandedState: newState.selectedState
|
||||
};
|
||||
}
|
||||
|
||||
class AlbumDetails extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isArtistHistoryModalOpen: false,
|
||||
isManageTracksOpen: false,
|
||||
isEditAlbumModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditAlbumPress = () => {
|
||||
this.setState({ isEditAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAlbumModalClose = () => {
|
||||
this.setState({ isEditAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onManageTracksPress = () => {
|
||||
this.setState({ isManageTracksOpen: true });
|
||||
}
|
||||
|
||||
onManageTracksModalClose = () => {
|
||||
this.setState({ isManageTracksOpen: false });
|
||||
}
|
||||
|
||||
onArtistHistoryPress = () => {
|
||||
this.setState({ isArtistHistoryModalOpen: true });
|
||||
}
|
||||
|
||||
onArtistHistoryModalClose = () => {
|
||||
this.setState({ isArtistHistoryModalOpen: false });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
expandedState
|
||||
} = this.state;
|
||||
|
||||
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
|
||||
}
|
||||
|
||||
onExpandPress = (albumId, isExpanded) => {
|
||||
this.setState((state) => {
|
||||
const convertedState = {
|
||||
allSelected: state.allExpanded,
|
||||
allUnselected: state.allCollapsed,
|
||||
selectedState: state.expandedState
|
||||
};
|
||||
|
||||
const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
|
||||
|
||||
return getExpandedState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
albumType,
|
||||
statistics,
|
||||
monitored,
|
||||
releaseDate,
|
||||
images,
|
||||
media,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
albumsError,
|
||||
trackFilesError,
|
||||
shortDateFormat,
|
||||
artist,
|
||||
previousAlbum,
|
||||
nextAlbum,
|
||||
isSearching,
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isArtistHistoryModalOpen,
|
||||
isEditAlbumModalOpen,
|
||||
isManageTracksOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState
|
||||
} = this.state;
|
||||
|
||||
let expandIcon = icons.EXPAND_INDETERMINATE;
|
||||
|
||||
if (allExpanded) {
|
||||
expandIcon = icons.COLLAPSE;
|
||||
} else if (allCollapsed) {
|
||||
expandIcon = icons.EXPAND;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent title={title}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Search Album"
|
||||
iconName={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manage Tracks"
|
||||
iconName={icons.TRACK_FILE}
|
||||
onPress={this.onManageTracksPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="History"
|
||||
iconName={icons.HISTORY}
|
||||
onPress={this.onArtistHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Edit"
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onEditAlbumPress}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<PageToolbarButton
|
||||
label={allExpanded ? 'Collapse All' : 'Expand All'}
|
||||
iconName={expandIcon}
|
||||
onPress={this.onExpandAllPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={{
|
||||
backgroundImage: `url(${getFanartUrl(images)})`
|
||||
}}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContent}>
|
||||
<AlbumCover
|
||||
className={styles.cover}
|
||||
images={images}
|
||||
size={500}
|
||||
lazy={false}
|
||||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className={styles.artistNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.artistNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={`Go to ${previousAlbum.title}`}
|
||||
to={`/album/${previousAlbum.foreignAlbumId}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.artistNavigationButton}
|
||||
name={icons.ARROW_UP}
|
||||
size={30}
|
||||
title={`Go to ${artist.artistName}`}
|
||||
to={`/artist/${artist.foreignArtistId}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.artistNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={`Go to ${nextAlbum.title}`}
|
||||
to={`/album/${nextAlbum.foreignAlbumId}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className={styles.details}>
|
||||
<div>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={20}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className={styles.detailsLabels}>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
moment(releaseDate).format(shortDateFormat)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(statistics.sizeOnDisk)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!albumType &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title="Type"
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{albumType}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !albumsError && !trackFilesError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && albumsError &&
|
||||
<div>Loading albums failed</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && trackFilesError &&
|
||||
<div>Loading track files failed</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!media.length &&
|
||||
<div>
|
||||
|
||||
{
|
||||
media.slice(0).map((medium) => {
|
||||
return (
|
||||
<AlbumDetailsMediumConnector
|
||||
key={medium.mediumNumber}
|
||||
albumId={id}
|
||||
{...medium}
|
||||
isExpanded={expandedState[medium.mediumNumber]}
|
||||
onExpandPress={this.onExpandPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<TrackFileEditorModal
|
||||
isOpen={isManageTracksOpen}
|
||||
artistId={artist.id}
|
||||
albumId={id}
|
||||
onModalClose={this.onManageTracksModalClose}
|
||||
/>
|
||||
|
||||
<ArtistHistoryModal
|
||||
isOpen={isArtistHistoryModalOpen}
|
||||
artistId={artist.id}
|
||||
albumId={id}
|
||||
onModalClose={this.onArtistHistoryModalClose}
|
||||
/>
|
||||
|
||||
<EditAlbumModalConnector
|
||||
isOpen={isEditAlbumModalOpen}
|
||||
albumId={id}
|
||||
artistId={artist.id}
|
||||
onModalClose={this.onEditAlbumModalClose}
|
||||
/>
|
||||
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumDetails.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
media: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSearching: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
isPopulated: PropTypes.bool,
|
||||
albumsError: PropTypes.object,
|
||||
tracksError: PropTypes.object,
|
||||
trackFilesError: PropTypes.object,
|
||||
artist: PropTypes.object,
|
||||
previousAlbum: PropTypes.object,
|
||||
nextAlbum: PropTypes.object,
|
||||
onRefreshPress: PropTypes.func,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AlbumDetails.defaultProps = {
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
export default AlbumDetails;
|
|
@ -0,0 +1,146 @@
|
|||
/* eslint max-params: 0 */
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import AlbumDetails from './AlbumDetails';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { foreignAlbumId }) => foreignAlbumId,
|
||||
(state) => state.tracks,
|
||||
(state) => state.trackFiles,
|
||||
(state) => state.albums,
|
||||
createAllArtistSelector(),
|
||||
createCommandsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(foreignAlbumId, tracks, trackFiles, albums, artists, commands, uiSettings) => {
|
||||
const sortedAlbums = _.orderBy(albums.items, 'releaseDate');
|
||||
const albumIndex = _.findIndex(sortedAlbums, { foreignAlbumId });
|
||||
const album = sortedAlbums[albumIndex];
|
||||
const artist = _.find(artists, { id: album.artistId });
|
||||
|
||||
if (!album) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const previousAlbum = sortedAlbums[albumIndex - 1] || _.last(sortedAlbums);
|
||||
const nextAlbum = sortedAlbums[albumIndex + 1] || _.first(sortedAlbums);
|
||||
const isSearching = !!findCommand(commands, { name: commandNames.ALBUM_SEARCH });
|
||||
|
||||
const isFetching = tracks.isFetching || trackFiles.isFetching;
|
||||
const isPopulated = tracks.isPopulated && trackFiles.isPopulated;
|
||||
const tracksError = tracks.error;
|
||||
const trackFilesError = trackFiles.error;
|
||||
|
||||
return {
|
||||
...album,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
artist,
|
||||
isSearching,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
tracksError,
|
||||
trackFilesError,
|
||||
previousAlbum,
|
||||
nextAlbum
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand,
|
||||
fetchTracks,
|
||||
clearTracks,
|
||||
fetchTrackFiles,
|
||||
clearTrackFiles
|
||||
};
|
||||
|
||||
class AlbumDetailsConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.populate);
|
||||
this.populate();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id
|
||||
} = this.props;
|
||||
|
||||
// If the id has changed we need to clear the tracks/track
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.populate);
|
||||
this.unpopulate();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
populate = () => {
|
||||
const albumId = this.props.id;
|
||||
|
||||
this.props.fetchTracks({ albumId });
|
||||
this.props.fetchTrackFiles({ albumId });
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.clearTracks();
|
||||
this.props.clearTrackFiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.ALBUM_SEARCH,
|
||||
albumIds: [this.props.id]
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AlbumDetails
|
||||
{...this.props}
|
||||
onSearchPress={this.onSearchPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isAlbumFetching: PropTypes.bool,
|
||||
isAlbumPopulated: PropTypes.bool,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
fetchTracks: PropTypes.func.isRequired,
|
||||
clearTracks: PropTypes.func.isRequired,
|
||||
fetchTrackFiles: PropTypes.func.isRequired,
|
||||
clearTrackFiles: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsConnector);
|
|
@ -0,0 +1,114 @@
|
|||
.medium {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.mediumNumber {
|
||||
margin-right: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mediumFormat {
|
||||
color: #8895aa;
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
flex-grow: 1;
|
||||
margin: 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 300px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.actions {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.actionsMenu {
|
||||
composes: menu from 'Components/Menu/Menu.css';
|
||||
|
||||
flex: 0 0 45px;
|
||||
}
|
||||
|
||||
.actionsMenuContent {
|
||||
composes: menuContent from 'Components/Menu/MenuContent.css';
|
||||
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actionMenuIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
composes: button from 'Components/Link/IconButton.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.tracks {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid $borderColor;
|
||||
}
|
||||
|
||||
.collapseButtonContainer {
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
composes: actionButton;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -12px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.noTracks {
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.medium {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
position: static;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import isAfter from 'Utilities/Date/isAfter';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TrackRowConnector from './TrackRowConnector';
|
||||
import styles from './AlbumDetailsMedium.css';
|
||||
|
||||
function getMediumStatistics(tracks) {
|
||||
let trackCount = 0;
|
||||
let trackFileCount = 0;
|
||||
let totalTrackCount = 0;
|
||||
|
||||
tracks.forEach((track) => {
|
||||
if (track.trackFileId) {
|
||||
trackCount++;
|
||||
trackFileCount++;
|
||||
}
|
||||
|
||||
totalTrackCount++;
|
||||
});
|
||||
|
||||
return {
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
};
|
||||
}
|
||||
|
||||
function getTrackCountKind(trackFileCount, trackCount) {
|
||||
if (trackFileCount === trackCount && trackCount > 0) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
class AlbumDetailsMedium extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this._expandByDefault();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.albumId !== this.props.albumId) {
|
||||
this._expandByDefault();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_expandByDefault() {
|
||||
const {
|
||||
mediumNumber,
|
||||
onExpandPress,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const expand = _.some(items, (item) => {
|
||||
return isAfter(item.airDateUtc) ||
|
||||
isAfter(item.airDateUtc, { days: -30 });
|
||||
});
|
||||
|
||||
onExpandPress(mediumNumber, expand && mediumNumber > 0);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onExpandPress = () => {
|
||||
const {
|
||||
mediumNumber,
|
||||
isExpanded
|
||||
} = this.props;
|
||||
|
||||
this.props.onExpandPress(mediumNumber, !isExpanded);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
mediumNumber,
|
||||
mediumFormat,
|
||||
items,
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
isExpanded,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
} = getMediumStatistics(items);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.medium}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.left}>
|
||||
{
|
||||
<div>
|
||||
<span className={styles.mediumNumber}>
|
||||
{mediumFormat} {mediumNumber}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Label
|
||||
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
|
||||
kind={getTrackCountKind(trackFileCount, trackCount)}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{
|
||||
<span>{totalTrackCount} / {trackCount}</span>
|
||||
}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.expandButton}
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.expandButtonIcon}
|
||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||
title={isExpanded ? 'Hide tracks' : 'Show tracks'}
|
||||
size={24}
|
||||
/>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<span> </span>
|
||||
}
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
isExpanded &&
|
||||
<div className={styles.tracks}>
|
||||
{
|
||||
items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<TrackRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
|
||||
<div className={styles.noTracks}>
|
||||
No tracks in this medium
|
||||
</div>
|
||||
}
|
||||
<div className={styles.collapseButtonContainer}>
|
||||
<IconButton
|
||||
name={icons.COLLAPSE}
|
||||
size={20}
|
||||
title="Hide tracks"
|
||||
onPress={this.onExpandPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumDetailsMedium.propTypes = {
|
||||
albumId: PropTypes.number.isRequired,
|
||||
mediumNumber: PropTypes.number.isRequired,
|
||||
mediumFormat: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
isExpanded: PropTypes.bool,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onExpandPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumDetailsMedium;
|
|
@ -0,0 +1,64 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { setTracksTableOption } from 'Store/Actions/trackActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import AlbumDetailsMedium from './AlbumDetailsMedium';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { mediumNumber }) => mediumNumber,
|
||||
(state) => state.tracks,
|
||||
createDimensionsSelector(),
|
||||
(mediumNumber, tracks, dimensions) => {
|
||||
|
||||
const tracksInMedium = _.filter(tracks.items, { mediumNumber });
|
||||
const sortedTracks = _.orderBy(tracksInMedium, ['absoluteTrackNumber'], ['asc']);
|
||||
|
||||
return {
|
||||
items: sortedTracks,
|
||||
columns: tracks.columns,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setTracksTableOption,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class AlbumDetailsMediumConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setTracksTableOption(payload);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AlbumDetailsMedium
|
||||
{...this.props}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumDetailsMediumConnector.propTypes = {
|
||||
albumId: PropTypes.number.isRequired,
|
||||
mediumNumber: PropTypes.number.isRequired,
|
||||
setTracksTableOption: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector);
|
|
@ -0,0 +1,110 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { push } from 'react-router-redux';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import AlbumDetailsConnector from './AlbumDetailsConnector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.albums,
|
||||
(match, albums) => {
|
||||
const foreignAlbumId = match.params.foreignAlbumId;
|
||||
const isAlbumsFetching = albums.isFetching;
|
||||
const isAlbumsPopulated = albums.isPopulated;
|
||||
|
||||
return {
|
||||
foreignAlbumId,
|
||||
isAlbumsFetching,
|
||||
isAlbumsPopulated
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
push,
|
||||
fetchAlbums,
|
||||
clearAlbums
|
||||
};
|
||||
|
||||
class AlbumDetailsPageConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.populate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unpopulate();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
populate = () => {
|
||||
const foreignAlbumId = this.props.foreignAlbumId;
|
||||
this.props.fetchAlbums({ foreignAlbumId });
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.clearAlbums();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
foreignAlbumId,
|
||||
isAlbumsFetching,
|
||||
isAlbumsPopulated
|
||||
} = this.props;
|
||||
|
||||
if (!foreignAlbumId) {
|
||||
return (
|
||||
<NotFound
|
||||
message="Sorry, that album cannot be found."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAlbumsFetching) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAlbumsFetching && !isAlbumsPopulated) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAlbumsFetching && isAlbumsPopulated) {
|
||||
return (
|
||||
<AlbumDetailsConnector
|
||||
foreignAlbumId={foreignAlbumId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlbumDetailsPageConnector.propTypes = {
|
||||
foreignAlbumId: PropTypes.string,
|
||||
match: PropTypes.shape({ params: PropTypes.shape({ foreignAlbumId: PropTypes.string.isRequired }).isRequired }).isRequired,
|
||||
push: PropTypes.func.isRequired,
|
||||
fetchAlbums: PropTypes.func.isRequired,
|
||||
clearAlbums: PropTypes.func.isRequired,
|
||||
isAlbumsFetching: PropTypes.bool.isRequired,
|
||||
isAlbumsPopulated: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsPageConnector);
|
|
@ -0,0 +1,31 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
.audio {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.language,
|
||||
.duration,
|
||||
.status {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
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 EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
||||
import TrackFileLanguageConnector from 'TrackFile/TrackFileLanguageConnector';
|
||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||
|
||||
import styles from './TrackRow.css';
|
||||
|
||||
class TrackRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
mediumNumber,
|
||||
trackFileId,
|
||||
absoluteTrackNumber,
|
||||
title,
|
||||
duration,
|
||||
trackFilePath,
|
||||
trackFileRelativePath,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'medium') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.trackNumber}
|
||||
>
|
||||
{mediumNumber}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'absoluteTrackNumber') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.trackNumber}
|
||||
>
|
||||
{absoluteTrackNumber}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.title}
|
||||
>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
trackFilePath
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'relativePath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
trackFileRelativePath
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'duration') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.duration}
|
||||
>
|
||||
{
|
||||
formatTimeSpan(duration)
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.language}
|
||||
>
|
||||
<TrackFileLanguageConnector
|
||||
episodeFileId={trackFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'audioInfo') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.audio}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
type={mediaInfoTypes.AUDIO}
|
||||
trackFileId={trackFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<EpisodeStatusConnector
|
||||
albumId={id}
|
||||
trackFileId={trackFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TrackRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
albumId: PropTypes.number.isRequired,
|
||||
trackFileId: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
mediumNumber: PropTypes.number.isRequired,
|
||||
trackNumber: PropTypes.string.isRequired,
|
||||
absoluteTrackNumber: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
trackFilePath: PropTypes.string,
|
||||
trackFileRelativePath: PropTypes.string,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default TrackRow;
|
|
@ -0,0 +1,22 @@
|
|||
/* eslint max-params: 0 */
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import TrackRow from './TrackRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state, { mediumNumber }) => mediumNumber,
|
||||
createTrackFileSelector(),
|
||||
createCommandsSelector(),
|
||||
(id, mediumNumber, trackFile, commands) => {
|
||||
return {
|
||||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileRelativePath: trackFile ? trackFile.relativePath : null
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
export default connect(createMapStateToProps)(TrackRow);
|
|
@ -0,0 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditAlbumModalContentConnector from './EditAlbumModalContentConnector';
|
||||
|
||||
function EditAlbumModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditAlbumModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditAlbumModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditAlbumModal;
|
|
@ -0,0 +1,39 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditAlbumModal from './EditAlbumModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditAlbumModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'albums' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAlbumModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAlbumModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector);
|
|
@ -0,0 +1,119 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
class EditAlbumModalContent extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
onSavePress(false);
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
currentRelease,
|
||||
releases
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Edit - {artistName} - {title} [{albumType}]
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>Monitored</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText="Lidarr will search for and download album"
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel> Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ALBUM_RELEASE_SELECT}
|
||||
name="currentRelease"
|
||||
helpText="Change release for this album"
|
||||
albumReleases={releases}
|
||||
selectedRelease={currentRelease}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAlbumModalContent.propTypes = {
|
||||
albumId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
albumType: PropTypes.string.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditAlbumModalContent;
|
|
@ -0,0 +1,97 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import createAlbumSelector from 'Store/Selectors/createAlbumSelector';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import { setAlbumValue, saveAlbum } from 'Store/Actions/albumActions';
|
||||
import EditAlbumModalContent from './EditAlbumModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.albums,
|
||||
createAlbumSelector(),
|
||||
createArtistSelector(),
|
||||
(albumState, album, artist) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = albumState;
|
||||
|
||||
const albumSettings = _.pick(album, [
|
||||
'monitored',
|
||||
'currentRelease',
|
||||
'releases'
|
||||
]);
|
||||
|
||||
const settings = selectSettings(albumSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: album.title,
|
||||
artistName: artist.artistName,
|
||||
albumType: album.albumType,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetAlbumValue: setAlbumValue,
|
||||
dispatchSaveAlbum: saveAlbum
|
||||
};
|
||||
|
||||
class EditAlbumModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetAlbumValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveAlbum({
|
||||
id: this.props.albumId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAlbumModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAlbumModalContentConnector.propTypes = {
|
||||
albumId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetAlbumValue: PropTypes.func.isRequired,
|
||||
dispatchSaveAlbum: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditAlbumModalContentConnector);
|
|
@ -20,6 +20,7 @@ class AlbumStudioRow extends Component {
|
|||
artistId,
|
||||
status,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
artistName,
|
||||
monitored,
|
||||
albums,
|
||||
|
@ -49,7 +50,7 @@ class AlbumStudioRow extends Component {
|
|||
|
||||
<TableRowCell className={styles.title}>
|
||||
<ArtistNameLink
|
||||
nameSlug={nameSlug}
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
@ -84,6 +85,7 @@ AlbumStudioRow.propTypes = {
|
|||
artistId: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
||||
import { toggleAlbumMonitored } from 'Store/Actions/albumActions';
|
||||
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
|
||||
import AlbumStudioRow from './AlbumStudioRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -32,7 +32,7 @@ function createMapStateToProps() {
|
|||
|
||||
const mapDispatchToProps = {
|
||||
toggleArtistMonitored,
|
||||
toggleAlbumMonitored
|
||||
toggleAlbumsMonitored
|
||||
};
|
||||
|
||||
class AlbumStudioRowConnector extends Component {
|
||||
|
@ -53,8 +53,9 @@ class AlbumStudioRowConnector extends Component {
|
|||
}
|
||||
|
||||
onAlbumMonitoredPress = (albumId, monitored) => {
|
||||
this.props.toggleAlbumMonitored({
|
||||
albumId,
|
||||
const albumIds = [albumId];
|
||||
this.props.toggleAlbumsMonitored({
|
||||
albumIds,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ AlbumStudioRowConnector.propTypes = {
|
|||
artistId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
toggleArtistMonitored: PropTypes.func.isRequired,
|
||||
toggleAlbumMonitored: PropTypes.func.isRequired
|
||||
toggleAlbumsMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);
|
||||
|
|
|
@ -14,6 +14,7 @@ import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
|
|||
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
|
||||
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
|
||||
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
|
||||
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
|
@ -92,10 +93,15 @@ function App({ store, history }) {
|
|||
/>
|
||||
|
||||
<Route
|
||||
path="/artist/:nameSlug"
|
||||
path="/artist/:foreignArtistId"
|
||||
component={ArtistDetailsPageConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/album/:foreignAlbumId"
|
||||
component={AlbumDetailsPageConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Calendar
|
||||
*/}
|
||||
|
|
|
@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
function ArtistNameLink({ nameSlug, artistName }) {
|
||||
const link = `/artist/${nameSlug}`;
|
||||
function ArtistNameLink({ foreignArtistId, artistName }) {
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
return (
|
||||
<Link to={link}>
|
||||
|
@ -13,7 +13,7 @@ function ArtistNameLink({ nameSlug, artistName }) {
|
|||
}
|
||||
|
||||
ArtistNameLink.propTypes = {
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ import Label from 'Components/Label';
|
|||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
|
||||
import AlbumTitleDetailLink from 'Album/AlbumTitleDetailLink';
|
||||
import styles from './AlbumRow.css';
|
||||
|
||||
function getTrackCountKind(monitored, trackFileCount, trackCount) {
|
||||
|
@ -33,7 +32,8 @@ class AlbumRow extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
isDetailsModalOpen: false,
|
||||
isEditAlbumModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,14 @@ class AlbumRow extends Component {
|
|||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
onEditAlbumPress = () => {
|
||||
this.setState({ isEditAlbumModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAlbumModalClose = () => {
|
||||
this.setState({ isEditAlbumModalOpen: false });
|
||||
}
|
||||
|
||||
onMonitorAlbumPress = (monitored, options) => {
|
||||
this.props.onMonitorAlbumPress(this.props.id, monitored, options);
|
||||
}
|
||||
|
@ -64,9 +72,11 @@ class AlbumRow extends Component {
|
|||
duration,
|
||||
releaseDate,
|
||||
mediumCount,
|
||||
secondaryTypes,
|
||||
title,
|
||||
isSaving,
|
||||
artistMonitored,
|
||||
foreignAlbumId,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
|
@ -111,11 +121,9 @@ class AlbumRow extends Component {
|
|||
key={name}
|
||||
className={styles.title}
|
||||
>
|
||||
<AlbumTitleLink
|
||||
albumId={id}
|
||||
artistId={artistId}
|
||||
albumTitle={title}
|
||||
showOpenArtistButton={false}
|
||||
<AlbumTitleDetailLink
|
||||
title={title}
|
||||
foreignAlbumId={foreignAlbumId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
|
@ -131,6 +139,16 @@ class AlbumRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'secondaryTypes') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
secondaryTypes
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackCount') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
|
@ -189,7 +207,6 @@ class AlbumRow extends Component {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
@ -206,6 +223,8 @@ AlbumRow.propTypes = {
|
|||
mediumCount: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
foreignAlbumId: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
artistMonitored: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -15,6 +15,7 @@ function createMapStateToProps() {
|
|||
createCommandsSelector(),
|
||||
(id, sceneSeasonNumber, artist, trackFile, commands) => {
|
||||
return {
|
||||
foreignArtistId: artist.foreignArtistId,
|
||||
artistMonitored: artist.monitored,
|
||||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileRelativePath: trackFile ? trackFile.relativePath : null
|
||||
|
|
|
@ -34,34 +34,6 @@ import ArtistDetailsLinks from './ArtistDetailsLinks';
|
|||
import styles from './ArtistDetails.css';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
|
||||
const albumTypes = [
|
||||
{
|
||||
name: 'album',
|
||||
label: 'Album',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'ep',
|
||||
label: 'EP',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'single',
|
||||
label: 'Single',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'broadcast',
|
||||
label: 'Broadcast',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'other',
|
||||
label: 'Other',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
|
@ -193,6 +165,7 @@ class ArtistDetails extends Component {
|
|||
trackFileCount,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
albumTypes,
|
||||
status,
|
||||
overview,
|
||||
links,
|
||||
|
@ -359,7 +332,7 @@ class ArtistDetails extends Component {
|
|||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={`Go to ${previousArtist.artistName}`}
|
||||
to={`/artist/${previousArtist.nameSlug}`}
|
||||
to={`/artist/${previousArtist.foreignArtistId}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
|
@ -367,7 +340,7 @@ class ArtistDetails extends Component {
|
|||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={`Go to ${nextArtist.artistName}`}
|
||||
to={`/artist/${nextArtist.nameSlug}`}
|
||||
to={`/artist/${nextArtist.foreignArtistId}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -545,12 +518,12 @@ class ArtistDetails extends Component {
|
|||
albumTypes.slice(0).map((albumType) => {
|
||||
return (
|
||||
<ArtistDetailsSeasonConnector
|
||||
key={albumType.name}
|
||||
key={albumType}
|
||||
artistId={id}
|
||||
name={albumType.name}
|
||||
label={albumType.label}
|
||||
name={albumType}
|
||||
label={albumType}
|
||||
{...albumType}
|
||||
isExpanded={expandedState[albumType.name]}
|
||||
isExpanded={expandedState[albumType]}
|
||||
onExpandPress={this.onExpandPress}
|
||||
/>
|
||||
);
|
||||
|
@ -614,6 +587,7 @@ ArtistDetails.propTypes = {
|
|||
trackFileCount: PropTypes.number,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
albumTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
|
|
@ -17,20 +17,30 @@ import ArtistDetails from './ArtistDetails';
|
|||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { nameSlug }) => nameSlug,
|
||||
(state, { foreignArtistId }) => foreignArtistId,
|
||||
(state) => state.albums,
|
||||
(state) => state.trackFiles,
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createAllArtistSelector(),
|
||||
createCommandsSelector(),
|
||||
(nameSlug, albums, trackFiles, allArtists, commands) => {
|
||||
(foreignArtistId, albums, trackFiles, metadataProfiles, allArtists, commands) => {
|
||||
const sortedArtist = _.orderBy(allArtists, 'sortName');
|
||||
const artistIndex = _.findIndex(sortedArtist, { nameSlug });
|
||||
const artistIndex = _.findIndex(sortedArtist, { foreignArtistId });
|
||||
const artist = sortedArtist[artistIndex];
|
||||
const metadataProfile = _.find(metadataProfiles.items, { id: artist.metadataProfileId });
|
||||
const albumTypes = _.reduce(metadataProfile.primaryAlbumTypes, (acc, primaryType) => {
|
||||
if (primaryType.allowed) {
|
||||
acc.push(primaryType.albumType.name);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sortedAlbumTypes = _.orderBy(albumTypes);
|
||||
|
||||
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
|
||||
const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist);
|
||||
const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id });
|
||||
|
@ -56,6 +66,7 @@ function createMapStateToProps() {
|
|||
|
||||
return {
|
||||
...artist,
|
||||
albumTypes: sortedAlbumTypes,
|
||||
alternateTitles,
|
||||
isArtistRefreshing,
|
||||
allArtistRefreshing,
|
||||
|
@ -176,7 +187,7 @@ class ArtistDetailsConnector extends Component {
|
|||
|
||||
ArtistDetailsConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
isArtistRefreshing: PropTypes.bool.isRequired,
|
||||
allArtistRefreshing: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -13,12 +13,12 @@ function createMapStateToProps() {
|
|||
(state, { match }) => match,
|
||||
createAllArtistSelector(),
|
||||
(match, allArtists) => {
|
||||
const nameSlug = match.params.nameSlug;
|
||||
const artistIndex = _.findIndex(allArtists, { nameSlug });
|
||||
const foreignArtistId = match.params.foreignArtistId;
|
||||
const artistIndex = _.findIndex(allArtists, { foreignArtistId });
|
||||
|
||||
if (artistIndex > -1) {
|
||||
return {
|
||||
nameSlug
|
||||
foreignArtistId
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ class ArtistDetailsPageConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.props.nameSlug) {
|
||||
if (!this.props.foreignArtistId) {
|
||||
this.props.push(`${window.Sonarr.urlBase}/`);
|
||||
return;
|
||||
}
|
||||
|
@ -48,10 +48,10 @@ class ArtistDetailsPageConnector extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
nameSlug
|
||||
foreignArtistId
|
||||
} = this.props;
|
||||
|
||||
if (!nameSlug) {
|
||||
if (!foreignArtistId) {
|
||||
return (
|
||||
<NotFound
|
||||
message="Sorry, that artist cannot be found."
|
||||
|
@ -61,15 +61,15 @@ class ArtistDetailsPageConnector extends Component {
|
|||
|
||||
return (
|
||||
<ArtistDetailsConnector
|
||||
nameSlug={nameSlug}
|
||||
foreignArtistId={foreignArtistId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistDetailsPageConnector.propTypes = {
|
||||
nameSlug: PropTypes.string,
|
||||
match: PropTypes.shape({ params: PropTypes.shape({ nameSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
|
||||
foreignArtistId: PropTypes.string,
|
||||
match: PropTypes.shape({ params: PropTypes.shape({ foreignArtistId: PropTypes.string.isRequired }).isRequired }).isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.season {
|
||||
.albumType {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
|
|
|
@ -27,7 +27,7 @@ class ArtistEditorRow extends Component {
|
|||
const {
|
||||
id,
|
||||
status,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
artistName,
|
||||
monitored,
|
||||
languageProfile,
|
||||
|
@ -56,7 +56,7 @@ class ArtistEditorRow extends Component {
|
|||
|
||||
<TableRowCell className={styles.title}>
|
||||
<ArtistNameLink
|
||||
nameSlug={nameSlug}
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
@ -105,7 +105,7 @@ class ArtistEditorRow extends Component {
|
|||
ArtistEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
languageProfile: PropTypes.object.isRequired,
|
||||
|
|
|
@ -59,7 +59,7 @@ class ArtistIndexBanner extends Component {
|
|||
artistName,
|
||||
monitored,
|
||||
status,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
nextAiring,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
|
@ -84,7 +84,7 @@ class ArtistIndexBanner extends Component {
|
|||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${nameSlug}`;
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${bannerWidth}px`,
|
||||
|
@ -213,7 +213,7 @@ ArtistIndexBanner.propTypes = {
|
|||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
trackCount: PropTypes.number,
|
||||
trackFileCount: PropTypes.number,
|
||||
|
|
|
@ -77,7 +77,7 @@ class ArtistIndexOverview extends Component {
|
|||
overview,
|
||||
monitored,
|
||||
status,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
nextAiring,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
|
@ -101,7 +101,7 @@ class ArtistIndexOverview extends Component {
|
|||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${nameSlug}`;
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
|
@ -226,7 +226,7 @@ ArtistIndexOverview.propTypes = {
|
|||
overview: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
trackCount: PropTypes.number,
|
||||
trackFileCount: PropTypes.number,
|
||||
|
|
|
@ -58,8 +58,8 @@ class ArtistIndexPoster extends Component {
|
|||
id,
|
||||
artistName,
|
||||
monitored,
|
||||
foreignArtistId,
|
||||
status,
|
||||
nameSlug,
|
||||
nextAiring,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
|
@ -84,7 +84,7 @@ class ArtistIndexPoster extends Component {
|
|||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${nameSlug}`;
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
|
@ -213,7 +213,7 @@ ArtistIndexPoster.propTypes = {
|
|||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
trackCount: PropTypes.number,
|
||||
trackFileCount: PropTypes.number,
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.nextAiring,
|
||||
.previousAiring,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.nextAiring,
|
||||
.previousAiring,
|
||||
.nextAlbum,
|
||||
.lastAlbum,
|
||||
.added {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
|
@ -32,8 +32,7 @@
|
|||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.trackProgress,
|
||||
.latestAlbum {
|
||||
.trackProgress {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
|
|
|
@ -12,6 +12,7 @@ import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
|||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import AlbumTitleDetailLink from 'Album/AlbumTitleDetailLink';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import ArtistStatusCell from './ArtistStatusCell';
|
||||
|
@ -66,12 +67,13 @@ class ArtistIndexRow extends Component {
|
|||
status,
|
||||
artistName,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
artistType,
|
||||
qualityProfile,
|
||||
languageProfile,
|
||||
metadataProfile,
|
||||
nextAiring,
|
||||
previousAiring,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
albumCount,
|
||||
trackCount,
|
||||
|
@ -81,7 +83,6 @@ class ArtistIndexRow extends Component {
|
|||
path,
|
||||
sizeOnDisk,
|
||||
tags,
|
||||
// useSceneNumbering,
|
||||
columns,
|
||||
isRefreshingArtist,
|
||||
onRefreshArtistPress
|
||||
|
@ -124,7 +125,7 @@ class ArtistIndexRow extends Component {
|
|||
className={styles[name]}
|
||||
>
|
||||
<ArtistNameLink
|
||||
nameSlug={nameSlug}
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
@ -175,25 +176,51 @@ class ArtistIndexRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'nextAiring') {
|
||||
if (name === 'nextAlbum') {
|
||||
if (nextAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<AlbumTitleDetailLink
|
||||
title={nextAlbum.title}
|
||||
foreignAlbumId={nextAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={nextAiring}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'previousAiring') {
|
||||
if (name === 'lastAlbum') {
|
||||
if (lastAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<AlbumTitleDetailLink
|
||||
title={lastAlbum.title}
|
||||
foreignAlbumId={lastAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={previousAiring}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -239,27 +266,6 @@ class ArtistIndexRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'latestAlbum') {
|
||||
const albumStatistics = latestAlbum.statistics;
|
||||
const progress = albumStatistics.trackCount ? albumStatistics.trackFileCount / albumStatistics.trackCount * 100 : 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${albumStatistics.trackFileCount} / ${albumStatistics.trackCount}`}
|
||||
title={`${albumStatistics.trackFileCount} / ${albumStatistics.trackCount} (Total: ${albumStatistics.totalTrackCount})`}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackCount') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
@ -356,14 +362,15 @@ ArtistIndexRow.propTypes = {
|
|||
status: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistType: PropTypes.string,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
languageProfile: PropTypes.object.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
previousAiring: PropTypes.string,
|
||||
nextAlbum: PropTypes.object,
|
||||
lastAlbum: PropTypes.object,
|
||||
added: PropTypes.string,
|
||||
albumCount: PropTypes.number.isRequired,
|
||||
albumCount: PropTypes.number,
|
||||
trackCount: PropTypes.number,
|
||||
trackFileCount: PropTypes.number,
|
||||
totalTrackCount: PropTypes.number,
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function artistIndexCellRenderers(cellProps) {
|
|||
status,
|
||||
name,
|
||||
nameSlug,
|
||||
foreignArtistId,
|
||||
qualityProfileId,
|
||||
nextAiring,
|
||||
previousAiring,
|
||||
|
@ -52,7 +53,7 @@ export default function artistIndexCellRenderers(cellProps) {
|
|||
{...otherProps}
|
||||
>
|
||||
<ArtistNameLink
|
||||
nameSlug={nameSlug}
|
||||
foreignArtistId={foreignArtistId}
|
||||
name={name}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { albumReleases }) => albumReleases,
|
||||
(state, { selectedRelease }) => selectedRelease,
|
||||
(albumReleases, selectedRelease) => {
|
||||
const values = _.map(albumReleases.value, (albumRelease) => {
|
||||
|
||||
return {
|
||||
key: albumRelease.id,
|
||||
value: `${albumRelease.mediaCount} med, ${albumRelease.trackCount} tracks` +
|
||||
`${albumRelease.country.length > 0 ? ', ' : ''}${albumRelease.country}` +
|
||||
`${albumRelease.disambiguation ? ', ' : ''}${titleCase(albumRelease.disambiguation)}` +
|
||||
`${albumRelease.format ? ', [' : ''}${albumRelease.format}${albumRelease.format ? ']' : ''}`
|
||||
};
|
||||
});
|
||||
|
||||
const value = selectedRelease.value.id;
|
||||
|
||||
return {
|
||||
values,
|
||||
value
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class AlbumReleaseSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
const {
|
||||
albumReleases
|
||||
} = this.props;
|
||||
|
||||
this.props.onChange({ name, value: _.find(albumReleases.value, { id: value }) });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumReleaseSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
albumReleases: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(AlbumReleaseSelectInputConnector);
|
|
@ -12,6 +12,7 @@ import PathInputConnector from './PathInputConnector';
|
|||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import SelectInput from './SelectInput';
|
||||
|
@ -53,6 +54,9 @@ function getComponent(type) {
|
|||
case inputTypes.METADATA_PROFILE_SELECT:
|
||||
return MetadataProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.ALBUM_RELEASE_SELECT:
|
||||
return AlbumReleaseSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
|
|
|
@ -218,12 +218,16 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
|
||||
handleTrack = (body) => {
|
||||
if (body.action === 'updated') {
|
||||
const action = body.action;
|
||||
const section = 'tracks';
|
||||
|
||||
if (action === 'updated') {
|
||||
this.props.updateItem({
|
||||
section: 'tracks',
|
||||
updateOnly: true,
|
||||
section,
|
||||
...body.resource
|
||||
});
|
||||
} else if (action === 'deleted') {
|
||||
this.props.removeItem({ section, id: body.resource.id });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ export const ACTIVITY = 'fa fa-clock-o';
|
|||
export const ADD = 'fa fa-plus';
|
||||
export const ALTERNATE_TITLES = 'fa fa-clone';
|
||||
export const ADVANCED_SETTINGS = 'fa fa-cog';
|
||||
export const ARROW_UP = 'fa fa-arrow-circle-up';
|
||||
export const ARROW_LEFT = 'fa fa-arrow-circle-left';
|
||||
export const ARROW_RIGHT = 'fa fa-arrow-circle-right';
|
||||
export const BACKUP = 'fa fa-file-archive-o';
|
||||
|
|
|
@ -8,6 +8,7 @@ export const PATH = 'path';
|
|||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
|
||||
export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'artistTypeSelect';
|
||||
|
@ -26,6 +27,7 @@ export const all = [
|
|||
QUALITY_PROFILE_SELECT,
|
||||
LANGUAGE_PROFILE_SELECT,
|
||||
METADATA_PROFILE_SELECT,
|
||||
ALBUM_RELEASE_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
|
|
|
@ -5,7 +5,9 @@ import { batchActions } from 'redux-batched-actions';
|
|||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import albumEntities from 'Album/albumEntities';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
@ -23,9 +25,12 @@ export const defaultState = {
|
|||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
sortKey: 'releaseDate',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
items: [],
|
||||
pendingChanges: {},
|
||||
|
||||
columns: [
|
||||
{
|
||||
|
@ -44,6 +49,11 @@ export const defaultState = {
|
|||
label: 'Release Date',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'secondaryTypes',
|
||||
label: 'Secondary Types',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'mediumCount',
|
||||
label: 'Media Count',
|
||||
|
@ -84,6 +94,8 @@ export const FETCH_ALBUMS = 'albums/fetchAlbums';
|
|||
export const SET_ALBUMS_SORT = 'albums/setAlbumsSort';
|
||||
export const SET_ALBUMS_TABLE_OPTION = 'albums/setAlbumsTableOption';
|
||||
export const CLEAR_ALBUMS = 'albums/clearAlbums';
|
||||
export const SET_ALBUM_VALUE = 'albums/setAlbumValue';
|
||||
export const SAVE_ALBUM = 'albums/saveAlbum';
|
||||
export const TOGGLE_ALBUM_MONITORED = 'albums/toggleAlbumMonitored';
|
||||
export const TOGGLE_ALBUMS_MONITORED = 'albums/toggleAlbumsMonitored';
|
||||
|
||||
|
@ -97,11 +109,21 @@ export const clearAlbums = createAction(CLEAR_ALBUMS);
|
|||
export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED);
|
||||
export const toggleAlbumsMonitored = createThunk(TOGGLE_ALBUMS_MONITORED);
|
||||
|
||||
export const saveAlbum = createThunk(SAVE_ALBUM);
|
||||
|
||||
export const setAlbumValue = createAction(SET_ALBUM_VALUE, (payload) => {
|
||||
return {
|
||||
section: 'albums',
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_ALBUMS]: createFetchHandler(section, '/album'),
|
||||
[SAVE_ALBUM]: createSaveProviderHandler(section, '/album'),
|
||||
|
||||
[TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
|
@ -203,6 +225,8 @@ export const reducers = createHandleActions({
|
|||
|
||||
[SET_ALBUMS_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||
|
||||
[SET_ALBUM_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLEAR_ALBUMS]: (state) => {
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
|
|
|
@ -93,14 +93,14 @@ export const defaultState = {
|
|||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'nextAiring',
|
||||
label: 'Next Airing',
|
||||
name: 'nextAlbum',
|
||||
label: 'Next Album',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'previousAiring',
|
||||
label: 'Previous Airing',
|
||||
name: 'lastAlbum',
|
||||
label: 'Last Album',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
|
@ -128,12 +128,6 @@ export const defaultState = {
|
|||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'latestAlbum',
|
||||
label: 'Latest Album',
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
|
|
|
@ -20,13 +20,15 @@ export const defaultState = {
|
|||
error: null,
|
||||
sortKey: 'mediumNumber',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
secondarySortKey: 'absoluteTrackNumber',
|
||||
secondarySortDirection: sortDirections.ASCENDING,
|
||||
items: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'medium',
|
||||
label: 'Medium',
|
||||
isVisible: true
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'absoluteTrackNumber',
|
||||
|
@ -38,11 +40,26 @@ export const defaultState = {
|
|||
label: 'Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: 'Duration',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'audioInfo',
|
||||
label: 'Audio Info',
|
||||
|
@ -63,6 +80,8 @@ export const defaultState = {
|
|||
};
|
||||
|
||||
export const persistState = [
|
||||
'tracks.sortKey',
|
||||
'tracks.sortDirection',
|
||||
'tracks.columns'
|
||||
];
|
||||
|
||||
|
|
|
@ -102,8 +102,9 @@ class TrackFileEditorModalContentConnector extends Component {
|
|||
|
||||
componentDidMount() {
|
||||
const artistId = this.props.artistId;
|
||||
const albumId = this.props.albumId;
|
||||
|
||||
this.props.dispatchFetchTracks({ artistId });
|
||||
this.props.dispatchFetchTracks({ artistId, albumId });
|
||||
|
||||
this.props.dispatchFetchLanguageProfileSchema();
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
|
|
|
@ -48,7 +48,7 @@ function CutoffUnmetRow(props) {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -47,7 +47,7 @@ function MissingRow(props) {
|
|||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
nameSlug={artist.nameSlug}
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.SignalR;
|
||||
|
@ -21,7 +22,7 @@ namespace Lidarr.Api.V1.Albums
|
|||
: base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster)
|
||||
{
|
||||
GetResourceAll = GetAlbums;
|
||||
Put[@"/(?<id>[\d]{1,10})"] = x => SetAlbumMonitored(x.Id);
|
||||
UpdateResource = UpdateAlbum;
|
||||
Put["/monitor"] = x => SetAlbumsMonitored();
|
||||
}
|
||||
|
||||
|
@ -29,8 +30,9 @@ namespace Lidarr.Api.V1.Albums
|
|||
{
|
||||
var artistIdQuery = Request.Query.ArtistId;
|
||||
var albumIdsQuery = Request.Query.AlbumIds;
|
||||
var foreignIdQuery = Request.Query.ForeignAlbumId;
|
||||
|
||||
if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue)
|
||||
if (!Request.Query.ArtistId.HasValue && !albumIdsQuery.HasValue && !foreignIdQuery.HasValue)
|
||||
{
|
||||
return MapToResource(_albumService.GetAllAlbums(), false);
|
||||
}
|
||||
|
@ -42,6 +44,13 @@ namespace Lidarr.Api.V1.Albums
|
|||
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
|
||||
}
|
||||
|
||||
if (foreignIdQuery.HasValue)
|
||||
{
|
||||
int artistId = _albumService.FindById(foreignIdQuery.Value).ArtistId;
|
||||
|
||||
return MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
|
||||
}
|
||||
|
||||
string albumIdsValue = albumIdsQuery.Value.ToString();
|
||||
|
||||
var albumIds = albumIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
|
@ -51,12 +60,15 @@ namespace Lidarr.Api.V1.Albums
|
|||
return MapToResource(_albumService.GetAlbums(albumIds), false);
|
||||
}
|
||||
|
||||
private Response SetAlbumMonitored(int id)
|
||||
private void UpdateAlbum(AlbumResource albumResource)
|
||||
{
|
||||
var resource = Request.Body.FromJson<AlbumResource>();
|
||||
_albumService.SetAlbumMonitored(id, resource.Monitored);
|
||||
var album = _albumService.GetAlbum(albumResource.Id);
|
||||
|
||||
return MapToResource(_albumService.GetAlbum(id), false).AsResponse(HttpStatusCode.Accepted);
|
||||
var model = albumResource.ToModel(album);
|
||||
|
||||
_albumService.UpdateAlbum(model);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, albumResource);
|
||||
}
|
||||
|
||||
private Response SetAlbumsMonitored()
|
||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using Lidarr.Api.V1.Artist;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
|
@ -9,10 +11,16 @@ using NzbDrone.Core.Music;
|
|||
using NzbDrone.Core.ArtistStats;
|
||||
using NzbDrone.SignalR;
|
||||
using Lidarr.Http;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
|
||||
namespace Lidarr.Api.V1.Albums
|
||||
{
|
||||
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album>
|
||||
public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album>,
|
||||
IHandle<AlbumGrabbedEvent>,
|
||||
IHandle<AlbumEditedEvent>,
|
||||
IHandle<TrackImportedEvent>
|
||||
{
|
||||
protected readonly IAlbumService _albumService;
|
||||
protected readonly IArtistStatisticsService _artistStatisticsService;
|
||||
|
@ -130,18 +138,26 @@ namespace Lidarr.Api.V1.Albums
|
|||
}
|
||||
}
|
||||
|
||||
//TODO: Implement Track or Album Grabbed/Dowloaded Events
|
||||
public void Handle(AlbumGrabbedEvent message)
|
||||
{
|
||||
foreach (var album in message.Album.Albums)
|
||||
{
|
||||
var resource = album.ToResource();
|
||||
resource.Grabbed = true;
|
||||
|
||||
//public void Handle(TrackGrabbedEvent message)
|
||||
//{
|
||||
// foreach (var track in message.Track.Tracks)
|
||||
// {
|
||||
// var resource = track.ToResource();
|
||||
// resource.Grabbed = true;
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(AlbumEditedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Album.Id);
|
||||
}
|
||||
|
||||
// BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
// }
|
||||
//}
|
||||
public void Handle(TrackImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.AlbumId);
|
||||
}
|
||||
|
||||
//public void Handle(TrackDownloadedEvent message)
|
||||
//{
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace Lidarr.Api.V1.Albums
|
||||
{
|
||||
public class AlbumReleaseResource
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public int MediaCount { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public List<string> Country { get; set; }
|
||||
public List<string> Label { get; set; }
|
||||
public string Format { get; set; }
|
||||
}
|
||||
|
||||
public static class AlbumReleaseResourceMapper
|
||||
{
|
||||
public static AlbumReleaseResource ToResource(this AlbumRelease model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AlbumReleaseResource
|
||||
{
|
||||
Id = model.Id,
|
||||
ReleaseDate = model.ReleaseDate,
|
||||
TrackCount = model.TrackCount,
|
||||
MediaCount = model.MediaCount,
|
||||
Disambiguation = model.Disambiguation,
|
||||
Country = model.Country,
|
||||
Label = model.Label,
|
||||
Format = model.Format
|
||||
};
|
||||
}
|
||||
|
||||
public static AlbumRelease ToModel(this AlbumReleaseResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AlbumRelease
|
||||
{
|
||||
Id = resource.Id,
|
||||
ReleaseDate = resource.ReleaseDate,
|
||||
TrackCount = resource.TrackCount,
|
||||
MediaCount = resource.MediaCount,
|
||||
Disambiguation = resource.Disambiguation,
|
||||
Country = resource.Country,
|
||||
Label = resource.Label,
|
||||
Format = resource.Format
|
||||
};
|
||||
}
|
||||
|
||||
public static List<AlbumReleaseResource> ToResource(this IEnumerable<AlbumRelease> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<AlbumRelease> ToModel(this IEnumerable<AlbumReleaseResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ namespace Lidarr.Api.V1.Albums
|
|||
public int ProfileId { get; set; }
|
||||
public int Duration { get; set; }
|
||||
public string AlbumType { get; set; }
|
||||
public List<string> SecondaryTypes { get; set; }
|
||||
public int MediumCount
|
||||
{
|
||||
get
|
||||
|
@ -33,6 +34,8 @@ namespace Lidarr.Api.V1.Albums
|
|||
}
|
||||
public Ratings Ratings { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public AlbumRelease CurrentRelease { get; set; }
|
||||
public List<AlbumReleaseResource> Releases { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public List<MediumResource> Media { get; set; }
|
||||
public ArtistResource Artist { get; set; }
|
||||
|
@ -65,15 +68,45 @@ namespace Lidarr.Api.V1.Albums
|
|||
Ratings = model.Ratings,
|
||||
Duration = model.Duration,
|
||||
AlbumType = model.AlbumType,
|
||||
SecondaryTypes = model.SecondaryTypes.Select(s => s.Name).ToList(),
|
||||
Media = model.Media.ToResource(),
|
||||
CurrentRelease = model.CurrentRelease,
|
||||
Releases = model.Releases.ToResource(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Album ToModel(this AlbumResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Album
|
||||
{
|
||||
Id = resource.Id,
|
||||
ForeignAlbumId = resource.ForeignAlbumId,
|
||||
Title = resource.Title,
|
||||
Images = resource.Images,
|
||||
Monitored = resource.Monitored,
|
||||
CurrentRelease = resource.CurrentRelease
|
||||
};
|
||||
}
|
||||
|
||||
public static Album ToModel(this AlbumResource resource, Album album)
|
||||
{
|
||||
var updatedAlbum = resource.ToModel();
|
||||
|
||||
album.ApplyChanges(updatedAlbum);
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
public static List<AlbumResource> ToResource(this IEnumerable<Album> models)
|
||||
{
|
||||
if (models == null) return null;
|
||||
return models?.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
return models.Select(ToResource).ToList();
|
||||
public static List<Album> ToModel(this IEnumerable<AlbumResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Lidarr.Api.V1.Albums
|
|||
public string MediumFormat { get; set; }
|
||||
}
|
||||
|
||||
public static class SeasonResourceMapper
|
||||
public static class MediumResourceMapper
|
||||
{
|
||||
public static MediumResource ToResource(this Medium model)
|
||||
{
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
|
||||
{
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IAddArtistService _addArtistService;
|
||||
private readonly IArtistStatisticsService _artistStatisticsService;
|
||||
private readonly IMapCoversToLocal _coverMapper;
|
||||
|
@ -41,6 +42,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
|
||||
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IAddArtistService addArtistService,
|
||||
IArtistStatisticsService artistStatisticsService,
|
||||
IMapCoversToLocal coverMapper,
|
||||
|
@ -57,6 +59,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
: base(signalRBroadcaster)
|
||||
{
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_addArtistService = addArtistService;
|
||||
_artistStatisticsService = artistStatisticsService;
|
||||
|
||||
|
@ -105,6 +108,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
var resource = artist.ToResource();
|
||||
MapCoversToLocal(resource);
|
||||
FetchAndLinkArtistStatistics(resource);
|
||||
LinkNextPreviousAlbums(resource);
|
||||
//PopulateAlternateTitles(resource);
|
||||
|
||||
return resource;
|
||||
|
@ -116,7 +120,7 @@ namespace Lidarr.Api.V1.Artist
|
|||
var artistsResources = _artistService.GetAllArtists().ToResource();
|
||||
|
||||
MapCoversToLocal(artistsResources.ToArray());
|
||||
//MapAlbums(artistsResources.ToArray());
|
||||
LinkNextPreviousAlbums(artistsResources.ToArray());
|
||||
LinkArtistStatistics(artistsResources, artistStats);
|
||||
//PopulateAlternateTitles(seriesResources);
|
||||
|
||||
|
@ -171,6 +175,16 @@ namespace Lidarr.Api.V1.Artist
|
|||
}
|
||||
}
|
||||
|
||||
private void LinkNextPreviousAlbums(params ArtistResource[] artists)
|
||||
{
|
||||
foreach (var artistResource in artists)
|
||||
{
|
||||
var artistAlbums = _albumService.GetAlbumsByArtist(artistResource.Id).OrderBy(s=>s.ReleaseDate);
|
||||
artistResource.NextAlbum = artistAlbums.Where(s => s.ReleaseDate >= DateTime.UtcNow && s.Monitored).FirstOrDefault();
|
||||
artistResource.LastAlbum = artistAlbums.Where(s => s.ReleaseDate <= DateTime.UtcNow && s.Monitored).LastOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchAndLinkArtistStatistics(ArtistResource resource)
|
||||
{
|
||||
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
|
||||
|
@ -195,13 +209,6 @@ namespace Lidarr.Api.V1.Artist
|
|||
resource.SizeOnDisk = artistStatistics.SizeOnDisk;
|
||||
resource.AlbumCount = artistStatistics.AlbumCount;
|
||||
|
||||
if (artistStatistics.AlbumStatistics != null)
|
||||
{
|
||||
foreach (var album in resource.Albums)
|
||||
{
|
||||
album.Statistics = artistStatistics.AlbumStatistics.SingleOrDefault(s => s.AlbumId == album.Id).ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//private void PopulateAlternateTitles(List<ArtistResource> resources)
|
||||
|
|
|
@ -36,13 +36,13 @@ namespace Lidarr.Api.V1.Artist
|
|||
public int? TrackCount { get; set; }
|
||||
public int? TrackFileCount { get; set; }
|
||||
public long? SizeOnDisk { get; set; }
|
||||
//public SeriesStatusType Status { get; set; }
|
||||
public Album NextAlbum { get; set; }
|
||||
public Album LastAlbum { get; set; }
|
||||
|
||||
public List<MediaCover> Images { get; set; }
|
||||
public List<Member> Members { get; set; }
|
||||
|
||||
public string RemotePoster { get; set; }
|
||||
public List<AlbumResource> Albums { get; set; }
|
||||
|
||||
|
||||
//View & Edit
|
||||
|
@ -89,8 +89,6 @@ namespace Lidarr.Api.V1.Artist
|
|||
|
||||
Images = model.Images,
|
||||
|
||||
Albums = model.Albums.ToResource(),
|
||||
|
||||
Path = model.Path,
|
||||
QualityProfileId = model.ProfileId,
|
||||
LanguageProfileId = model.LanguageProfileId,
|
||||
|
@ -128,8 +126,6 @@ namespace Lidarr.Api.V1.Artist
|
|||
|
||||
Status = resource.Status,
|
||||
Overview = resource.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
|
||||
Images = resource.Images,
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
</Compile>
|
||||
<Compile Include="Albums\AlbumModule.cs" />
|
||||
<Compile Include="Albums\AlbumModuleWithSignalR.cs" />
|
||||
<Compile Include="Albums\AlbumReleaseResource.cs" />
|
||||
<Compile Include="Albums\AlbumResource.cs" />
|
||||
<Compile Include="Albums\AlbumsMonitoredResource.cs" />
|
||||
<Compile Include="Albums\AlbumStatisticsResource.cs" />
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
|
||||
public static class MediaInfoResourceMapper
|
||||
{
|
||||
public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName)
|
||||
public static MediaInfoResource ToResource(this MediaInfoModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
|
|
|
@ -80,8 +80,9 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
{
|
||||
int albumId = Convert.ToInt32(albumIdQuery.Value);
|
||||
var album = _albumService.GetAlbum(albumId);
|
||||
var albumArtist = _artistService.GetArtist(album.ArtistId);
|
||||
|
||||
return _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id).ConvertAll(f => f.ToResource(album.Artist, _upgradableSpecification));
|
||||
return _mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification));
|
||||
}
|
||||
|
||||
else
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
// SceneName = model.SceneName,
|
||||
Language = model.Language,
|
||||
Quality = model.Quality,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName)
|
||||
MediaInfo = model.MediaInfo.ToResource()
|
||||
//QualityCutoffNotMet
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,7 @@ namespace Lidarr.Api.V1.TrackFiles
|
|||
//SceneName = model.SceneName,
|
||||
Language = model.Language,
|
||||
Quality = model.Quality,
|
||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||
MediaInfo = model.MediaInfo.ToResource(),
|
||||
|
||||
QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(artist.Profile.Value, model.Quality),
|
||||
LanguageCutoffNotMet = upgradableSpecification.LanguageCutoffNotMet(artist.LanguageProfile.Value, model.Language)
|
||||
|
|
|
@ -2,20 +2,20 @@ using System.Collections.Generic;
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
using NzbDrone.SignalR;
|
||||
using Lidarr.Api.V1.TrackFiles;
|
||||
using Lidarr.Api.V1.Artist;
|
||||
using Lidarr.Http;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
|
||||
namespace Lidarr.Api.V1.Tracks
|
||||
{
|
||||
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>
|
||||
//IHandle<EpisodeGrabbedEvent>,
|
||||
//IHandle<EpisodeImportedEvent>
|
||||
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>,
|
||||
IHandle<TrackInfoRefreshedEvent>,
|
||||
IHandle<TrackImportedEvent>
|
||||
{
|
||||
protected readonly ITrackService _trackService;
|
||||
protected readonly IArtistService _artistService;
|
||||
|
@ -31,24 +31,24 @@ namespace Lidarr.Api.V1.Tracks
|
|||
_artistService = artistService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
|
||||
GetResourceById = GetEpisode;
|
||||
GetResourceById = GetTrack;
|
||||
}
|
||||
|
||||
protected TrackModuleWithSignalR(ITrackService episodeService,
|
||||
IArtistService seriesService,
|
||||
protected TrackModuleWithSignalR(ITrackService trackService,
|
||||
IArtistService artistService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
string resource)
|
||||
: base(signalRBroadcaster, resource)
|
||||
{
|
||||
_trackService = episodeService;
|
||||
_artistService = seriesService;
|
||||
_trackService = trackService;
|
||||
_artistService = artistService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
|
||||
GetResourceById = GetEpisode;
|
||||
GetResourceById = GetTrack;
|
||||
}
|
||||
|
||||
protected TrackResource GetEpisode(int id)
|
||||
protected TrackResource GetTrack(int id)
|
||||
{
|
||||
var episode = _trackService.GetTrack(id);
|
||||
var resource = MapToResource(episode, true, true);
|
||||
|
@ -76,28 +76,28 @@ namespace Lidarr.Api.V1.Tracks
|
|||
return resource;
|
||||
}
|
||||
|
||||
protected List<TrackResource> MapToResource(List<Track> episodes, bool includeSeries, bool includeEpisodeFile)
|
||||
protected List<TrackResource> MapToResource(List<Track> tracks, bool includeArtist, bool includeTrackFile)
|
||||
{
|
||||
var result = episodes.ToResource();
|
||||
var result = tracks.ToResource();
|
||||
|
||||
if (includeSeries || includeEpisodeFile)
|
||||
if (includeArtist || includeTrackFile)
|
||||
{
|
||||
var seriesDict = new Dictionary<int, NzbDrone.Core.Music.Artist>();
|
||||
for (var i = 0; i < episodes.Count; i++)
|
||||
var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>();
|
||||
for (var i = 0; i < tracks.Count; i++)
|
||||
{
|
||||
var episode = episodes[i];
|
||||
var track = tracks[i];
|
||||
var resource = result[i];
|
||||
|
||||
var series = episode.Artist ?? seriesDict.GetValueOrDefault(episodes[i].ArtistId) ?? _artistService.GetArtist(episodes[i].ArtistId);
|
||||
seriesDict[series.Id] = series;
|
||||
var series = track.Artist ?? artistDict.GetValueOrDefault(tracks[i].ArtistId) ?? _artistService.GetArtist(tracks[i].ArtistId);
|
||||
artistDict[series.Id] = series;
|
||||
|
||||
if (includeSeries)
|
||||
if (includeArtist)
|
||||
{
|
||||
resource.Artist = series.ToResource();
|
||||
}
|
||||
if (includeEpisodeFile && episodes[i].TrackFileId != 0)
|
||||
if (includeTrackFile && tracks[i].TrackFileId != 0)
|
||||
{
|
||||
resource.TrackFile = episodes[i].TrackFile.Value.ToResource(series, _upgradableSpecification);
|
||||
resource.TrackFile = tracks[i].TrackFile.Value.ToResource(series, _upgradableSpecification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,28 +105,31 @@ namespace Lidarr.Api.V1.Tracks
|
|||
return result;
|
||||
}
|
||||
|
||||
//public void Handle(EpisodeGrabbedEvent message)
|
||||
//{
|
||||
// foreach (var episode in message.Episode.Episodes)
|
||||
// {
|
||||
// var resource = episode.ToResource();
|
||||
// resource.Grabbed = true;
|
||||
public void Handle(TrackInfoRefreshedEvent message)
|
||||
{
|
||||
foreach (var track in message.Removed)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Deleted, track.ToResource());
|
||||
}
|
||||
|
||||
// BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
// }
|
||||
//}
|
||||
foreach (var track in message.Added)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, track.ToResource());
|
||||
}
|
||||
|
||||
//public void Handle(EpisodeImportedEvent message)
|
||||
//{
|
||||
// if (!message.NewDownload)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
foreach (var track in message.Updated)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, track.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(TrackImportedEvent message)
|
||||
{
|
||||
foreach (var track in message.TrackInfo.Tracks)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, track.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// foreach (var episode in message.EpisodeInfo.Episodes)
|
||||
// {
|
||||
// BroadcastResourceChange(ModelAction.Updated, episode.Id);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace NzbDrone.Common.Cloud
|
|||
Services = new HttpRequestBuilder("https://services.lidarr.audio/v1/")
|
||||
.CreateFactory();
|
||||
|
||||
Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0/{route}/") // TODO: Add {version} once LidarrAPI.Metadata is released.
|
||||
Search = new HttpRequestBuilder("https://api.lidarr.audio/api/v0.3/{route}")
|
||||
.CreateFactory();
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_remoteAlbum.Albums = Builder<Album>.CreateListOfSize(1).Build().ToList();
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(new List<TrackFile> { });
|
||||
|
||||
Mocker.GetMock<IDelayProfileService>()
|
||||
|
@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
private void GivenExistingFile(QualityModel quality, Language language)
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(new List<TrackFile> { new TrackFile {
|
||||
Quality = quality,
|
||||
Language = language
|
||||
|
|
|
@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
private void SetupMediaFile(List<TrackFile> files)
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(v => v.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(v => v.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(files);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
.Build();
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(new List<TrackFile> { _firstFile, _secondFile });
|
||||
|
||||
_parseResultMulti = new RemoteAlbum
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
.Build();
|
||||
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(new List<TrackFile> { _firstFile, _secondFile });
|
||||
|
||||
_parseResultMulti = new RemoteAlbum
|
||||
|
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
public void should_return_true_if_album_has_no_existing_file()
|
||||
{
|
||||
Mocker.GetMock<IMediaFileService>()
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Setup(c => c.GetFilesByAlbum(It.IsAny<int>()))
|
||||
.Returns(new List<TrackFile> { });
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(9)]
|
||||
public class album_releases : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Albums").AddColumn("Releases").AsString().WithDefaultValue("").Nullable();
|
||||
Alter.Table("Albums").AddColumn("CurrentRelease").AsString().WithDefaultValue("").Nullable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
|
||||
foreach (var album in subject.Albums)
|
||||
{
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id);
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
if (trackFiles.Any())
|
||||
{
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
{
|
||||
foreach (var album in subject.Albums)
|
||||
{
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id);
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
if (trackFiles.Any())
|
||||
{
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
}
|
||||
|
||||
var missingTrackFiles = subject.Albums
|
||||
.SelectMany(v => _albumService.GetFilesByAlbum(v.ArtistId, v.Id))
|
||||
.SelectMany(v => _albumService.GetFilesByAlbum(v.Id))
|
||||
.DistinctBy(v => v.Id)
|
||||
.Where(v => IsTrackFileMissing(subject.Artist, v))
|
||||
.ToArray();
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
|
||||
foreach (var album in subject.Albums)
|
||||
{
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id);
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
if (trackFiles.Any())
|
||||
{
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
|
||||
foreach (var album in subject.Albums)
|
||||
{
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.ArtistId, album.Id);
|
||||
var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id);
|
||||
|
||||
if (trackFiles.Any())
|
||||
{
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
using NzbDrone.Common.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class AlbumNotFoundException : NzbDroneException
|
||||
{
|
||||
public string MusicBrainzId { get; set; }
|
||||
|
||||
public AlbumNotFoundException(string musicbrainzId)
|
||||
: base(string.Format("Album with MusicBrainz {0} was not found, it may have been removed from MusicBrainz.", musicbrainzId))
|
||||
{
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public AlbumNotFoundException(string musicbrainzId, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
|
||||
public AlbumNotFoundException(string musicbrainzId, string message)
|
||||
: base(message)
|
||||
{
|
||||
MusicBrainzId = musicbrainzId;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
void Update(List<TrackFile> trackFile);
|
||||
void Delete(TrackFile trackFile, DeleteMediaFileReason reason);
|
||||
List<TrackFile> GetFilesByArtist(int artistId);
|
||||
List<TrackFile> GetFilesByAlbum(int artistId, int albumId);
|
||||
List<TrackFile> GetFilesByAlbum(int albumId);
|
||||
List<TrackFile> GetFiles(IEnumerable<int> ids);
|
||||
List<TrackFile> GetFilesWithoutMediaInfo();
|
||||
List<string> FilterExistingFiles(List<string> files, Artist artist);
|
||||
|
@ -27,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
|
||||
}
|
||||
|
||||
public class MediaFileService : IMediaFileService, IHandleAsync<ArtistDeletedEvent>
|
||||
public class MediaFileService : IMediaFileService, IHandleAsync<ArtistDeletedEvent>, IHandleAsync<AlbumDeletedEvent>
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IMediaFileRepository _mediaFileRepository;
|
||||
|
@ -104,12 +104,18 @@ namespace NzbDrone.Core.MediaFiles
|
|||
_mediaFileRepository.DeleteMany(files);
|
||||
}
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
var files = GetFilesByAlbum(message.Album.Id);
|
||||
_mediaFileRepository.DeleteMany(files);
|
||||
}
|
||||
|
||||
public List<TrackFile> GetFilesByArtist(int artistId)
|
||||
{
|
||||
return _mediaFileRepository.GetFilesByArtist(artistId);
|
||||
}
|
||||
|
||||
public List<TrackFile> GetFilesByAlbum(int artistId, int albumId)
|
||||
public List<TrackFile> GetFilesByAlbum(int albumId)
|
||||
{
|
||||
return _mediaFileRepository.GetFilesByAlbum(albumId);
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
|
||||
var artist = _artistService.GetArtist(artistId);
|
||||
var tracks = _trackService.GetTracksByAlbum(albumId);
|
||||
var files = _mediaFileService.GetFilesByAlbum(artistId, albumId);
|
||||
var files = _mediaFileService.GetFilesByAlbum(albumId);
|
||||
|
||||
return GetPreviews(artist, tracks, files)
|
||||
.OrderByDescending(e => e.TrackNumbers.First()).ToList();
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using NzbDrone.Core.Music;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource
|
||||
{
|
||||
public interface IProvideAlbumInfo
|
||||
{
|
||||
Tuple<Album, List<Track>> GetAlbumInfo(string lidarrId, string releaseId);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
|||
public List<string> SecondaryTypes { get; set; }
|
||||
public List<MediumResource> Media { get; set; }
|
||||
public List<TrackResource> Tracks { get; set; }
|
||||
public List<ReleaseResource> Releases { get; set; }
|
||||
public string SelectedRelease { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
||||
{
|
||||
public class ReleaseResource
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public int MediaCount { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public List<string> Label {get; set;}
|
||||
public List<string> Country { get; set; }
|
||||
public string Format { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,16 +9,13 @@ using NzbDrone.Common.Http;
|
|||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Core.Music;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist
|
||||
public class SkyHookProxy : IProvideArtistInfo, ISearchForNewArtist, IProvideAlbumInfo
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
@ -28,9 +25,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
private readonly IConfigService _configService;
|
||||
private readonly IMetadataProfileService _metadataProfileService;
|
||||
|
||||
private IHttpRequestBuilderFactory customerRequestBuilder;
|
||||
private IHttpRequestBuilderFactory _customerRequestBuilder;
|
||||
|
||||
public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger, IConfigService configService, IMetadataProfileService metadataProfileService)
|
||||
public SkyHookProxy(IHttpClient httpClient,
|
||||
ILidarrCloudRequestBuilder requestBuilder,
|
||||
IArtistService artistService, Logger logger,
|
||||
IConfigService configService,
|
||||
IMetadataProfileService metadataProfileService)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_configService = configService;
|
||||
|
@ -52,8 +53,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
var primaryTypes = metadataProfile.PrimaryAlbumTypes.Where(s => s.Allowed).Select(s => s.PrimaryAlbumType.Name);
|
||||
var secondaryTypes = metadataProfile.SecondaryAlbumTypes.Where(s => s.Allowed).Select(s => s.SecondaryAlbumType.Name);
|
||||
|
||||
var httpRequest = customerRequestBuilder.Create()
|
||||
.SetSegment("route", "artists/" + foreignArtistId)
|
||||
var httpRequest = _customerRequestBuilder.Create()
|
||||
.SetSegment("route", "artist/" + foreignArtistId)
|
||||
.AddQueryParam("primTypes", string.Join("|", primaryTypes))
|
||||
.AddQueryParam("secTypes", string.Join("|", secondaryTypes))
|
||||
.Build();
|
||||
|
@ -86,16 +87,55 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
return new Tuple<Artist, List<Album>>(artist, albums.ToList());
|
||||
}
|
||||
|
||||
public Tuple<Album, List<Track>> GetAlbumInfo(string foreignAlbumId, string releaseId)
|
||||
{
|
||||
_logger.Debug("Getting Album with LidarrAPI.MetadataID of {0}", foreignAlbumId);
|
||||
|
||||
SetCustomProvider();
|
||||
|
||||
var httpRequest = _customerRequestBuilder.Create()
|
||||
.SetSegment("route", "album/" + foreignAlbumId)
|
||||
.AddQueryParam("release", releaseId ?? string.Empty)
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get<AlbumResource>(httpRequest);
|
||||
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new AlbumNotFoundException(foreignAlbumId);
|
||||
}
|
||||
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new BadRequestException(foreignAlbumId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = httpResponse.Resource.Tracks.Select(MapTrack);
|
||||
var album = MapAlbum(httpResponse.Resource);
|
||||
|
||||
return new Tuple<Album, List<Track>>(album, tracks.ToList());
|
||||
}
|
||||
|
||||
public List<Artist> SearchForNewArtist(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lowerTitle = title.ToLowerInvariant();
|
||||
Console.WriteLine("Searching for " + lowerTitle);
|
||||
|
||||
if (lowerTitle.StartsWith("lidarr:") || lowerTitle.StartsWith("lidarrid:"))
|
||||
{
|
||||
var slug = lowerTitle.Split(':')[1].Trim();
|
||||
|
||||
Guid searchGuid;
|
||||
|
||||
bool isValid = Guid.TryParse(slug, out searchGuid);
|
||||
|
@ -118,10 +158,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
|
||||
SetCustomProvider();
|
||||
|
||||
var httpRequest = customerRequestBuilder.Create()
|
||||
var httpRequest = _customerRequestBuilder.Create()
|
||||
.SetSegment("route", "search")
|
||||
.AddQueryParam("type", "artist")
|
||||
.AddQueryParam("query", title.ToLower().Trim())
|
||||
//.AddQueryParam("images","false") // Should pass these on import search to avoid looking to fanart and wiki
|
||||
//.AddQueryParam("overview","false")
|
||||
.Build();
|
||||
|
||||
|
||||
|
@ -143,12 +185,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
|
||||
private Artist MapSearhResult(ArtistResource resource)
|
||||
{
|
||||
var artist = _artistService.FindById(resource.Id);
|
||||
|
||||
if (artist == null)
|
||||
{
|
||||
artist = MapArtist(resource);
|
||||
}
|
||||
var artist = _artistService.FindById(resource.Id) ?? MapArtist(resource);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
@ -161,37 +198,66 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
album.ReleaseDate = resource.ReleaseDate;
|
||||
album.CleanTitle = Parser.Parser.CleanArtistName(album.Title);
|
||||
album.AlbumType = resource.Type;
|
||||
album.Images = resource.Images.Select(MapImage).ToList();
|
||||
album.Label = resource.Labels;
|
||||
|
||||
if (resource.Images != null)
|
||||
{
|
||||
album.Images = resource.Images.Select(MapImage).ToList();
|
||||
}
|
||||
|
||||
album.Label = resource.Labels;
|
||||
album.Media = resource.Media.Select(MapMedium).ToList();
|
||||
album.Tracks = resource.Tracks.Select(MapTrack).ToList();
|
||||
album.SecondaryTypes = resource.SecondaryTypes.Select(MapSecondaryTypes).ToList();
|
||||
|
||||
if (resource.Releases != null)
|
||||
{
|
||||
album.Releases = resource.Releases.Select(MapAlbumRelease).ToList();
|
||||
album.CurrentRelease = album.Releases.FirstOrDefault(s => s.Id == resource.SelectedRelease);
|
||||
}
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
private static Medium MapMedium(MediumResource resource)
|
||||
{
|
||||
Medium medium = new Medium();
|
||||
medium.Name = resource.Name;
|
||||
medium.Number = resource.Position;
|
||||
medium.Format = resource.Format;
|
||||
Medium medium = new Medium
|
||||
{
|
||||
Name = resource.Name,
|
||||
Number = resource.Position,
|
||||
Format = resource.Format
|
||||
};
|
||||
|
||||
return medium;
|
||||
}
|
||||
|
||||
private static AlbumRelease MapAlbumRelease(ReleaseResource resource)
|
||||
{
|
||||
AlbumRelease albumRelease = new AlbumRelease
|
||||
{
|
||||
Id = resource.Id,
|
||||
ReleaseDate = resource.ReleaseDate,
|
||||
TrackCount = resource.TrackCount,
|
||||
Format = resource.Format,
|
||||
MediaCount = resource.MediaCount,
|
||||
Country = resource.Country,
|
||||
Disambiguation = resource.Disambiguation,
|
||||
Label = resource.Label
|
||||
};
|
||||
|
||||
return albumRelease;
|
||||
}
|
||||
|
||||
private static Track MapTrack(TrackResource resource)
|
||||
{
|
||||
Track track = new Track();
|
||||
track.Title = resource.TrackName;
|
||||
track.ForeignTrackId = resource.Id;
|
||||
track.TrackNumber = resource.TrackNumber;
|
||||
track.AbsoluteTrackNumber = resource.TrackPosition;
|
||||
track.Duration = resource.DurationMs;
|
||||
track.MediumNumber = resource.MediumNumber;
|
||||
|
||||
Track track = new Track
|
||||
{
|
||||
Title = resource.TrackName,
|
||||
ForeignTrackId = resource.Id,
|
||||
TrackNumber = resource.TrackNumber,
|
||||
AbsoluteTrackNumber = resource.TrackPosition,
|
||||
Duration = resource.DurationMs,
|
||||
MediumNumber = resource.MediumNumber
|
||||
};
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
|
@ -213,7 +279,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
artist.Status = MapArtistStatus(resource.Status);
|
||||
artist.Ratings = MapRatings(resource.Rating);
|
||||
artist.Links = resource.Links.Select(MapLink).ToList();
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
|
@ -251,14 +316,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
return ArtistStatusType.Continuing;
|
||||
}
|
||||
|
||||
private static Music.Ratings MapRatings(RatingResource rating)
|
||||
private static Ratings MapRatings(RatingResource rating)
|
||||
{
|
||||
if (rating == null)
|
||||
{
|
||||
return new Music.Ratings();
|
||||
return new Ratings();
|
||||
}
|
||||
|
||||
return new Music.Ratings
|
||||
return new Ratings
|
||||
{
|
||||
Votes = rating.Count,
|
||||
Value = rating.Value
|
||||
|
@ -274,9 +339,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
};
|
||||
}
|
||||
|
||||
private static Music.Links MapLink(LinkResource arg)
|
||||
private static Links MapLink(LinkResource arg)
|
||||
{
|
||||
return new Music.Links
|
||||
return new Links
|
||||
{
|
||||
Url = arg.Target,
|
||||
Name = arg.Type
|
||||
|
@ -337,11 +402,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
{
|
||||
if (_configService.MetadataSource.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource.TrimEnd("/") + "/{route}/").CreateFactory();
|
||||
_customerRequestBuilder = new HttpRequestBuilder(_configService.MetadataSource.TrimEnd("/") + "/{route}").CreateFactory();
|
||||
}
|
||||
else
|
||||
{
|
||||
customerRequestBuilder = _requestBuilder;
|
||||
_customerRequestBuilder = _requestBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,10 +39,26 @@ namespace NzbDrone.Core.Music
|
|||
public AddArtistOptions AddOptions { get; set; }
|
||||
public Artist Artist { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
public List<AlbumRelease> Releases { get; set; }
|
||||
public AlbumRelease CurrentRelease { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe());
|
||||
}
|
||||
|
||||
public void ApplyChanges(Album otherAlbum)
|
||||
{
|
||||
|
||||
ForeignAlbumId = otherAlbum.ForeignAlbumId;
|
||||
|
||||
Tracks = otherAlbum.Tracks;
|
||||
|
||||
ProfileId = otherAlbum.ProfileId;
|
||||
AddOptions = otherAlbum.AddOptions;
|
||||
Monitored = otherAlbum.Monitored;
|
||||
CurrentRelease = otherAlbum.CurrentRelease;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Music.Commands;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AlbumEditedService : IHandle<AlbumEditedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public AlbumEditedService(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Handle(AlbumEditedEvent message)
|
||||
{
|
||||
if (message.Album.CurrentRelease.Id != message.OldAlbum.CurrentRelease.Id)
|
||||
{
|
||||
_commandQueueManager.Push(new RefreshAlbumCommand(message.Album.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public class AlbumRelease : IEmbeddedDocument
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public int TrackCount { get; set; }
|
||||
public int MediaCount { get; set; }
|
||||
public string Disambiguation { get; set; }
|
||||
public List<string> Country { get; set; }
|
||||
public string Format { get; set; }
|
||||
public List<string> Label { get; set; }
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.Music
|
|||
List<Album> GetAlbums(IEnumerable<int> albumIds);
|
||||
List<Album> GetAlbumsByArtist(int artistId);
|
||||
Album AddAlbum(Album newAlbum);
|
||||
List<Album> AddAlbums(List<Album> newAlbums);
|
||||
Album FindById(string spotifyId);
|
||||
Album FindByTitle(int artistId, string title);
|
||||
Album FindByTitleInexact(string title);
|
||||
|
@ -61,11 +62,19 @@ namespace NzbDrone.Core.Music
|
|||
public Album AddAlbum(Album newAlbum)
|
||||
{
|
||||
_albumRepository.Insert(newAlbum);
|
||||
_eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id)));
|
||||
//_eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id)));
|
||||
|
||||
return newAlbum;
|
||||
}
|
||||
|
||||
public List<Album> AddAlbums(List<Album> newAlbums)
|
||||
{
|
||||
_albumRepository.InsertMany(newAlbums);
|
||||
//_eventAggregator.PublishEvent(new AlbumsAddedEvent(newAlbums.Select(s => s.Id).ToList()));
|
||||
|
||||
return newAlbums;
|
||||
}
|
||||
|
||||
public void DeleteAlbum(int albumId, bool deleteFiles)
|
||||
{
|
||||
var album = _albumRepository.Get(albumId);
|
||||
|
@ -140,11 +149,16 @@ namespace NzbDrone.Core.Music
|
|||
public void DeleteMany(List<Album> albums)
|
||||
{
|
||||
_albumRepository.DeleteMany(albums);
|
||||
|
||||
foreach (var album in albums)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new AlbumDeletedEvent(album, false));
|
||||
}
|
||||
}
|
||||
|
||||
public Album UpdateAlbum(Album album)
|
||||
{
|
||||
var storedAlbum = GetAlbum(album.Id); // Is it Id or iTunesId?
|
||||
var storedAlbum = GetAlbum(album.Id);
|
||||
|
||||
var updatedAlbum = _albumRepository.Update(album);
|
||||
_eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum));
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Music.Commands
|
||||
{
|
||||
public class RefreshAlbumCommand : Command
|
||||
{
|
||||
public int? AlbumId { get; set; }
|
||||
|
||||
public RefreshAlbumCommand()
|
||||
{
|
||||
}
|
||||
|
||||
public RefreshAlbumCommand(int? albumId)
|
||||
{
|
||||
AlbumId = albumId;
|
||||
}
|
||||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
|
||||
public override bool UpdateScheduledTask => !AlbumId.HasValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Music.Events
|
||||
{
|
||||
public class AlbumsAddedEvent : IEvent
|
||||
{
|
||||
public List<int> AlbumIds { get; private set; }
|
||||
|
||||
public AlbumsAddedEvent(List<int> albumIds)
|
||||
{
|
||||
AlbumIds = albumIds;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
@ -12,12 +12,14 @@ namespace NzbDrone.Core.Music.Events
|
|||
public Album Album { get; set; }
|
||||
public ReadOnlyCollection<Track> Added { get; private set; }
|
||||
public ReadOnlyCollection<Track> Updated { get; private set; }
|
||||
public ReadOnlyCollection<Track> Removed { get; private set; }
|
||||
|
||||
public TrackInfoRefreshedEvent(Album album, IList<Track> added, IList<Track> updated)
|
||||
public TrackInfoRefreshedEvent(Album album, IList<Track> added, IList<Track> updated, IList<Track> removed)
|
||||
{
|
||||
Album = album;
|
||||
Added = new ReadOnlyCollection<Track>(added);
|
||||
Updated = new ReadOnlyCollection<Track>(updated);
|
||||
Removed = new ReadOnlyCollection<Track>(removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,31 +7,104 @@ using System.Collections.Generic;
|
|||
using NzbDrone.Core.Organizer;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
|
||||
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
using NzbDrone.Core.Music.Commands;
|
||||
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Music
|
||||
{
|
||||
public interface IRefreshAlbumService
|
||||
{
|
||||
void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums);
|
||||
void RefreshAlbumInfo(Album album);
|
||||
}
|
||||
|
||||
public class RefreshAlbumService : IRefreshAlbumService
|
||||
public class RefreshAlbumService : IRefreshAlbumService, IExecute<RefreshAlbumCommand>
|
||||
{
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IProvideAlbumInfo _albumInfo;
|
||||
private readonly IRefreshTrackService _refreshTrackService;
|
||||
private readonly ITrackService _trackService;
|
||||
private readonly IBuildFileNames _fileNameBuilder;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RefreshAlbumService(IAlbumService albumService, IBuildFileNames fileNameBuilder, IRefreshTrackService refreshTrackService, IEventAggregator eventAggregator, Logger logger)
|
||||
public RefreshAlbumService(IAlbumService albumService,
|
||||
IArtistService artistService,
|
||||
IProvideAlbumInfo albumInfo,
|
||||
IRefreshTrackService refreshTrackService,
|
||||
ITrackService trackService,
|
||||
IBuildFileNames fileNameBuilder,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_albumService = albumService;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_artistService = artistService;
|
||||
_albumInfo = albumInfo;
|
||||
_refreshTrackService = refreshTrackService;
|
||||
_trackService = trackService;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void RefreshAlbumInfo(Album album)
|
||||
{
|
||||
_logger.ProgressInfo("Updating Info for {0}", album.Title);
|
||||
|
||||
Tuple<Album, List<Track>> tuple;
|
||||
|
||||
try
|
||||
{
|
||||
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, album.CurrentRelease.Id);
|
||||
}
|
||||
catch (AlbumNotFoundException)
|
||||
{
|
||||
_logger.Error(
|
||||
"Album '{0}' (LidarrAPI {1}) was not found, it may have been removed from Metadata sources.",
|
||||
album.Title, album.ForeignAlbumId);
|
||||
return;
|
||||
}
|
||||
|
||||
var albumInfo = tuple.Item1;
|
||||
|
||||
if (album.ForeignAlbumId != albumInfo.ForeignAlbumId)
|
||||
{
|
||||
_logger.Warn(
|
||||
"Album '{0}' (Album {1}) was replaced with '{2}' (LidarrAPI {3}), because the original was a duplicate.",
|
||||
album.Title, album.ForeignAlbumId, albumInfo.Title, albumInfo.ForeignAlbumId);
|
||||
album.ForeignAlbumId = albumInfo.ForeignAlbumId;
|
||||
}
|
||||
|
||||
album.LastInfoSync = DateTime.UtcNow;
|
||||
album.CleanTitle = albumInfo.CleanTitle;
|
||||
album.Title = albumInfo.Title ?? "Unknown";
|
||||
album.CleanTitle = Parser.Parser.CleanArtistName(album.Title);
|
||||
album.AlbumType = albumInfo.AlbumType;
|
||||
album.SecondaryTypes = albumInfo.SecondaryTypes;
|
||||
album.Genres = albumInfo.Genres;
|
||||
album.Media = albumInfo.Media;
|
||||
album.Label = albumInfo.Label;
|
||||
album.Images = albumInfo.Images;
|
||||
album.ReleaseDate = albumInfo.ReleaseDate;
|
||||
album.Duration = tuple.Item2.Sum(track => track.Duration);
|
||||
album.Releases = albumInfo.Releases;
|
||||
|
||||
_refreshTrackService.RefreshTrackInfo(album, tuple.Item2);
|
||||
|
||||
_albumService.UpdateAlbum(album);
|
||||
|
||||
}
|
||||
|
||||
public void RefreshAlbumInfo(Artist artist, IEnumerable<Album> remoteAlbums)
|
||||
{
|
||||
_logger.Info("Starting album info refresh for: {0}", artist);
|
||||
|
@ -42,67 +115,80 @@ namespace NzbDrone.Core.Music
|
|||
|
||||
var updateList = new List<Album>();
|
||||
var newList = new List<Album>();
|
||||
var dupeFreeRemoteAlbums = remoteAlbums.DistinctBy(m => new { m.ForeignAlbumId, m.ReleaseDate }).ToList();
|
||||
var dupeFreeRemoteAlbums = remoteAlbums.DistinctBy(m => new {m.ForeignAlbumId, m.ReleaseDate}).ToList();
|
||||
|
||||
foreach (var album in OrderAlbums(artist, dupeFreeRemoteAlbums))
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
var albumToUpdate = GetAlbumToUpdate(artist, album, existingAlbums);
|
||||
|
||||
Tuple<Album, List<Track>> tuple;
|
||||
var albumInfo = new Album();
|
||||
|
||||
if (albumToUpdate != null)
|
||||
{
|
||||
|
||||
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, albumToUpdate.CurrentRelease.Id);
|
||||
albumInfo = tuple.Item1;
|
||||
existingAlbums.Remove(albumToUpdate);
|
||||
updateList.Add(albumToUpdate);
|
||||
}
|
||||
else
|
||||
{
|
||||
albumToUpdate = new Album();
|
||||
albumToUpdate.Monitored = artist.Monitored;
|
||||
albumToUpdate.ProfileId = artist.ProfileId;
|
||||
albumToUpdate.Added = DateTime.UtcNow;
|
||||
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId, null);
|
||||
albumInfo = tuple.Item1;
|
||||
albumToUpdate = new Album
|
||||
{
|
||||
Monitored = artist.Monitored,
|
||||
ProfileId = artist.ProfileId,
|
||||
Added = DateTime.UtcNow
|
||||
};
|
||||
|
||||
albumToUpdate.ArtistId = artist.Id;
|
||||
albumToUpdate.CleanTitle = albumInfo.CleanTitle;
|
||||
albumToUpdate.ForeignAlbumId = albumInfo.ForeignAlbumId;
|
||||
albumToUpdate.Title = albumInfo.Title ?? "Unknown";
|
||||
albumToUpdate.AlbumType = albumInfo.AlbumType;
|
||||
|
||||
_albumService.AddAlbum(albumToUpdate);
|
||||
|
||||
newList.Add(albumToUpdate);
|
||||
}
|
||||
|
||||
|
||||
albumToUpdate.ForeignAlbumId = album.ForeignAlbumId;
|
||||
|
||||
|
||||
albumToUpdate.LastInfoSync = DateTime.UtcNow;
|
||||
albumToUpdate.CleanTitle = album.CleanTitle;
|
||||
albumToUpdate.Title = album.Title ?? "Unknown";
|
||||
albumToUpdate.CleanTitle = albumInfo.CleanTitle;
|
||||
albumToUpdate.Title = albumInfo.Title ?? "Unknown";
|
||||
albumToUpdate.CleanTitle = Parser.Parser.CleanArtistName(albumToUpdate.Title);
|
||||
albumToUpdate.ArtistId = artist.Id;
|
||||
albumToUpdate.AlbumType = album.AlbumType;
|
||||
albumToUpdate.SecondaryTypes = album.SecondaryTypes;
|
||||
albumToUpdate.Genres = album.Genres;
|
||||
albumToUpdate.Media = album.Media;
|
||||
albumToUpdate.Label = album.Label;
|
||||
albumToUpdate.Images = album.Images;
|
||||
albumToUpdate.ReleaseDate = album.ReleaseDate;
|
||||
albumToUpdate.Duration = album.Tracks.Sum(track => track.Duration);
|
||||
albumToUpdate.AlbumType = albumInfo.AlbumType;
|
||||
albumToUpdate.SecondaryTypes = albumInfo.SecondaryTypes;
|
||||
albumToUpdate.Genres = albumInfo.Genres;
|
||||
albumToUpdate.Media = albumInfo.Media;
|
||||
albumToUpdate.Label = albumInfo.Label;
|
||||
albumToUpdate.Images = albumInfo.Images;
|
||||
albumToUpdate.ReleaseDate = albumInfo.ReleaseDate;
|
||||
albumToUpdate.Duration = tuple.Item2.Sum(track => track.Duration);
|
||||
albumToUpdate.Releases = albumInfo.Releases;
|
||||
albumToUpdate.CurrentRelease = albumInfo.CurrentRelease;
|
||||
|
||||
_refreshTrackService.RefreshTrackInfo(albumToUpdate, tuple.Item2);
|
||||
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, album);
|
||||
_logger.Fatal(e, "An error has occurred while updating album info for artist {0}. {1}", artist,
|
||||
album);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var allAlbums = new List<Album>();
|
||||
allAlbums.AddRange(newList);
|
||||
allAlbums.AddRange(updateList);
|
||||
|
||||
// TODO: See if anything needs to be done here
|
||||
//AdjustMultiEpisodeAirTime(artist, allTracks);
|
||||
//AdjustDirectToDvdAirDate(artist, allTracks);
|
||||
|
||||
_albumService.DeleteMany(existingAlbums);
|
||||
_albumService.UpdateMany(updateList);
|
||||
_albumService.InsertMany(newList);
|
||||
|
||||
_albumService.UpdateMany(newList);
|
||||
|
||||
_eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newList, updateList));
|
||||
|
||||
if (failCount != 0)
|
||||
|
@ -125,13 +211,26 @@ namespace NzbDrone.Core.Music
|
|||
|
||||
private Album GetAlbumToUpdate(Artist artist, Album album, List<Album> existingAlbums)
|
||||
{
|
||||
return existingAlbums.FirstOrDefault(e => e.ForeignAlbumId == album.ForeignAlbumId/* && e.ReleaseDate == album.ReleaseDate*/);
|
||||
return existingAlbums.FirstOrDefault(
|
||||
e => e.ForeignAlbumId == album.ForeignAlbumId /* && e.ReleaseDate == album.ReleaseDate*/);
|
||||
}
|
||||
|
||||
private IEnumerable<Album> OrderAlbums(Artist artist, List<Album> albums)
|
||||
{
|
||||
return albums.OrderBy(e => e.ForeignAlbumId).ThenBy(e => e.ReleaseDate);
|
||||
}
|
||||
|
||||
public void Execute(RefreshAlbumCommand message)
|
||||
{
|
||||
if (message.AlbumId.HasValue)
|
||||
{
|
||||
var album = _albumService.GetAlbum(message.AlbumId.Value);
|
||||
var artist = _artistService.GetArtist(album.ArtistId);
|
||||
RefreshAlbumInfo(album);
|
||||
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace NzbDrone.Core.Music
|
|||
{
|
||||
private readonly IProvideArtistInfo _artistInfo;
|
||||
private readonly IArtistService _artistService;
|
||||
private readonly IAlbumService _albumService;
|
||||
private readonly IRefreshAlbumService _refreshAlbumService;
|
||||
private readonly IRefreshTrackService _refreshTrackService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
@ -29,6 +30,7 @@ namespace NzbDrone.Core.Music
|
|||
|
||||
public RefreshArtistService(IProvideArtistInfo artistInfo,
|
||||
IArtistService artistService,
|
||||
IAlbumService albumService,
|
||||
IRefreshAlbumService refreshAlbumService,
|
||||
IRefreshTrackService refreshTrackService,
|
||||
IEventAggregator eventAggregator,
|
||||
|
@ -38,6 +40,7 @@ namespace NzbDrone.Core.Music
|
|||
{
|
||||
_artistInfo = artistInfo;
|
||||
_artistService = artistService;
|
||||
_albumService = albumService;
|
||||
_refreshAlbumService = refreshAlbumService;
|
||||
_refreshTrackService = refreshTrackService;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
@ -92,52 +95,13 @@ namespace NzbDrone.Core.Music
|
|||
{
|
||||
_logger.Warn(e, "Couldn't update artist path for " + artist.Path);
|
||||
}
|
||||
|
||||
//artist.Albums = UpdateAlbums(artist, artistInfo); # We don't need this since we don't store albums in artist table.
|
||||
|
||||
_artistService.UpdateArtist(artist);
|
||||
|
||||
_refreshAlbumService.RefreshAlbumInfo(artist, tuple.Item2);
|
||||
foreach (var album in tuple.Item2)
|
||||
{
|
||||
_refreshTrackService.RefreshTrackInfo(album, album.Tracks);
|
||||
}
|
||||
|
||||
_refreshAlbumService.RefreshAlbumInfo(artist, tuple.Item2);
|
||||
|
||||
_logger.Debug("Finished artist refresh for {0}", artist.Name);
|
||||
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));
|
||||
}
|
||||
|
||||
private List<Album> UpdateAlbums(Artist artist, Artist artistInfo)
|
||||
{
|
||||
var albums = artistInfo.Albums.DistinctBy(s => s.ForeignAlbumId).ToList();
|
||||
|
||||
foreach (var album in albums)
|
||||
{
|
||||
var existingAlbum = artist.Albums.FirstOrDefault(s => s.ForeignAlbumId == album.ForeignAlbumId);
|
||||
|
||||
//Todo: Should this should use the previous season's monitored state?
|
||||
if (existingAlbum == null)
|
||||
{
|
||||
//if (album.SeasonNumber == 0)
|
||||
//{
|
||||
// album.Monitored = false;
|
||||
// continue;
|
||||
//}
|
||||
|
||||
_logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.ForeignArtistId, artist.Name);
|
||||
album.Monitored = true;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
album.Monitored = existingAlbum.Monitored;
|
||||
}
|
||||
}
|
||||
|
||||
return albums;
|
||||
}
|
||||
|
||||
public void Execute(RefreshArtistCommand message)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new ArtistRefreshStartingEvent(message.Trigger == CommandTrigger.Manual));
|
||||
|
|
|
@ -68,6 +68,7 @@ namespace NzbDrone.Core.Music
|
|||
trackToUpdate.AbsoluteTrackNumber = track.AbsoluteTrackNumber;
|
||||
trackToUpdate.Title = track.Title ?? "Unknown";
|
||||
trackToUpdate.AlbumId = album.Id;
|
||||
trackToUpdate.ArtistId = album.ArtistId;
|
||||
trackToUpdate.Album = track.Album ?? album;
|
||||
trackToUpdate.Explicit = track.Explicit;
|
||||
trackToUpdate.ArtistId = album.ArtistId;
|
||||
|
@ -89,15 +90,11 @@ namespace NzbDrone.Core.Music
|
|||
allTracks.AddRange(newList);
|
||||
allTracks.AddRange(updateList);
|
||||
|
||||
// TODO: See if anything needs to be done here
|
||||
//AdjustMultiEpisodeAirTime(artist, allTracks);
|
||||
//AdjustDirectToDvdAirDate(artist, allTracks);
|
||||
|
||||
_trackService.DeleteMany(existingTracks);
|
||||
_trackService.UpdateMany(updateList);
|
||||
_trackService.InsertMany(newList);
|
||||
|
||||
_eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(album, newList, updateList));
|
||||
_eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(album, newList, updateList, existingTracks));
|
||||
|
||||
if (failCount != 0)
|
||||
{
|
||||
|
|
|
@ -35,6 +35,7 @@ namespace NzbDrone.Core.Music
|
|||
|
||||
public class TrackService : ITrackService,
|
||||
IHandleAsync<ArtistDeletedEvent>,
|
||||
IHandleAsync<AlbumDeletedEvent>,
|
||||
IHandle<TrackFileDeletedEvent>,
|
||||
IHandle<TrackFileAddedEvent>
|
||||
{
|
||||
|
@ -163,6 +164,12 @@ namespace NzbDrone.Core.Music
|
|||
_trackRepository.DeleteMany(tracks);
|
||||
}
|
||||
|
||||
public void HandleAsync(AlbumDeletedEvent message)
|
||||
{
|
||||
var tracks = GetTracksByAlbum(message.Album.Id);
|
||||
_trackRepository.DeleteMany(tracks);
|
||||
}
|
||||
|
||||
public void Handle(TrackFileDeletedEvent message)
|
||||
{
|
||||
foreach (var track in GetTracksByFileId(message.TrackFile.Id))
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
<Compile Include="Datastore\Migration\003_add_medium_support.cs" />
|
||||
<Compile Include="Datastore\Migration\006_separate_automatic_and_interactive_search.cs" />
|
||||
<Compile Include="Datastore\Migration\007_change_album_path_to_relative.cs" />
|
||||
<Compile Include="Datastore\Migration\009_album_releases.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
||||
|
@ -424,6 +425,7 @@
|
|||
<Compile Include="Download\ProcessDownloadDecisions.cs" />
|
||||
<Compile Include="Download\ProcessedDecisions.cs" />
|
||||
<Compile Include="Download\RedownloadFailedDownloadService.cs" />
|
||||
<Compile Include="Exceptions\AlbumNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\ArtistNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\BadRequestException.cs" />
|
||||
<Compile Include="Exceptions\DownstreamException.cs" />
|
||||
|
@ -730,6 +732,7 @@
|
|||
<Compile Include="Messaging\IProcessMessage.cs" />
|
||||
<Compile Include="MetadataSource\IProvideArtistInfo.cs" />
|
||||
<Compile Include="MetadataSource\ISearchForNewArtist.cs" />
|
||||
<Compile Include="MetadataSource\IProvideAlbumInfo.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\MediumResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\MemberResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\AlbumResource.cs" />
|
||||
|
@ -737,6 +740,7 @@
|
|||
<Compile Include="MetadataSource\SkyHook\Resource\LinkResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ReleaseResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\TimeOfDayResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\TrackResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" />
|
||||
|
@ -761,9 +765,13 @@
|
|||
<Compile Include="Extras\Metadata\MetadataRepository.cs" />
|
||||
<Compile Include="Extras\Metadata\MetadataService.cs" />
|
||||
<Compile Include="Extras\Metadata\MetadataType.cs" />
|
||||
<Compile Include="Music\AlbumEditedService.cs" />
|
||||
<Compile Include="Music\AlbumRelease.cs" />
|
||||
<Compile Include="Music\ArtistStatusType.cs" />
|
||||
<Compile Include="Music\AlbumCutoffService.cs" />
|
||||
<Compile Include="Music\Commands\BulkMoveArtistCommand.cs" />
|
||||
<Compile Include="Music\Commands\RefreshAlbumCommand.cs" />
|
||||
<Compile Include="Music\Events\AlbumsAddedEvent.cs" />
|
||||
<Compile Include="Music\Events\ArtistsImportedEvent.cs" />
|
||||
<Compile Include="Music\SecondaryAlbumType.cs" />
|
||||
<Compile Include="Music\PrimaryAlbumType.cs" />
|
||||
|
|
|
@ -237,15 +237,6 @@ namespace NzbDrone.Integration.Test
|
|||
changed = true;
|
||||
}
|
||||
|
||||
result.Albums.ForEach(season =>
|
||||
{
|
||||
if (season.Monitored != monitored.Value)
|
||||
{
|
||||
season.Monitored = monitored.Value;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Artist.Put(result);
|
||||
|
|
Loading…
Reference in New Issue