Various UI Fixes and Updates

Closes #188
Closes #185
Closes #187
pull/190/head
Qstick 6 years ago
parent 3beac03c00
commit 54e9f88648

@ -119,6 +119,7 @@ class HistoryRow extends Component {
artistId={artist.id} artistId={artist.id}
albumTitle={album.title} albumTitle={album.title}
showOpenArtistButton={true} showOpenArtistButton={true}
showOpenAlbumButton={true}
/> />
</TableRowCell> </TableRowCell>
); );

@ -161,6 +161,7 @@ class QueueRow extends Component {
trackFileId={album.trackFileId} trackFileId={album.trackFileId}
albumTitle={album.title} albumTitle={album.title}
showOpenArtistButton={true} showOpenArtistButton={true}
showOpenAlbumButton={true}
/> />
</TableRowCell> </TableRowCell>
); );

@ -78,6 +78,10 @@ class AddNewArtistSearchResult extends Component {
isSmallScreen isSmallScreen
} = this.props; } = this.props;
const {
isNewAddArtistModalOpen
} = this.state;
const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress }; const linkProps = isExistingArtist ? { to: `/artist/${foreignArtistId}` } : { onPress: this.onPress };
let albums = '1 Album'; let albums = '1 Album';
@ -88,78 +92,78 @@ class AddNewArtistSearchResult extends Component {
const height = calculateHeight(230, isSmallScreen); const height = calculateHeight(230, isSmallScreen);
return ( return (
<Link <div>
className={styles.searchResult} <Link
{...linkProps} className={styles.searchResult}
> {...linkProps}
{ >
!isSmallScreen && {
!isSmallScreen &&
<ArtistPoster <ArtistPoster
className={styles.poster} className={styles.poster}
images={images} images={images}
size={250} size={250}
/> />
} }
<div> <div>
<div className={styles.name}> <div className={styles.name}>
{artistName} {artistName}
{ {
!name.contains(year) && !!year && !name.contains(year) && !!year &&
<span className={styles.year}>({year})</span> <span className={styles.year}>({year})</span>
} }
{ {
!!disambiguation && !!disambiguation &&
<span className={styles.year}>({disambiguation})</span> <span className={styles.year}>({disambiguation})</span>
} }
{ {
isExistingArtist && isExistingArtist &&
<Icon <Icon
className={styles.alreadyExistsIcon} className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE} name={icons.CHECK_CIRCLE}
size={36} size={36}
title="Already in your library" title="Already in your library"
/> />
} }
</div> </div>
<div> <div>
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
<HeartRating <HeartRating
rating={ratings.value} rating={ratings.value}
iconSize={13} iconSize={13}
/> />
</Label> </Label>
{ {
!!artistType && !!artistType &&
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
{artistType} {artistType}
</Label> </Label>
} }
{ {
!!albumCount && !!albumCount &&
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
{albums} {albums}
</Label> </Label>
} }
{ {
status === 'ended' && status === 'ended' &&
<Label <Label
kind={kinds.DANGER} kind={kinds.DANGER}
size={sizes.LARGE} size={sizes.LARGE}
> >
Ended Ended
</Label> </Label>
} }
</div> </div>
<div>
<div <div
className={styles.overview} className={styles.overview}
style={{ style={{
@ -173,10 +177,10 @@ class AddNewArtistSearchResult extends Component {
/> />
</div> </div>
</div> </div>
</div> </Link>
<AddNewArtistModal <AddNewArtistModal
isOpen={this.state.isNewAddArtistModalOpen && !isExistingArtist} isOpen={isNewAddArtistModalOpen && !isExistingArtist}
foreignArtistId={foreignArtistId} foreignArtistId={foreignArtistId}
artistName={artistName} artistName={artistName}
year={year} year={year}
@ -184,7 +188,7 @@ class AddNewArtistSearchResult extends Component {
images={images} images={images}
onModalClose={this.onAddArtistModalClose} onModalClose={this.onAddArtistModalClose}
/> />
</Link> </div>
); );
} }
} }

@ -4,7 +4,6 @@ import ReactDOM from 'react-dom';
import TetherComponent from 'react-tether'; import TetherComponent from 'react-tether';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import SpinnerIcon from 'Components/SpinnerIcon';
import FormInputButton from 'Components/Form/FormInputButton'; import FormInputButton from 'Components/Form/FormInputButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';

@ -43,7 +43,7 @@ class ImportArtistSelectFolderConnector extends Component {
const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id); const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id);
if (newRootFolders.length === 1) { if (newRootFolders.length === 1) {
this.props.push(`${window.Sonarr.urlBase}/add/import/${newRootFolders[0].id}`); this.props.push(`${window.Lidarr.urlBase}/add/import/${newRootFolders[0].id}`);
} }
} }
} }

@ -42,3 +42,7 @@
margin-right: auto; margin-right: auto;
} }
.openButtons {
margin-right: auto;
}

@ -45,17 +45,20 @@ class AlbumDetailsModalContent extends Component {
albumId, albumId,
artistName, artistName,
foreignArtistId, foreignArtistId,
foreignAlbumId,
artistMonitored, artistMonitored,
albumTitle, albumTitle,
monitored, monitored,
isSaving, isSaving,
showOpenArtistButton, showOpenArtistButton,
showOpenAlbumButton,
startInteractiveSearch, startInteractiveSearch,
onMonitorAlbumPress, onMonitorAlbumPress,
onModalClose onModalClose
} = this.props; } = this.props;
const artistLink = `/artist/${foreignArtistId}`; const artistLink = `/artist/${foreignArtistId}`;
const albumLink = `/album/${foreignAlbumId}`;
return ( return (
<ModalContent <ModalContent
@ -121,18 +124,30 @@ class AlbumDetailsModalContent extends Component {
</Tabs> </Tabs>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter >
{ <div className={styles.openButtons}>
showOpenArtistButton && {
<Button showOpenArtistButton &&
className={styles.openArtistButton} <Button
to={artistLink} className={styles.openArtistButton}
onPress={onModalClose} to={artistLink}
> onPress={onModalClose}
Open Artist >
</Button> Open Artist
} </Button>
}
{
showOpenAlbumButton &&
<Button
className={styles.openAlbumButton}
to={albumLink}
onPress={onModalClose}
>
Open Album
</Button>
}
</div>
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
@ -150,6 +165,7 @@ AlbumDetailsModalContent.propTypes = {
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
foreignAlbumId: PropTypes.string.isRequired,
artistMonitored: PropTypes.bool.isRequired, artistMonitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired, albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired,
@ -157,6 +173,7 @@ AlbumDetailsModalContent.propTypes = {
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
showOpenArtistButton: PropTypes.bool, showOpenArtistButton: PropTypes.bool,
showOpenAlbumButton: PropTypes.bool,
selectedTab: PropTypes.string.isRequired, selectedTab: PropTypes.string.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired, startInteractiveSearch: PropTypes.bool.isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired, onMonitorAlbumPress: PropTypes.func.isRequired,

@ -5,7 +5,6 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import AlbumDetailsModal from './AlbumDetailsModal'; import AlbumDetailsModal from './AlbumDetailsModal';
import EditAlbumModalConnector from './Edit/EditAlbumModalConnector';
import styles from './AlbumSearchCell.css'; import styles from './AlbumSearchCell.css';
class AlbumSearchCell extends Component { class AlbumSearchCell extends Component {
@ -17,8 +16,7 @@ class AlbumSearchCell extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isDetailsModalOpen: false, isDetailsModalOpen: false
isEditAlbumModalOpen: false
}; };
} }
@ -33,14 +31,6 @@ class AlbumSearchCell extends Component {
this.setState({ isDetailsModalOpen: false }); this.setState({ isDetailsModalOpen: false });
} }
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
// //
// Render // Render
@ -67,12 +57,6 @@ class AlbumSearchCell extends Component {
onPress={this.onManualSearchPress} onPress={this.onManualSearchPress}
/> />
<IconButton
name={icons.EDIT}
title="Edit Album"
onPress={this.onEditAlbumPress}
/>
<AlbumDetailsModal <AlbumDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
albumId={albumId} albumId={albumId}
@ -84,12 +68,6 @@ class AlbumSearchCell extends Component {
{...otherProps} {...otherProps}
/> />
<EditAlbumModalConnector
isOpen={this.state.isEditAlbumModalOpen}
albumId={albumId}
artistId={artistId}
onModalClose={this.onEditAlbumModalClose}
/>
</TableRowCell> </TableRowCell>
); );
} }

@ -10,10 +10,9 @@ import AlbumSearchCell from './AlbumSearchCell';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { albumId }) => albumId, (state, { albumId }) => albumId,
(state, { sceneSeasonNumber }) => sceneSeasonNumber,
createArtistSelector(), createArtistSelector(),
createCommandsSelector(), createCommandsSelector(),
(albumId, sceneSeasonNumber, artist, commands) => { (albumId, artist, commands) => {
const isSearching = _.some(commands, (command) => { const isSearching = _.some(commands, (command) => {
const albumSearch = command.name === commandNames.ALBUM_SEARCH; const albumSearch = command.name === commandNames.ALBUM_SEARCH;

@ -11,6 +11,7 @@ import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label'; import Label from 'Components/Label';
import AlbumCover from 'Album/AlbumCover'; import AlbumCover from 'Album/AlbumCover';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector'; import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@ -50,6 +51,7 @@ class AlbumDetails extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isOrganizeModalOpen: false,
isArtistHistoryModalOpen: false, isArtistHistoryModalOpen: false,
isManageTracksOpen: false, isManageTracksOpen: false,
isEditAlbumModalOpen: false, isEditAlbumModalOpen: false,
@ -62,6 +64,14 @@ class AlbumDetails extends Component {
// //
// Listeners // Listeners
onOrganizePress = () => {
this.setState({ isOrganizeModalOpen: true });
}
onOrganizeModalClose = () => {
this.setState({ isOrganizeModalOpen: false });
}
onEditAlbumPress = () => { onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true }); this.setState({ isEditAlbumModalOpen: true });
} }
@ -135,6 +145,7 @@ class AlbumDetails extends Component {
} = this.props; } = this.props;
const { const {
isOrganizeModalOpen,
isArtistHistoryModalOpen, isArtistHistoryModalOpen,
isEditAlbumModalOpen, isEditAlbumModalOpen,
isManageTracksOpen, isManageTracksOpen,
@ -164,6 +175,12 @@ class AlbumDetails extends Component {
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton
label="Preview Rename"
iconName={icons.ORGANIZE}
onPress={this.onOrganizePress}
/>
<PageToolbarButton <PageToolbarButton
label="Manage Tracks" label="Manage Tracks"
iconName={icons.TRACK_FILE} iconName={icons.TRACK_FILE}
@ -364,6 +381,13 @@ class AlbumDetails extends Component {
</div> </div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onOrganizeModalClose}
/>
<TrackFileEditorModal <TrackFileEditorModal
isOpen={isManageTracksOpen} isOpen={isManageTracksOpen}
artistId={artist.id} artistId={artist.id}

@ -26,7 +26,7 @@ class EditAlbumModalContent extends Component {
} }
// //
// Render // Render
render() { render() {

@ -2,41 +2,9 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux'; import { ConnectedRouter } from 'react-router-redux';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import PageConnector from 'Components/Page/PageConnector'; import PageConnector from 'Components/Page/PageConnector';
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector'; import AppRoutes from './AppRoutes';
import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector';
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';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status';
import TasksConnector from 'System/Tasks/TasksConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
function App({ store, history }) { function App({ store, history }) {
return ( return (
@ -44,205 +12,7 @@ function App({ store, history }) {
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<PageConnector> <PageConnector>
<Switch> <AppRoutes app={App} />
{/*
Artist
*/}
<Route
exact={true}
path="/"
component={ArtistIndexConnector}
/>
{
window.Sonarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={App}
/>
);
}}
/>
}
<Route
path="/add/new"
component={AddNewArtistConnector}
/>
<Route
path="/add/import"
component={ImportArtist}
/>
<Route
path="/artisteditor"
component={ArtistEditorConnector}
/>
<Route
path="/albumstudio"
component={AlbumStudioConnector}
/>
<Route
path="/artist/:foreignArtistId"
component={ArtistDetailsPageConnector}
/>
<Route
path="/album/:foreignAlbumId"
component={AlbumDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
<Route
path="/activity/history"
component={HistoryConnector}
/>
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={Quality}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettings}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
{/*
System
*/}
<Route
path="/system/status"
component={Status}
/>
<Route
path="/system/tasks"
component={TasksConnector}
/>
<Route
path="/system/backup"
component={BackupsConnector}
/>
<Route
path="/system/updates"
component={UpdatesConnector}
/>
<Route
path="/system/events"
component={LogsTableConnector}
/>
<Route
path="/system/logs/files"
component={Logs}
/>
{/*
Not Found
*/}
<Route
path="*"
component={NotFound}
/>
</Switch>
</PageConnector> </PageConnector>
</ConnectedRouter> </ConnectedRouter>
</Provider> </Provider>

@ -0,0 +1,249 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector';
import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector';
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';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status';
import TasksConnector from 'System/Tasks/TasksConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
function AppRoutes(props) {
const {
app
} = props;
return (
<Switch>
{/*
Artist
*/}
<Route
exact={true}
path="/"
component={ArtistIndexConnector}
/>
{
window.Lidarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
}
<Route
path="/add/new"
component={AddNewArtistConnector}
/>
<Route
path="/add/import"
component={ImportArtist}
/>
<Route
path="/artisteditor"
component={ArtistEditorConnector}
/>
<Route
path="/albumstudio"
component={AlbumStudioConnector}
/>
<Route
path="/artist/:foreignArtistId"
component={ArtistDetailsPageConnector}
/>
<Route
path="/album/:foreignAlbumId"
component={AlbumDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
<Route
path="/activity/history"
component={HistoryConnector}
/>
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={Quality}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettings}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
{/*
System
*/}
<Route
path="/system/status"
component={Status}
/>
<Route
path="/system/tasks"
component={TasksConnector}
/>
<Route
path="/system/backup"
component={BackupsConnector}
/>
<Route
path="/system/updates"
component={UpdatesConnector}
/>
<Route
path="/system/events"
component={LogsTableConnector}
/>
<Route
path="/system/logs/files"
component={Logs}
/>
{/*
Not Found
*/}
<Route
path="*"
component={NotFound}
/>
</Switch>
);
}
AppRoutes.propTypes = {
app: PropTypes.func.isRequired
};
export default AppRoutes;

@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) {
}, },
onSeeChangesPress() { onSeeChangesPress() {
window.location = `${window.Sonarr.urlBase}/system/updates`; window.location = `${window.Lidarr.urlBase}/system/updates`;
} }
}; };
} }

@ -38,7 +38,7 @@ class ArtistDetailsPageConnector extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (!this.props.foreignArtistId) { if (!this.props.foreignArtistId) {
this.props.push(`${window.Sonarr.urlBase}/`); this.props.push(`${window.Lidarr.urlBase}/`);
return; return;
} }
} }

@ -51,7 +51,7 @@ class EditArtistModalContent extends Component {
this.props.onSavePress(true); this.props.onSavePress(true);
} }
// //
// Render // Render
render() { render() {

@ -5,7 +5,6 @@ import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ArtistPoster from 'Artist/ArtistPoster'; import ArtistPoster from 'Artist/ArtistPoster';

@ -5,6 +5,5 @@
} }
.statusIcon { .statusIcon {
margin-right: 6px; width: 20px !important;
width: 20px;
} }

@ -17,16 +17,9 @@ function createMapStateToProps() {
); );
} }
function createMapDispatchToProps(dispatch) { const mapDispatchToProps = {
return { onNavigatePrevious: gotoCalendarPreviousRange,
onNavigatePrevious() { onNavigateNext: gotoCalendarNextRange
dispatch(gotoCalendarPreviousRange()); };
},
onNavigateNext() { export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);
dispatch(gotoCalendarNextRange());
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarDays);

@ -22,7 +22,7 @@ function getUrls(state) {
tags tags
} = state; } = state;
let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/v1/calendar/Lidarr.ics?`; let icalUrl = `${window.location.host}${window.Lidarr.urlBase}/feed/v1/calendar/Lidarr.ics?`;
if (unmonitored) { if (unmonitored) {
icalUrl += 'unmonitored=true&'; icalUrl += 'unmonitored=true&';
@ -40,7 +40,7 @@ function getUrls(state) {
icalUrl += `tags=${tags.toString()}&`; icalUrl += `tags=${tags.toString()}&`;
} }
icalUrl += `apikey=${window.Sonarr.apiKey}`; icalUrl += `apikey=${window.Lidarr.apiKey}`;
const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`; const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`;
const iCalWebCalUrl = `webcal://${icalUrl}`; const iCalWebCalUrl = `webcal://${icalUrl}`;

@ -14,3 +14,8 @@
.scroller { .scroller {
margin-top: 20px; margin-top: 20px;
} }
.loading {
display: inline-block;
margin-right: auto;
}

@ -3,11 +3,12 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Scroller from 'Components/Scroller/Scroller'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import PathInput from 'Components/Form/PathInput'; import PathInput from 'Components/Form/PathInput';
@ -43,12 +44,15 @@ class FileBrowserModalContent extends Component {
}; };
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps, prevState) {
const { const {
currentPath currentPath
} = this.props; } = this.props;
if (currentPath !== this.state.currentPath) { if (
currentPath !== this.state.currentPath &&
currentPath !== prevState.currentPath
) {
this.setState({ currentPath }); this.setState({ currentPath });
this._scrollerNode.scrollTop = 0; this._scrollerNode.scrollTop = 0;
} }
@ -91,6 +95,9 @@ class FileBrowserModalContent extends Component {
render() { render() {
const { const {
isFetching,
isPopulated,
error,
parent, parent,
directories, directories,
files, files,
@ -125,61 +132,77 @@ class FileBrowserModalContent extends Component {
ref={this.setScrollerRef} ref={this.setScrollerRef}
className={styles.scroller} className={styles.scroller}
> >
<Table columns={columns}> {
<TableBody> !!error &&
{ <div>Error loading contents</div>
emptyParent && }
<FileBrowserRow
type="computer" {
name="My Computer" isPopulated && !error &&
path={parent} <Table columns={columns}>
onPress={this.onRowPress} <TableBody>
/> {
} emptyParent &&
{
!emptyParent && parent &&
<FileBrowserRow
type="parent"
name="..."
path={parent}
onPress={this.onRowPress}
/>
}
{
directories.map((directory) => {
return (
<FileBrowserRow <FileBrowserRow
key={directory.path} type="computer"
type={directory.type} name="My Computer"
name={directory.name} path={parent}
path={directory.path}
onPress={this.onRowPress} onPress={this.onRowPress}
/> />
); }
})
}
{ {
files.map((file) => { !emptyParent && parent &&
return (
<FileBrowserRow <FileBrowserRow
key={file.path} type="parent"
type={file.type} name="..."
name={file.name} path={parent}
path={file.path}
onPress={this.onRowPress} onPress={this.onRowPress}
/> />
); }
})
} {
</TableBody> directories.map((directory) => {
</Table> return (
<FileBrowserRow
key={directory.path}
type={directory.type}
name={directory.name}
path={directory.path}
onPress={this.onRowPress}
/>
);
})
}
{
files.map((file) => {
return (
<FileBrowserRow
key={file.path}
type={file.type}
name={file.name}
path={file.path}
onPress={this.onRowPress}
/>
);
})
}
</TableBody>
</Table>
}
</Scroller> </Scroller>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
@ -200,6 +223,9 @@ class FileBrowserModalContent extends Component {
FileBrowserModalContent.propTypes = { FileBrowserModalContent.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
parent: PropTypes.string, parent: PropTypes.string,
currentPath: PropTypes.string.isRequired, currentPath: PropTypes.string.isRequired,
directories: PropTypes.arrayOf(PropTypes.object).isRequired, directories: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -11,6 +11,9 @@ function createMapStateToProps() {
(state) => state.paths, (state) => state.paths,
(paths) => { (paths) => {
const { const {
isFetching,
isPopulated,
error,
parent, parent,
currentPath, currentPath,
directories, directories,
@ -22,6 +25,9 @@ function createMapStateToProps() {
}); });
return { return {
isFetching,
isPopulated,
error,
parent, parent,
currentPath, currentPath,
directories, directories,

@ -8,12 +8,23 @@ class NumberInput extends Component {
// Listeners // Listeners
onChange = ({ name, value }) => { onChange = ({ name, value }) => {
const {
min,
max
} = this.props;
let newValue = null; let newValue = null;
if (value) { if (value) {
newValue = this.props.isFloat ? parseFloat(value) : parseInt(value); newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
} }
if (min != null && newValue < min) {
newValue = min;
} else if (max != null && newValue > max) {
newValue = max;
}
this.props.onChange({ this.props.onChange({
name, name,
value: newValue value: newValue
@ -40,6 +51,8 @@ class NumberInput extends Component {
NumberInput.propTypes = { NumberInput.propTypes = {
value: PropTypes.number, value: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
isFloat: PropTypes.bool.isRequired, isFloat: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired
}; };

@ -98,7 +98,7 @@ class ClipboardButton extends Component {
className={styles.button} className={styles.button}
{...otherProps} {...otherProps}
> >
<span className={showStateIcon && styles.showStateIcon}> <span className={showStateIcon ? styles.showStateIcon : undefined}>
{ {
showSuccess && showSuccess &&
<span className={styles.stateIconContainer}> <span className={styles.stateIconContainer}>

@ -41,7 +41,8 @@ IconButton.propTypes = {
}; };
IconButton.defaultProps = { IconButton.defaultProps = {
className: styles.button className: styles.button,
size: 12
}; };
export default IconButton; export default IconButton;

@ -47,13 +47,13 @@ class Link extends Component {
el = 'a'; el = 'a';
linkProps.href = to; linkProps.href = to;
linkProps.target = target || '_self'; linkProps.target = target || '_self';
} else if (to.startsWith(window.Sonarr.urlBase)) { } else if (to.startsWith(window.Lidarr.urlBase)) {
el = RouterLink; el = RouterLink;
linkProps.to = to; linkProps.to = to;
linkProps.target = target; linkProps.target = target;
} else { } else {
el = RouterLink; el = RouterLink;
linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`; linkProps.to = `${window.Lidarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target; linkProps.target = target;
} }
} }

@ -32,6 +32,6 @@
.label { .label {
left: 100%; left: 100%;
opacity: 0; visibility: hidden;
} }
} }

@ -129,7 +129,7 @@ class SpinnerErrorButton extends Component {
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
<span className={showIcon && styles.showIcon}> <span className={showIcon ? styles.showIcon : undefined}>
{ {
showIcon && showIcon &&
<span className={styles.iconContainer}> <span className={styles.iconContainer}>

@ -16,6 +16,7 @@ function SpinnerIconButton(props) {
<IconButton <IconButton
name={isSpinning ? (spinningName || name) : name} name={isSpinning ? (spinningName || name) : name}
isDisabled={isDisabled || isSpinning} isDisabled={isDisabled || isSpinning}
isSpinning={isSpinning}
{...otherProps} {...otherProps}
/> />
); );

@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Portal from 'react-portal';
import classNames from 'classnames'; import classNames from 'classnames';
import elementClass from 'element-class'; import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
@ -27,6 +26,8 @@ class Modal extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._node = document.getElementById('modal-root');
this._backgroundRef = null;
this._modalId = getUniqueElememtId(); this._modalId = getUniqueElememtId();
} }
@ -57,6 +58,10 @@ class Modal extends Component {
// //
// Control // Control
_setBackgroundRef = (ref) => {
this._backgroundRef = ref;
}
_openModal() { _openModal() {
openModals.push(this._modalId); openModals.push(this._modalId);
window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keydown', this.onKeyDown);
@ -79,9 +84,9 @@ class Modal extends Component {
const targetElement = this._findEventTarget(event); const targetElement = this._findEventTarget(event);
if (targetElement) { if (targetElement) {
const modalElement = ReactDOM.findDOMNode(this.refs.modal); const backgroundElement = ReactDOM.findDOMNode(this._backgroundRef);
return !modalElement || !modalElement.contains(targetElement); return backgroundElement.isEqualNode(targetElement);
} }
return false; return false;
@ -138,10 +143,6 @@ class Modal extends Component {
} }
} }
onClosePress = (event) => {
this.props.onModalClose();
}
// //
// Render // Render
@ -155,36 +156,32 @@ class Modal extends Component {
isOpen isOpen
} = this.props; } = this.props;
return ( if (!isOpen) {
<Portal return null;
isOpened={isOpen} }
return ReactDOM.createPortal(
<div
className={styles.modalContainer}
> >
<div> <div
{ ref={this._setBackgroundRef}
isOpen && className={backdropClassName}
<div onMouseDown={this.onBackdropBeginPress}
className={styles.modalContainer} onMouseUp={this.onBackdropEndPress}
> >
<div <div
className={backdropClassName} className={classNames(
onMouseDown={this.onBackdropBeginPress} className,
onMouseUp={this.onBackdropEndPress} styles[size]
> )}
<div style={style}
ref="modal" >
className={classNames( {children}
className, </div>
styles[size]
)}
style={style}
>
{children}
</div>
</div>
</div>
}
</div> </div>
</Portal> </div>,
this._node
); );
} }
} }

@ -13,7 +13,7 @@ function NotFound({ message }) {
<img <img
className={styles.image} className={styles.image}
src={`${window.Sonarr.urlBase}/Content/Images/404.png`} src={`${window.Lidarr.urlBase}/Content/Images/404.png`}
/> />
</div> </div>
</PageContent> </PageContent>

@ -19,11 +19,11 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onGoToArtist(foreignArtistId) { onGoToArtist(foreignArtistId) {
dispatch(push(`${window.Sonarr.urlBase}/artist/${foreignArtistId}`)); dispatch(push(`${window.Lidarr.urlBase}/artist/${foreignArtistId}`));
}, },
onGoToAddNewArtist(query) { onGoToAddNewArtist(query) {
dispatch(push(`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(query)}`)); dispatch(push(`${window.Lidarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
} }
}; };
} }

@ -51,10 +51,10 @@ class PageHeader extends Component {
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles.logoContainer}> <div className={styles.logoContainer}>
<Link to={`${window.Sonarr.urlBase}/`}> <Link to={`${window.Lidarr.urlBase}/`}>
<img <img
className={styles.logo} className={styles.logo}
src={`${window.Sonarr.urlBase}/Content/Images/logo.svg`} src={`${window.Lidarr.urlBase}/Content/Images/logo.svg`}
/> />
</Link> </Link>
</div> </div>
@ -74,6 +74,7 @@ class PageHeader extends Component {
className={styles.donate} className={styles.donate}
name={icons.HEART} name={icons.HEART}
to="https://www.paypal.me/Lidarr" to="https://www.paypal.me/Lidarr"
size={14}
/> />
<PageHeaderActionsMenuConnector <PageHeaderActionsMenuConnector
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal} onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}

@ -61,7 +61,7 @@ function PageHeaderActionsMenu(props) {
{ {
formsAuth && formsAuth &&
<MenuItem <MenuItem
to={`${window.Sonarr.urlBase}/logout`} to={`${window.Lidarr.urlBase}/logout`}
noRouter={true} noRouter={true}
> >
<Icon <Icon

@ -15,7 +15,7 @@ import LoadingPage from './LoadingPage';
import Page from './Page'; import Page from './Page';
function testLocalStorage() { function testLocalStorage() {
const key = 'sonarrTest'; const key = 'lidarrTest';
try { try {
localStorage.setItem(key, key); localStorage.setItem(key, key);
@ -64,7 +64,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
dispatchFetchSeries() { dispatchFetchArtist() {
dispatch(fetchArtist()); dispatch(fetchArtist());
}, },
dispatchFetchTags() { dispatchFetchTags() {
@ -109,7 +109,7 @@ class PageConnector extends Component {
componentDidMount() { componentDidMount() {
if (!this.props.isPopulated) { if (!this.props.isPopulated) {
this.props.dispatchFetchSeries(); this.props.dispatchFetchArtist();
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguageProfiles(); this.props.dispatchFetchLanguageProfiles();
@ -133,7 +133,7 @@ class PageConnector extends Component {
const { const {
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchSeries, dispatchFetchArtist,
dispatchFetchTags, dispatchFetchTags,
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchLanguageProfiles, dispatchFetchLanguageProfiles,
@ -171,7 +171,7 @@ PageConnector.propTypes = {
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchSeries: PropTypes.func.isRequired, dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguageProfiles: PropTypes.func.isRequired, dispatchFetchLanguageProfiles: PropTypes.func.isRequired,

@ -415,7 +415,7 @@ class PageSidebar extends Component {
transform transform
} = this.state; } = this.state;
const urlBase = window.Sonarr.urlBase; const urlBase = window.Lidarr.urlBase;
const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname; const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname;
const activeParent = getActiveParent(pathname); const activeParent = getActiveParent(pathname);

@ -80,7 +80,7 @@ class SignalRConnector extends Component {
componentDidMount() { componentDidMount() {
console.log('Starting signalR'); console.log('Starting signalR');
this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey }); this.signalRconnection = $.connection('/signalr', { apiKey: window.Lidarr.apiKey });
this.signalRconnection.stateChanged(this.onStateChanged); this.signalRconnection.stateChanged(this.onStateChanged);
this.signalRconnection.received(this.onReceived); this.signalRconnection.received(this.onReceived);
@ -232,11 +232,12 @@ class SignalRConnector extends Component {
} }
handleTrackFile = (body) => { handleTrackFile = (body) => {
const section = 'trackFiles';
if (body.action === 'updated') { if (body.action === 'updated') {
this.props.updateItem({ this.props.updateItem({ section, ...body.resource });
section: 'trackFiles', } else if (body.action === 'deleted') {
...body.resource this.props.removeItem({ section, id: body.resource.id });
});
} }
} }
@ -335,7 +336,7 @@ class SignalRConnector extends Component {
} }
onReconnecting = () => { onReconnecting = () => {
if (window.Sonarr.unloading) { if (window.Lidarr.unloading) {
return; return;
} }
@ -349,7 +350,7 @@ class SignalRConnector extends Component {
} }
onDisconnected = () => { onDisconnected = () => {
if (window.Sonarr.unloading) { if (window.Lidarr.unloading) {
return; return;
} }

@ -14,14 +14,15 @@ function SpinnerIcon(props) {
return ( return (
<Icon <Icon
name={isSpinning ? (spinningName || name) : name} name={isSpinning ? (spinningName || name) : name}
isSpinning={isSpinning}
{...otherProps} {...otherProps}
/> />
); );
} }
SpinnerIcon.propTypes = { SpinnerIcon.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.object.isRequired,
spinningName: PropTypes.string.isRequired, spinningName: PropTypes.object.isRequired,
isSpinning: PropTypes.bool.isRequired isSpinning: PropTypes.bool.isRequired
}; };

@ -18,7 +18,7 @@ function TableOptionsColumn(props) {
} = props; } = props;
return ( return (
<div className={!isModifiable && styles.notDragable}> <div className={isModifiable ? undefined : styles.notDragable}>
<div <div
className={classNames( className={classNames(
styles.column, styles.column,

@ -0,0 +1,31 @@
.button {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
position: relative;
}
.labelContainer {
composes: labelContainer from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.label {
composes: label from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.indicatorContainer {
position: absolute;
top: 10px;
right: 12px;
}
.indicatorBackground {
color: $themeDarkColor;
}
.enabled {
color: $successColor;
}
.disabled {
color: $dangerColor;
}

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress
} = props;
return (
<Link
className={styles.button}
title={advancedSettings ? 'Shown, click to hide' : 'Hidden, click to show'}
onPress={onAdvancedSettingsPress}
>
<Icon
name={icons.ADVANCED_SETTINGS}
size={21}
/>
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={advancedSettings ? styles.enabled : styles.disabled}
name={advancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? 'Hide Advanced' : 'Show Advanced'}
</div>
</div>
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired
};
export default AdvancedSettingsButton;

@ -14,7 +14,10 @@ class DownloadClientSettings extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._saveCallback = null;
this.state = { this.state = {
isSaving: false,
hasPendingChanges: false hasPendingChanges: false
}; };
} }
@ -22,28 +25,34 @@ class DownloadClientSettings extends Component {
// //
// Listeners // Listeners
setDownloadClientOptionsRef = (ref) => { onChildMounted = (saveCallback) => {
this._downloadClientOptions = ref; this._saveCallback = saveCallback;
} }
onHasPendingChange = (hasPendingChanges) => { onChildStateChange = (payload) => {
this.setState({ this.setState(payload);
hasPendingChanges
});
} }
onSavePress = () => { onSavePress = () => {
this._downloadClientOptions.getWrappedInstance().save(); if (this._saveCallback) {
this._saveCallback();
}
} }
// //
// Render // Render
render() { render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return ( return (
<PageContent title="Download Client Settings"> <PageContent title="Download Client Settings">
<SettingsToolbarConnector <SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges} isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
/> />
@ -51,8 +60,8 @@ class DownloadClientSettings extends Component {
<DownloadClientsConnector /> <DownloadClientsConnector />
<DownloadClientOptionsConnector <DownloadClientOptionsConnector
ref={this.setDownloadClientOptionsRef} onChildMounted={this.onChildMounted}
onHasPendingChange={this.onHasPendingChange} onChildStateChange={this.onChildStateChange}
/> />
<RemotePathMappingsConnector /> <RemotePathMappingsConnector />

@ -60,6 +60,7 @@ class DownloadClient extends Component {
return ( return (
<Card <Card
className={styles.downloadClient} className={styles.downloadClient}
overlayContent={true}
onPress={this.onEditDownloadClientPress} onPress={this.onEditDownloadClientPress}
> >
<div className={styles.name}> <div className={styles.name}>

@ -21,10 +21,10 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchDownloadClientOptions, dispatchFetchDownloadClientOptions: fetchDownloadClientOptions,
setDownloadClientOptionsValue, dispatchSetDownloadClientOptionsValue: setDownloadClientOptionsValue,
saveDownloadClientOptions, dispatchSaveDownloadClientOptions: saveDownloadClientOptions,
clearPendingChanges dispatchClearPendingChanges: clearPendingChanges
}; };
class DownloadClientOptionsConnector extends Component { class DownloadClientOptionsConnector extends Component {
@ -33,31 +33,43 @@ class DownloadClientOptionsConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchDownloadClientOptions(); const {
dispatchFetchDownloadClientOptions,
dispatchSaveDownloadClientOptions,
onChildMounted
} = this.props;
dispatchFetchDownloadClientOptions();
onChildMounted(dispatchSaveDownloadClientOptions);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { const {
this.props.onHasPendingChange(this.props.hasPendingChanges); hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section }); this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveDownloadClientOptions();
} }
// //
// Listeners // Listeners
onInputChange = ({ name, value }) => { onInputChange = ({ name, value }) => {
this.props.setDownloadClientOptionsValue({ name, value }); this.props.dispatchSetDownloadClientOptionsValue({ name, value });
} }
// //
@ -75,18 +87,20 @@ class DownloadClientOptionsConnector extends Component {
DownloadClientOptionsConnector.propTypes = { DownloadClientOptionsConnector.propTypes = {
section: PropTypes.string.isRequired, section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
fetchDownloadClientOptions: PropTypes.func.isRequired, dispatchFetchDownloadClientOptions: PropTypes.func.isRequired,
setDownloadClientOptionsValue: PropTypes.func.isRequired, dispatchSetDownloadClientOptionsValue: PropTypes.func.isRequired,
saveDownloadClientOptions: PropTypes.func.isRequired, dispatchSaveDownloadClientOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired, dispatchClearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
}; };
export default connectSection( export default connectSection(
createMapStateToProps, createMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
undefined, undefined,
{ withRef: true }, undefined,
{ section: 'settings.downloadClientOptions' } { section: 'settings.downloadClientOptions' }
)(DownloadClientOptionsConnector); )(DownloadClientOptionsConnector);

@ -49,6 +49,8 @@ function HostSettings(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="port" name="port"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect" helpTextWarning="Requires restart to take effect"
onChange={onInputChange} onChange={onInputChange}
{...port} {...port}
@ -95,6 +97,8 @@ function HostSettings(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="sslPort" name="sslPort"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect" helpTextWarning="Requires restart to take effect"
onChange={onInputChange} onChange={onInputChange}
{...sslPort} {...sslPort}

@ -74,6 +74,8 @@ function ProxySettings(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="proxyPort" name="proxyPort"
min={1}
max={65535}
onChange={onInputChange} onChange={onInputChange}
{...proxyPort} {...proxyPort}
/> />

@ -14,7 +14,10 @@ class IndexerSettings extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._saveCallback = null;
this.state = { this.state = {
isSaving: false,
hasPendingChanges: false hasPendingChanges: false
}; };
} }
@ -22,28 +25,34 @@ class IndexerSettings extends Component {
// //
// Listeners // Listeners
setIndexerOptionsRef = (ref) => { onChildMounted = (saveCallback) => {
this._indexerOptions = ref; this._saveCallback = saveCallback;
} }
onHasPendingChange = (hasPendingChanges) => { onChildStateChange = (payload) => {
this.setState({ this.setState(payload);
hasPendingChanges
});
} }
onSavePress = () => { onSavePress = () => {
this._indexerOptions.getWrappedInstance().save(); if (this._saveCallback) {
this._saveCallback();
}
} }
// //
// Render // Render
render() { render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return ( return (
<PageContent title="Indexer Settings"> <PageContent title="Indexer Settings">
<SettingsToolbarConnector <SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges} isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
/> />
@ -51,8 +60,8 @@ class IndexerSettings extends Component {
<IndexersConnector /> <IndexersConnector />
<IndexerOptionsConnector <IndexerOptionsConnector
ref={this.setIndexerOptionsRef} onChildMounted={this.onChildMounted}
onHasPendingChange={this.onHasPendingChange} onChildStateChange={this.onChildStateChange}
/> />
<RestrictionsConnector /> <RestrictionsConnector />

@ -76,6 +76,7 @@ class Indexer extends Component {
return ( return (
<Card <Card
className={styles.indexer} className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress} onPress={this.onEditIndexerPress}
> >
<div className={styles.name}> <div className={styles.name}>

@ -41,6 +41,7 @@ function IndexerOptions(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="minimumAge" name="minimumAge"
min={0}
helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider." helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
onChange={onInputChange} onChange={onInputChange}
{...settings.minimumAge} {...settings.minimumAge}
@ -53,6 +54,7 @@ function IndexerOptions(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="maximumSize" name="maximumSize"
min={0}
helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited." helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited."
onChange={onInputChange} onChange={onInputChange}
{...settings.maximumSize} {...settings.maximumSize}
@ -65,6 +67,7 @@ function IndexerOptions(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="retention" name="retention"
min={0}
helpText="Usenet only: Set to zero to set for unlimited retention" helpText="Usenet only: Set to zero to set for unlimited retention"
onChange={onInputChange} onChange={onInputChange}
{...settings.retention} {...settings.retention}
@ -80,6 +83,7 @@ function IndexerOptions(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="rssSyncInterval" name="rssSyncInterval"
min={0}
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)" helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
helpTextWarning="This will apply to all indexers, please follow the rules set forth by them" helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
helpLink="https://github.com/Sonarr/Sonarr/wiki/RSS-Sync" helpLink="https://github.com/Sonarr/Sonarr/wiki/RSS-Sync"

@ -21,10 +21,10 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchIndexerOptions, dispatchFetchIndexerOptions: fetchIndexerOptions,
setIndexerOptionsValue, dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
saveIndexerOptions, dispatchSaveIndexerOptions: saveIndexerOptions,
clearPendingChanges dispatchClearPendingChanges: clearPendingChanges
}; };
class IndexerOptionsConnector extends Component { class IndexerOptionsConnector extends Component {
@ -33,31 +33,43 @@ class IndexerOptionsConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchIndexerOptions(); const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { const {
this.props.onHasPendingChange(this.props.hasPendingChanges); hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section }); this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveIndexerOptions();
} }
// //
// Listeners // Listeners
onInputChange = ({ name, value }) => { onInputChange = ({ name, value }) => {
this.props.setIndexerOptionsValue({ name, value }); this.props.dispatchSetIndexerOptionsValue({ name, value });
} }
// //
@ -75,18 +87,20 @@ class IndexerOptionsConnector extends Component {
IndexerOptionsConnector.propTypes = { IndexerOptionsConnector.propTypes = {
section: PropTypes.string.isRequired, section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
fetchIndexerOptions: PropTypes.func.isRequired, dispatchFetchIndexerOptions: PropTypes.func.isRequired,
setIndexerOptionsValue: PropTypes.func.isRequired, dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
saveIndexerOptions: PropTypes.func.isRequired, dispatchSaveIndexerOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired, dispatchClearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
}; };
export default connectSection( export default connectSection(
createMapStateToProps, createMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
undefined, undefined,
{ withRef: true }, undefined,
{ section: 'settings.indexerOptions' } { section: 'settings.indexerOptions' }
)(IndexerOptionsConnector); )(IndexerOptionsConnector);

@ -64,6 +64,7 @@ class Restriction extends Component {
return ( return (
<Card <Card
className={styles.restriction} className={styles.restriction}
overlayContent={true}
onPress={this.onEditRestrictionPress} onPress={this.onEditRestrictionPress}
> >
<div> <div>

@ -52,6 +52,7 @@ class Metadata extends Component {
return ( return (
<Card <Card
className={styles.metadata} className={styles.metadata}
overlayContent={true}
onPress={this.onEditMetadataPress} onPress={this.onEditMetadataPress}
> >
<div className={styles.name}> <div className={styles.name}>

@ -79,6 +79,7 @@ class Notification extends Component {
return ( return (
<Card <Card
className={styles.notification} className={styles.notification}
overlayContent={true}
onPress={this.onEditNotificationPress} onPress={this.onEditNotificationPress}
> >
<div className={styles.name}> <div className={styles.name}>

@ -101,7 +101,7 @@ function EditLanguageProfileModalContent(props) {
id && id &&
<div <div
className={styles.deleteButtonContainer} className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a language profile that is attached to a artist'} title={isInUse ? 'Can\'t delete a language profile that is attached to a artist' : undefined}
> >
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}

@ -97,7 +97,7 @@ function EditMetadataProfileModalContent(props) {
id && id &&
<div <div
className={styles.deleteButtonContainer} className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a metadata profile that is attached to a artist'} title={isInUse ? 'Can\'t delete a metadata profile that is attached to a artist' : undefined}
> >
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}

@ -200,7 +200,7 @@ class EditQualityProfileModalContent extends Component {
id && id &&
<div <div
className={styles.deleteButtonContainer} className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a quality profile that is attached to a artist'} title={isInUse ? 'Can\'t delete a quality profile that is attached to a artist' : undefined}
> >
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}

@ -28,6 +28,29 @@ function getValue(value) {
class QualityDefinition extends Component { class QualityDefinition extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._forceUpdateTimeout = null;
}
componentDidMount() {
// A hack to deal with a bug in the slider component until a fix for it
// lands and an updated version is available.
// See: https://github.com/mpowaga/react-slider/issues/115
this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1);
}
componentWillUnmount() {
if (this._forceUpdateTimeout) {
clearTimeout(this._forceUpdateTimeout);
}
}
// //
// Listeners // Listeners
@ -131,6 +154,8 @@ class QualityDefinition extends Component {
<NumberInput <NumberInput
className={styles.sizeInput} className={styles.sizeInput}
name={`${id}.min`} name={`${id}.min`}
min={slider.min}
max={maxSize ? maxSize - 10 : slider.max - 10}
value={minSize || slider.min} value={minSize || slider.min}
isFloat={true} isFloat={true}
onChange={this.onMinSizeChange} onChange={this.onMinSizeChange}
@ -143,6 +168,7 @@ class QualityDefinition extends Component {
<NumberInput <NumberInput
className={styles.sizeInput} className={styles.sizeInput}
name={`${id}.max`} name={`${id}.max`}
min={minSize + 10}
value={maxSize || slider.max} value={maxSize || slider.max}
isFloat={true} isFloat={true}
onChange={this.onMaxSizeChange} onChange={this.onMaxSizeChange}

@ -40,7 +40,7 @@ class QualityDefinitionConnector extends Component {
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize }); this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
} }
if (minSize !== currentMaxSize) { if (maxSize !== currentMaxSize) {
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize }); this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
} }
} }

@ -26,8 +26,8 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchQualityDefinitions, dispatchFetchQualityDefinitions: fetchQualityDefinitions,
saveQualityDefinitions dispatchSaveQualityDefinitions: saveQualityDefinitions
}; };
class QualityDefinitionsConnector extends Component { class QualityDefinitionsConnector extends Component {
@ -36,24 +36,34 @@ class QualityDefinitionsConnector extends Component {
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.fetchQualityDefinitions(); this.props.dispatchFetchQualityDefinitions();
}
componentDidUpdate(prevProps) {
const { const {
hasPendingChanges dispatchFetchQualityDefinitions,
dispatchSaveQualityDefinitions,
onChildMounted
} = this.props; } = this.props;
if (hasPendingChanges !== prevProps.hasPendingChanges) { dispatchFetchQualityDefinitions();
this.props.onHasPendingChange(hasPendingChanges); onChildMounted(dispatchSaveQualityDefinitions);
}
} }
// componentDidUpdate(prevProps) {
// Control const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
save = () => { if (
this.props.saveQualityDefinitions(); prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
} }
// //
@ -69,10 +79,12 @@ class QualityDefinitionsConnector extends Component {
} }
QualityDefinitionsConnector.propTypes = { QualityDefinitionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
fetchQualityDefinitions: PropTypes.func.isRequired, dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
saveQualityDefinitions: PropTypes.func.isRequired, dispatchSaveQualityDefinitions: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector); export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector);

@ -12,7 +12,10 @@ class Quality extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._saveCallback = null;
this.state = { this.state = {
isSaving: false,
hasPendingChanges: false hasPendingChanges: false
}; };
} }
@ -20,35 +23,41 @@ class Quality extends Component {
// //
// Listeners // Listeners
setQualityDefinitionsRef = (ref) => { onChildMounted = (saveCallback) => {
this._qualityDefinitions = ref; this._saveCallback = saveCallback;
} }
onHasPendingChange = (hasPendingChanges) => { onChildStateChange = (payload) => {
this.setState({ this.setState(payload);
hasPendingChanges
});
} }
onSavePress = () => { onSavePress = () => {
this._qualityDefinitions.getWrappedInstance().save(); if (this._saveCallback) {
this._saveCallback();
}
} }
// //
// Render // Render
render() { render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return ( return (
<PageContent title="Quality Settings"> <PageContent title="Quality Settings">
<SettingsToolbarConnector <SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges} isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
/> />
<PageContentBodyConnector> <PageContentBodyConnector>
<QualityDefinitionsConnector <QualityDefinitionsConnector
ref={this.setQualityDefinitionsRef} onChildMounted={this.onChildMounted}
onHasPendingChange={this.onHasPendingChange} onChildStateChange={this.onChildStateChange}
/> />
</PageContentBodyConnector> </PageContentBodyConnector>
</PageContent> </PageContent>

@ -1,7 +0,0 @@
.advancedSettings {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.advancedSettingsEnabled {
color: $toobarButtonHoverColor;
}

@ -1,13 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PendingChangesModal from './PendingChangesModal'; import PendingChangesModal from './PendingChangesModal';
import styles from './SettingsToolbar.css'; import AdvancedSettingsButton from './AdvancedSettingsButton';
class SettingsToolbar extends Component { class SettingsToolbar extends Component {
@ -53,14 +52,9 @@ class SettingsToolbar extends Component {
return ( return (
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <AdvancedSettingsButton
label={advancedSettings ? 'Hide Advanced' : 'Show Advanced'} advancedSettings={advancedSettings}
className={classNames( onAdvancedSettingsPress={onAdvancedSettingsPress}
styles.advancedSettings,
advancedSettings && styles.advancedSettingsEnabled
)}
iconName={icons.ADVANCED_SETTINGS}
onPress={onAdvancedSettingsPress}
/> />
{ {

@ -1,4 +1,4 @@
if (window.Sonarr.analytics) { if (window.Lidarr.analytics) {
const d = document; const d = document;
const g = d.createElement('script'); const g = d.createElement('script');
const s = d.getElementsByTagName('script')[0]; const s = d.getElementsByTagName('script')[0];

@ -7,7 +7,7 @@ import updateSectionState from 'Utilities/State/updateSectionState';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler'; import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
import { clearPendingChanges, update } from 'Store/Actions/baseActions'; import { clearPendingChanges, set, update } from 'Store/Actions/baseActions';
// //
// Variables // Variables
@ -70,6 +70,11 @@ export default {
return; return;
} }
dispatch(set({
section,
isSaving: true
}));
const promise = $.ajax({ const promise = $.ajax({
method: 'PUT', method: 'PUT',
url: '/qualityDefinition/update', url: '/qualityDefinition/update',
@ -78,10 +83,24 @@ export default {
promise.done((data) => { promise.done((data) => {
dispatch(batchActions([ dispatch(batchActions([
set({
section,
isSaving: false,
saveError: null
}),
update({ section, data }), update({ section, data }),
clearPendingChanges({ section }) clearPendingChanges({ section })
])); ]));
}); });
promise.fail((xhr) => {
dispatch(set({
section,
isSaving: false,
saveError: xhr
}));
});
} }
}, },

@ -31,7 +31,7 @@ export const defaultState = {
messages: { messages: {
items: [] items: []
}, },
version: window.Sonarr.version, version: window.Lidarr.version,
isUpdated: false, isUpdated: false,
isConnected: true, isConnected: true,
isReconnecting: false, isReconnecting: false,

@ -176,25 +176,23 @@ export const actionHandlers = handleThunks({
[FETCH_QUEUE_STATUS]: createFetchHandler(status, '/queue/status'), [FETCH_QUEUE_STATUS]: createFetchHandler(status, '/queue/status'),
[FETCH_QUEUE_DETAILS]: function(payload) { [FETCH_QUEUE_DETAILS]: function(getState, payload, dispatch) {
return function(dispatch, getState) { let params = payload;
let params = payload;
// If the payload params are empty try to get params from state. // If the payload params are empty try to get params from state.
if (params && !_.isEmpty(params)) { if (params && !_.isEmpty(params)) {
dispatch(set({ section: details, params })); dispatch(set({ section: details, params }));
} else { } else {
params = getState().queue.details.params; params = getState().queue.details.params;
} }
// Ensure there are params before trying to fetch the queue // Ensure there are params before trying to fetch the queue
// so we don't make a bad request to the server. // so we don't make a bad request to the server.
if (params && !_.isEmpty(params)) { if (params && !_.isEmpty(params)) {
fetchQueueDetailsHelper(getState, params, dispatch); fetchQueueDetailsHelper(getState, params, dispatch);
} }
};
}, },
...createServerSideCollectionHandlers( ...createServerSideCollectionHandlers(

@ -87,7 +87,7 @@ const config = {
slicer, slicer,
serialize, serialize,
merge, merge,
key: 'sonarr' key: 'lidarr'
}; };
export default persistState(paths, config); export default persistState(paths, config);

@ -25,7 +25,7 @@ export default function sentryMiddleware() {
version, version,
release, release,
isProduction isProduction
} = window.Sonarr; } = window.Lidarr;
if (!analytics) { if (!analytics) {
return; return;

@ -1,3 +1,3 @@
export default function getPathWithUrlBase(path) { export default function getPathWithUrlBase(path) {
return `${window.Sonarr.urlBase}${path}`; return `${window.Lidarr.urlBase}${path}`;
} }

@ -64,6 +64,7 @@ function CutoffUnmetRow(props) {
albumEntity={albumEntities.WANTED_CUTOFF_UNMET} albumEntity={albumEntities.WANTED_CUTOFF_UNMET}
albumTitle={title} albumTitle={title}
showOpenArtistButton={true} showOpenArtistButton={true}
showOpenAlbumButton={true}
/> />
</TableRowCell> </TableRowCell>
); );

@ -81,6 +81,7 @@ function MissingRow(props) {
albumEntity={albumEntities.WANTED_MISSING} albumEntity={albumEntities.WANTED_MISSING}
albumTitle={title} albumTitle={title}
showOpenArtistButton={true} showOpenArtistButton={true}
showOpenAlbumButton={true}
/> />
</TableRowCell> </TableRowCell>
); );

@ -53,7 +53,7 @@
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
window.Sonarr = { window.Lidarr = {
apiRoot: 'API_ROOT', apiRoot: 'API_ROOT',
apiKey: 'API_KEY', apiKey: 'API_KEY',
release: 'APP_RELEASE', release: 'APP_RELEASE',

@ -1,8 +1,8 @@
import $ from 'jquery'; import $ from 'jquery';
const absUrlRegex = /^(https?:)?\/\//i; const absUrlRegex = /^(https?:)?\/\//i;
const apiRoot = window.Sonarr.apiRoot; const apiRoot = window.Lidarr.apiRoot;
const urlBase = window.Sonarr.urlBase; const urlBase = window.Lidarr.urlBase;
function isRelative(xhr) { function isRelative(xhr) {
return !absUrlRegex.test(xhr.url); return !absUrlRegex.test(xhr.url);
@ -31,7 +31,7 @@ function addRootUrl(xhr) {
function addApiKey(xhr) { function addApiKey(xhr) {
xhr.headers = xhr.headers || {}; xhr.headers = xhr.headers || {};
xhr.headers['X-Api-Key'] = window.Sonarr.apiKey; xhr.headers['X-Api-Key'] = window.Lidarr.apiKey;
} }
export default function() { export default function() {

@ -1,4 +1,4 @@
/* eslint no-undef: 0 */ /* eslint no-undef: 0 */
import 'Shims/jquery'; import 'Shims/jquery';
__webpack_public_path__ = `${window.Sonarr.urlBase}/`; __webpack_public_path__ = `${window.Lidarr.urlBase}/`;

@ -33,87 +33,86 @@
"classnames": "2.2.5", "classnames": "2.2.5",
"clipboard": "1.7.1", "clipboard": "1.7.1",
"create-react-class": "^15.6.2", "create-react-class": "^15.6.2",
"css-loader": "0.28.7", "css-loader": "0.28.9",
"del": "3.0.0", "del": "3.0.0",
"element-class": "0.2.2", "element-class": "0.2.2",
"esformatter": "0.10.0", "esformatter": "0.10.0",
"eslint": "4.8.0", "eslint": "4.16.0",
"eslint-loader": "1.9.0", "eslint-loader": "1.9.0",
"eslint-plugin-filenames": "1.2.0", "eslint-plugin-filenames": "1.2.0",
"eslint-plugin-react": "7.4.0", "eslint-plugin-react": "7.5.1",
"esprint": "0.4.0", "esprint": "0.4.0",
"extract-text-webpack-plugin": "3.0.1", "extract-text-webpack-plugin": "3.0.2",
"file-loader": "1.1.5", "file-loader": "1.1.6",
"filesize": "3.5.10", "filesize": "3.5.11",
"gulp": "3.9.1", "gulp": "3.9.1",
"gulp-cached": "1.1.1", "gulp-cached": "1.1.1",
"gulp-clean-css": "3.9.0", "gulp-clean-css": "3.9.2",
"gulp-concat": "2.6.1", "gulp-concat": "2.6.1",
"gulp-declare": "0.3.0", "gulp-declare": "0.3.0",
"gulp-livereload": "3.8.1", "gulp-livereload": "3.8.1",
"gulp-postcss": "7.0.0", "gulp-postcss": "7.0.1",
"gulp-print": "2.0.1", "gulp-print": "2.0.1",
"gulp-sourcemaps": "2.6.1", "gulp-sourcemaps": "2.6.3",
"gulp-stripbom": "1.0.4", "gulp-stripbom": "1.0.4",
"gulp-util": "3.0.8", "gulp-util": "3.0.8",
"gulp-watch": "4.3.11", "gulp-watch": "5.0.0",
"gulp-wrap": "0.13.0", "gulp-wrap": "0.13.0",
"history": "4.7.2", "history": "4.7.2",
"jdu": "1.0.0", "jdu": "1.0.0",
"jquery": "3.2.1", "jquery": "3.3.1",
"loader-utils": "^1.1.0", "loader-utils": "^1.1.0",
"lodash": "4.17.4", "lodash": "4.17.4",
"mobile-detect": "1.3.7", "mobile-detect": "1.4.1",
"moment": "2.18.1", "moment": "2.20.1",
"mousetrap": "1.6.1", "mousetrap": "1.6.1",
"normalize.css": "7.0.0", "normalize.css": "7.0.0",
"postcss-loader": "2.0.6", "postcss-loader": "2.0.10",
"postcss-mixins": "6.1.1", "postcss-mixins": "6.2.0",
"postcss-nested": "2.1.2", "postcss-nested": "3.0.0",
"postcss-simple-vars": "4.1.0", "postcss-simple-vars": "4.1.0",
"prop-types": "15.6.0", "prop-types": "15.6.0",
"qs": "6.5.1", "qs": "6.5.1",
"query-string": "5.0.0", "query-string": "5.0.1",
"raven-for-redux": "1.0.0", "raven-for-redux": "1.0.0",
"raven-js": "3.17.0", "raven-js": "3.17.0",
"react": "15.6.0", "react": "16.2.0",
"react-addons-shallow-compare": "15.6.2", "react-addons-shallow-compare": "15.6.2",
"react-async-script": "0.9.1", "react-async-script": "0.9.1",
"react-autosuggest": "9.3.2", "react-autosuggest": "9.3.2",
"react-custom-scrollbars": "4.1.2", "react-custom-scrollbars": "4.2.1",
"react-dnd": "2.5.4", "react-dnd": "2.5.4",
"react-dnd-html5-backend": "2.5.4", "react-dnd-html5-backend": "2.5.4",
"react-document-title": "2.0.3", "react-document-title": "2.0.3",
"react-dom": "15.6.0", "react-dom": "16.2.0",
"react-google-recaptcha": "0.9.7", "react-google-recaptcha": "0.9.9",
"react-lazyload": "2.2.7", "react-lazyload": "2.3.0",
"react-measure": "1.4.7", "react-measure": "1.4.7",
"react-portal": "3.1.0",
"react-redux": "5.0.6", "react-redux": "5.0.6",
"react-router-dom": "4.2.2", "react-router-dom": "4.2.2",
"react-router-redux": "5.0.0-alpha.6", "react-router-redux": "5.0.0-alpha.9",
"react-slider": "0.9.0", "react-slider": "0.9.0",
"react-tabs": "2.1.0", "react-tabs": "2.2.1",
"react-tag-autocomplete": "5.4.1", "react-tag-autocomplete": "5.5.0",
"react-tether": "0.5.7", "react-tether": "0.6.1",
"react-text-truncate": "0.12.0", "react-text-truncate": "0.12.1",
"react-virtualized": "9.10.1", "react-virtualized": "9.18.0",
"redux": "3.7.2", "redux": "3.7.2",
"redux-actions": "2.2.1", "redux-actions": "2.2.1",
"redux-batched-actions": "0.2.0", "redux-batched-actions": "0.2.1",
"redux-localstorage": "0.4.1", "redux-localstorage": "0.4.1",
"redux-thunk": "2.2.0", "redux-thunk": "2.2.0",
"require-nocache": "1.0.0", "require-nocache": "1.0.0",
"reselect": "3.0.1", "reselect": "3.0.1",
"run-sequence": "2.2.0", "run-sequence": "2.2.1",
"signalr": "2.2.2", "signalr": "2.2.2",
"streamqueue": "1.1.1", "streamqueue": "1.1.2",
"style-loader": "0.19.0", "style-loader": "0.19.1",
"stylelint": "8.2.0", "stylelint": "8.4.0",
"stylelint-order": "0.7.0", "stylelint-order": "0.8.0",
"tar.gz": "1.0.5", "tar.gz": "1.0.7",
"url-loader": "0.6.2", "url-loader": "0.6.2",
"webpack": "3.6.0", "webpack": "3.10.0",
"webpack-stream": "^4.0.0" "webpack-stream": "^4.0.0"
}, },
"main": "index.js" "main": "index.js"

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Nancy.Responses; using Nancy.Responses;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -16,12 +17,18 @@ namespace Lidarr.Api.V1.Queue
{ {
private readonly IQueueService _queueService; private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService; private readonly IPendingReleaseService _pendingReleaseService;
private readonly Debouncer _broadcastDebounce;
public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService)
: base(broadcastSignalRMessage, "queue/status") : base(broadcastSignalRMessage, "queue/status")
{ {
_queueService = queueService; _queueService = queueService;
_pendingReleaseService = pendingReleaseService; _pendingReleaseService = pendingReleaseService;
_broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5));
Get["/"] = x => GetQueueStatusResponse(); Get["/"] = x => GetQueueStatusResponse();
} }
@ -32,25 +39,38 @@ namespace Lidarr.Api.V1.Queue
private QueueStatusResource GetQueueStatus() private QueueStatusResource GetQueueStatus()
{ {
_broadcastDebounce.Pause();
var queue = _queueService.GetQueue(); var queue = _queueService.GetQueue();
var pending = _pendingReleaseService.GetPendingQueue(); var pending = _pendingReleaseService.GetPendingQueue();
return new QueueStatusResource var resource = new QueueStatusResource
{ {
Count = queue.Count + pending.Count, Count = queue.Count + pending.Count,
Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)),
Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase))
}; };
_broadcastDebounce.Resume();
return resource;
} }
public void Handle(QueueUpdatedEvent message) private void BroadcastChange()
{ {
BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); BroadcastResourceChange(ModelAction.Updated, GetQueueStatus());
} }
public void Handle(QueueUpdatedEvent message)
{
_broadcastDebounce.Execute();
}
public void Handle(PendingReleasesUpdatedEvent message) public void Handle(PendingReleasesUpdatedEvent message)
{ {
BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); _broadcastDebounce.Execute();
} }
} }
} }

@ -17,8 +17,9 @@ using HttpStatusCode = System.Net.HttpStatusCode;
namespace Lidarr.Api.V1.TrackFiles namespace Lidarr.Api.V1.TrackFiles
{ {
public class TrackModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>, public class TrackFileModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>,
IHandle<TrackFileAddedEvent> IHandle<TrackFileAddedEvent>,
IHandle<TrackFileDeletedEvent>
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IDeleteMediaFiles _mediaFileDeletionService; private readonly IDeleteMediaFiles _mediaFileDeletionService;
@ -26,7 +27,7 @@ namespace Lidarr.Api.V1.TrackFiles
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
public TrackModule(IBroadcastSignalRMessage signalRBroadcaster, public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService, IDeleteMediaFiles mediaFileDeletionService,
IArtistService artistService, IArtistService artistService,
@ -170,5 +171,11 @@ namespace Lidarr.Api.V1.TrackFiles
{ {
BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id);
} }
public void Handle(TrackFileDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.TrackFile.Id);
}
} }
} }

@ -2,6 +2,7 @@ using System.Collections.Generic;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events; using NzbDrone.Core.Music.Events;
@ -15,7 +16,8 @@ namespace Lidarr.Api.V1.Tracks
{ {
public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>, public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track>,
IHandle<TrackInfoRefreshedEvent>, IHandle<TrackInfoRefreshedEvent>,
IHandle<TrackImportedEvent> IHandle<TrackImportedEvent>,
IHandle<TrackFileDeletedEvent>
{ {
protected readonly ITrackService _trackService; protected readonly ITrackService _trackService;
protected readonly IArtistService _artistService; protected readonly IArtistService _artistService;
@ -131,5 +133,13 @@ namespace Lidarr.Api.V1.Tracks
} }
} }
public void Handle(TrackFileDeletedEvent message)
{
foreach (var track in message.TrackFile.Tracks.Value)
{
BroadcastResourceChange(ModelAction.Deleted, track.Id);
}
}
} }
} }

@ -35,8 +35,15 @@ namespace Lidarr.Http
protected void BroadcastResourceChange(ModelAction action, int id) protected void BroadcastResourceChange(ModelAction action, int id)
{ {
var resource = GetResourceById(id); if (action == ModelAction.Deleted)
BroadcastResourceChange(action, resource); {
BroadcastResourceChange(action, new TResource { Id = id });
}
else
{
var resource = GetResourceById(id);
BroadcastResourceChange(action, resource);
}
} }

@ -83,6 +83,10 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(new TrackFileMoveResult()); .Returns(new TrackFileMoveResult());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build(); _downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<TrackFile>());
} }
[Test] [Test]
@ -198,5 +202,19 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IUpgradeMediaFiles>() Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false), Times.Once()); .Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false), Times.Once());
} }
[Test]
public void should_delete_existing_metadata_files_with_the_same_path()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());
}
} }
} }

@ -26,7 +26,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
var monoVersion = _platformInfo.Version; var monoVersion = _platformInfo.Version;
if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1")) if (monoVersion == new Version("4.4") || monoVersion == new Version("4.4.1"))
{ {
_logger.Debug("Mono version {0}", monoVersion); _logger.Debug("Mono version {0}", monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version"); return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version");
@ -34,7 +34,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
if (monoVersion >= new Version("4.4")) if (monoVersion >= new Version("4.4"))
{ {
_logger.Debug("Mono version is 4.6 or better: {0}", monoVersion); _logger.Debug("Mono version is 4.4 or better: {0}", monoVersion);
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }

@ -248,6 +248,12 @@ namespace NzbDrone.Core.History
_logger.Debug("Removing track file from DB as part of cleanup routine, not creating history event."); _logger.Debug("Removing track file from DB as part of cleanup routine, not creating history event.");
return; return;
} }
else if (message.Reason == DeleteMediaFileReason.ManualOverride)
{
_logger.Debug("Removing track file from DB as part of manual override of existing file, not creating history event.");
return;
}
foreach (var track in message.TrackFile.Tracks.Value) foreach (var track in message.TrackFile.Tracks.Value)
{ {

@ -1,10 +1,11 @@
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public enum DeleteMediaFileReason public enum DeleteMediaFileReason
{ {
MissingFromDisk, MissingFromDisk,
Manual, Manual,
Upgrade, Upgrade,
NoLinkedEpisodes NoLinkedEpisodes,
ManualOverride
} }
} }

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles
List<TrackFile> GetFilesByArtist(int artistId); List<TrackFile> GetFilesByArtist(int artistId);
List<TrackFile> GetFilesByAlbum(int albumId); List<TrackFile> GetFilesByAlbum(int albumId);
List<TrackFile> GetFilesWithoutMediaInfo(); List<TrackFile> GetFilesWithoutMediaInfo();
List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath);
} }
@ -35,5 +36,13 @@ namespace NzbDrone.Core.MediaFiles
{ {
return Query.Where(c => c.AlbumId == albumId).ToList(); return Query.Where(c => c.AlbumId == albumId).ToList();
} }
public List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath)
{
return Query.Where(c => c.ArtistId == artistId)
.AndWhere(c => c.RelativePath == relativePath)
.ToList();
}
} }
} }

@ -24,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles
List<string> FilterExistingFiles(List<string> files, Artist artist); List<string> FilterExistingFiles(List<string> files, Artist artist);
TrackFile Get(int id); TrackFile Get(int id);
List<TrackFile> Get(IEnumerable<int> ids); List<TrackFile> Get(IEnumerable<int> ids);
List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath);
} }
@ -98,6 +99,11 @@ namespace NzbDrone.Core.MediaFiles
return _mediaFileRepository.Get(ids).ToList(); return _mediaFileRepository.Get(ids).ToList();
} }
public List<TrackFile> GetFilesWithRelativePath(int artistId, string relativePath)
{
return _mediaFileRepository.GetFilesWithRelativePath(artistId, relativePath);
}
public void HandleAsync(ArtistDeletedEvent message) public void HandleAsync(ArtistDeletedEvent message)
{ {
var files = GetFilesByArtist(message.Artist.Id); var files = GetFilesByArtist(message.Artist.Id);

@ -147,7 +147,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
} }
Logger.Debug() Logger.Debug()
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioCodecLibrary)) .Message("Unknown audio format: '{0}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioCodecLibrary))
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, audioFormat, audioCodecID) .WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, audioFormat, audioCodecID)
.Write(); .Write();

@ -115,6 +115,15 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
else else
{ {
trackFile.RelativePath = localTrack.Artist.Path.GetRelativePath(trackFile.Path); trackFile.RelativePath = localTrack.Artist.Path.GetRelativePath(trackFile.Path);
// Delete existing files from the DB mapped to this path
var previousFiles = _mediaFileService.GetFilesWithRelativePath(localTrack.Artist.Id, trackFile.RelativePath);
foreach (var previousFile in previousFiles)
{
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride);
}
} }
_mediaFileService.Add(trackFile); _mediaFileService.Add(trackFile);

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save