New: Album Studio is now part of artists list

(cherry picked from commit bdcfef80d627e777d7932c54cda04cbe7c656ffc)

Load albums for album details on mouse hover
pull/4254/head
Mark McDowall 1 year ago committed by Bogdan
parent de6c9589d0
commit 72267d3cb4

@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import Artist from 'Artist/Artist';
export interface Statistics {
trackCount: number;
@ -9,13 +10,16 @@ export interface Statistics {
}
interface Album extends ModelBase {
artist: Artist;
foreignAlbumId: string;
title: string;
overview: string;
disambiguation?: string;
albumType: string;
monitored: boolean;
releaseDate: string;
statistics: Statistics;
isSaving?: boolean;
}
export default Album;

@ -0,0 +1,8 @@
import Album from 'Album/Album';
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
interface AlbumAppState extends AppSectionState<Album>, AppSectionDeleteState {}
export default AlbumAppState;

@ -1,3 +1,4 @@
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@ -35,6 +36,7 @@ export interface CustomFilter {
}
interface AppState {
albums: AlbumAppState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
settings: SettingsAppState;

@ -23,6 +23,7 @@ export interface Ratings {
interface Artist extends ModelBase {
added: string;
artistMetadataId: string;
foreignArtistId: string;
cleanName: string;
ended: boolean;
@ -37,7 +38,6 @@ interface Artist extends ModelBase {
metadataProfileId: number;
ratings: Ratings;
rootFolderPath: string;
albums: Album[];
sortName: string;
statistics: Statistics;
status: string;

@ -0,0 +1,10 @@
.albums {
display: flex;
flex-wrap: wrap;
}
.truncated {
align-self: center;
flex: 0 0 100%;
padding: 4px 6px;
}

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'albums': string;
'truncated': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,88 @@
import _ from 'lodash';
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
import createArtistAlbumsSelector from 'Store/Selectors/createArtistAlbumsSelector';
import translate from 'Utilities/String/translate';
import AlbumStudioAlbum from './AlbumStudioAlbum';
import styles from './AlbumDetails.css';
interface AlbumDetailsProps {
artistId: number;
}
function AlbumDetails(props: AlbumDetailsProps) {
const { artistId } = props;
const {
isFetching,
isPopulated,
error,
items: albums,
} = useSelector(createArtistAlbumsSelector(artistId));
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchAlbums({ artistId }));
return () => {
dispatch(clearAlbums());
};
}, [dispatch, artistId]);
const latestAlbums = useMemo(() => {
const sortedAlbums = _.orderBy(albums, 'releaseDate', 'desc');
return sortedAlbums.slice(0, 20);
}, [albums]);
return (
<div className={styles.albums}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('AlbumsLoadError')}</Alert>
) : null}
{isPopulated && !error
? latestAlbums.map((album) => {
const {
id: albumId,
title,
disambiguation,
albumType,
monitored,
statistics,
isSaving,
} = album;
return (
<AlbumStudioAlbum
key={albumId}
artistId={artistId}
albumId={albumId}
title={title}
disambiguation={disambiguation}
albumType={albumType}
monitored={monitored}
statistics={statistics}
isSaving={isSaving}
/>
);
})
: null}
{latestAlbums.length < albums.length ? (
<div className={styles.truncated}>
{translate('AlbumStudioTruncated')}
</div>
) : null}
</div>
);
}
export default AlbumDetails;

@ -0,0 +1,39 @@
.album {
display: flex;
align-items: stretch;
overflow: hidden;
margin: 2px 4px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--albumBackgroundColor);
cursor: default;
}
.info {
padding: 0 4px;
}
.albumType {
padding: 0 4px;
border-width: 0 1px;
border-style: solid;
border-color: var(--borderColor);
background-color: var(--albumBackgroundColor);
color: var(--defaultColor);
}
.tracks {
padding: 0 4px;
background-color: var(--trackBackgroundColor);
color: var(--defaultColor);
}
.allTracks {
background-color: color(#27c24c saturation(-25%));
color: var(--white);
}
.missingWanted {
background-color: color(#f05050 saturation(-20%));
color: var(--white);
}

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'album': string;
'albumType': string;
'allTracks': string;
'info': string;
'missingWanted': string;
'tracks': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,83 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Statistics } from 'Album/Album';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import translate from 'Utilities/String/translate';
import styles from './AlbumStudioAlbum.css';
interface AlbumStudioAlbumProps {
artistId: number;
albumId: number;
title: string;
disambiguation: string;
albumType: string;
monitored: boolean;
statistics: Statistics;
isSaving: boolean;
}
function AlbumStudioAlbum(props: AlbumStudioAlbumProps) {
const {
albumId,
title,
disambiguation,
albumType,
monitored,
statistics = {
trackFileCount: 0,
totalTrackCount: 0,
percentOfTracks: 0,
},
isSaving = false,
} = props;
const { trackFileCount, totalTrackCount, percentOfTracks } = statistics;
const dispatch = useDispatch();
const onAlbumMonitoredPress = useCallback(() => {
dispatch(
toggleAlbumsMonitored({
albumIds: [albumId],
monitored: !monitored,
})
);
}, [albumId, monitored, dispatch]);
return (
<div className={styles.album}>
<div className={styles.info}>
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={onAlbumMonitoredPress}
/>
<span>
{disambiguation ? `${title} (${disambiguation})` : `${title}`}
</span>
</div>
<div className={styles.albumType}>
<span>{albumType}</span>
</div>
<div
className={classNames(
styles.tracks,
percentOfTracks < 100 && monitored && styles.missingWanted,
percentOfTracks === 100 && styles.allTracks
)}
title={translate('AlbumStudioTracksDownloaded', {
trackFileCount,
totalTrackCount,
})}
>
{totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`}
</div>
</div>
);
}
export default AlbumStudioAlbum;

@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ChangeMonitoringModalContent from './ChangeMonitoringModalContent';
interface ChangeMonitoringModalProps {
isOpen: boolean;
artistIds: number[];
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModal(props: ChangeMonitoringModalProps) {
const { isOpen, artistIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ChangeMonitoringModalContent
artistIds={artistIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ChangeMonitoringModal;

@ -0,0 +1,26 @@
.labelIcon {
margin-left: 8px;
}
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'labelIcon': string;
'message': string;
'modalFooter': string;
'selected': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,96 @@
import React, { useCallback, useState } from 'react';
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ChangeMonitoringModalContent.css';
const NO_CHANGE = 'noChange';
interface ChangeMonitoringModalContentProps {
artistIds: number[];
saveError?: object;
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModalContent(
props: ChangeMonitoringModalContentProps
) {
const { artistIds, onSavePress, onModalClose, ...otherProps } = props;
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }) => {
setMonitor(value);
},
[setMonitor]
);
const onSavePressWrapper = useCallback(() => {
onSavePress(monitor);
}, [monitor, onSavePress]);
const selectedCount = artistIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorArtists')}</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO} className={styles.message}>
{translate('MonitorAlbumExistingOnlyWarning')}
</Alert>
<Form {...otherProps}>
<FormGroup>
<FormLabel>
{translate('MonitorExistingAlbums')}
<Popover
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
title={translate('MonitoringOptions')}
body={<ArtistMonitoringOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_ALBUMS_SELECT}
name="monitor"
value={monitor}
includeNoChange={true}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountArtistsSelected', { count: selectedCount })}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={onSavePressWrapper}>{translate('Save')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ChangeMonitoringModalContent;

@ -51,6 +51,10 @@
gap: 20px;
}
.actionButtons {
flex-wrap: wrap;
}
.actionButtons,
.deleteButtons {
display: flex;

@ -2,15 +2,20 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { SelectActionType, useSelect } from 'App/SelectContext';
import AppState from 'App/State/AppState';
import { RENAME_ARTIST, RETAG_ARTIST } from 'Commands/commandNames';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import { saveArtistEditor } from 'Store/Actions/artistActions';
import {
saveArtistEditor,
updateArtistsMonitor,
} from 'Store/Actions/artistActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ChangeMonitoringModal from './AlbumStudio/ChangeMonitoringModal';
import RetagArtistModal from './AudioTags/RetagArtistModal';
import DeleteArtistModal from './Delete/DeleteArtistModal';
import EditArtistModal from './Edit/EditArtistModal';
@ -19,7 +24,7 @@ import TagsModal from './Tags/TagsModal';
import styles from './ArtistIndexSelectFooter.css';
const artistEditorSelector = createSelector(
(state) => state.artist,
(state: AppState) => state.artist,
(artist) => {
const { isSaving, isDeleting, deleteError } = artist;
@ -48,9 +53,11 @@ function ArtistIndexSelectFooter() {
const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false);
const [isRetaggingModalOpen, setIsRetaggingModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSavingArtist, setIsSavingArtist] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const [isSavingMonitoring, setIsSavingMonitoring] = useState(false);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
@ -124,6 +131,29 @@ function ArtistIndexSelectFooter() {
[artistIds, dispatch]
);
const onMonitoringPress = useCallback(() => {
setIsMonitoringModalOpen(true);
}, [setIsMonitoringModalOpen]);
const onMonitoringClose = useCallback(() => {
setIsMonitoringModalOpen(false);
}, [setIsMonitoringModalOpen]);
const onMonitoringSavePress = useCallback(
(monitor: string) => {
setIsSavingMonitoring(true);
setIsMonitoringModalOpen(false);
dispatch(
updateArtistsMonitor({
artistIds,
monitor,
})
);
},
[artistIds, dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@ -136,6 +166,7 @@ function ArtistIndexSelectFooter() {
if (!isSaving) {
setIsSavingArtist(false);
setIsSavingTags(false);
setIsSavingMonitoring(false);
}
}, [isSaving]);
@ -188,6 +219,14 @@ function ArtistIndexSelectFooter() {
>
{translate('SetAppTags')}
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving && isSavingMonitoring}
isDisabled={!anySelected || isOrganizingArtist || isRetaggingArtist}
onPress={onMonitoringPress}
>
{translate('UpdateMonitoring')}
</SpinnerButton>
</div>
<div className={styles.deleteButtons}>
@ -220,6 +259,13 @@ function ArtistIndexSelectFooter() {
onModalClose={onTagsModalClose}
/>
<ChangeMonitoringModal
isOpen={isMonitoringModalOpen}
artistIds={artistIds}
onSavePress={onMonitoringSavePress}
onModalClose={onMonitoringClose}
/>
<OrganizeArtistModal
isOpen={isOrganizeModalOpen}
artistIds={artistIds}

@ -0,0 +1,4 @@
.albumCount {
width: 100%;
cursor: default;
}

@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'albumCount': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,38 @@
import React from 'react';
import AlbumDetails from 'Artist/Index/Select/AlbumStudio/AlbumDetails';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import Popover from 'Components/Tooltip/Popover';
import TooltipPosition from 'Helpers/Props/TooltipPosition';
import translate from 'Utilities/String/translate';
import styles from './AlbumsCell.css';
interface SeriesStatusCellProps {
className: string;
artistId: number;
albumCount: number;
isSelectMode: boolean;
}
function AlbumsCell(props: SeriesStatusCellProps) {
const { className, artistId, albumCount, isSelectMode, ...otherProps } =
props;
return (
<VirtualTableRowCell className={className} {...otherProps}>
{isSelectMode && albumCount > 0 ? (
<Popover
className={styles.albumCount}
anchor={albumCount}
title={translate('AlbumDetails')}
body={<AlbumDetails artistId={artistId} />}
position={TooltipPosition.Left}
canFlip={true}
/>
) : (
albumCount
)}
</VirtualTableRowCell>
);
}
export default AlbumsCell;

@ -26,6 +26,7 @@ import { executeCommand } from 'Store/Actions/commandActions';
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import AlbumsCell from './AlbumsCell';
import hasGrowableColumns from './hasGrowableColumns';
import selectTableOptions from './selectTableOptions';
import styles from './ArtistIndexRow.css';
@ -164,6 +165,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
artistType={artistType}
monitored={monitored}
status={status}
isSelectMode={isSelectMode}
isSaving={isSaving}
component={VirtualTableRowCell}
/>
@ -288,9 +290,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'albumCount') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{albumCount}
</VirtualTableRowCell>
<AlbumsCell
key={name}
className={styles[name]}
artistId={artistId}
albumCount={albumCount}
isSelectMode={isSelectMode}
/>
);
}

@ -14,6 +14,7 @@ interface ArtistStatusCellProps {
artistType?: string;
monitored: boolean;
status: string;
isSelectMode: boolean;
isSaving: boolean;
component?: React.ElementType;
}
@ -25,12 +26,14 @@ function ArtistStatusCell(props: ArtistStatusCellProps) {
artistType,
monitored,
status,
isSelectMode,
isSaving,
component: Component = VirtualTableRowCell,
...otherProps
} = props;
const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive';
const endedString =
artistType === 'Person' ? translate('Deceased') : translate('Inactive');
const dispatch = useDispatch();
const onMonitoredPress = useCallback(() => {
@ -39,13 +42,24 @@ function ArtistStatusCell(props: ArtistStatusCellProps) {
return (
<Component className={className} {...otherProps}>
<MonitorToggleButton
className={styles.monitorToggle}
monitored={monitored}
size={14}
isSaving={isSaving}
onPress={onMonitoredPress}
/>
{isSelectMode ? (
<MonitorToggleButton
className={styles.statusIcon}
monitored={monitored}
isSaving={isSaving}
onPress={onMonitoredPress}
/>
) : (
<Icon
className={styles.statusIcon}
name={monitored ? icons.MONITORED : icons.UNMONITORED}
title={
monitored
? translate('ArtistIsMonitored')
: translate('ArtistIsUnmonitored')
}
/>
)}
<Icon
className={styles.statusIcon}

@ -47,7 +47,8 @@ class MonitoringOptionsModalContentConnector extends Component {
onSavePress = ({ monitor }) => {
this.props.dispatchUpdateMonitoringOptions({
artistIds: [this.props.artistId],
monitor
monitor,
shouldFetchAlbumsAfterUpdate: true
});
};

@ -358,7 +358,8 @@ export const actionHandlers = handleThunks({
artistIds,
monitor,
monitored,
monitorNewItems
monitorNewItems,
shouldFetchAlbumsAfterUpdate = false
} = payload;
const artists = [];
@ -390,7 +391,9 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
dispatch(fetchAlbums({ artistId: artistIds[0] }));
if (shouldFetchAlbumsAfterUpdate) {
dispatch(fetchAlbums({ artistId: artistIds[0] }));
}
dispatch(set({
section,

@ -0,0 +1,27 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import { createArtistSelectorForHook } from './createArtistSelector';
function createArtistAlbumsSelector(artistId: number) {
return createSelector(
(state: AppState) => state.albums,
createArtistSelectorForHook(artistId),
(albums, artist = {} as Artist) => {
const { isFetching, isPopulated, error, items } = albums;
const filteredAlbums = items.filter(
(album) => album.artist.artistMetadataId === artist.artistMetadataId
);
return {
isFetching,
isPopulated,
error,
items: filteredAlbums,
};
}
);
}
export default createArtistAlbumsSelector;

@ -41,6 +41,7 @@
"AgeWhenGrabbed": "Age (when grabbed)",
"Album": "Album",
"AlbumCount": "Album Count",
"AlbumDetails": "Album Details",
"AlbumHasNotAired": "Album has not aired",
"AlbumIsDownloading": "Album is downloading",
"AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}",
@ -49,9 +50,12 @@
"AlbumReleaseDate": "Album Release Date",
"AlbumStatus": "Album Status",
"AlbumStudio": "Album Studio",
"AlbumStudioTracksDownloaded": "{trackFileCount}/{totalTrackCount} tracks downloaded",
"AlbumStudioTruncated": "Only latest 20 albums are shown, go to details to see all albums",
"AlbumTitle": "Album Title",
"AlbumType": "Album Type",
"Albums": "Albums",
"AlbumsLoadError": "Unable to load albums",
"All": "All",
"AllAlbums": "All Albums",
"AllAlbumsData": "Monitor all albums except specials",
@ -99,6 +103,8 @@
"ArtistClickToChangeAlbum": "Click to change album",
"ArtistEditor": "Artist Editor",
"ArtistFolderFormat": "Artist Folder Format",
"ArtistIsMonitored": "Artist is monitored",
"ArtistIsUnmonitored": "Artist is unmonitored",
"ArtistMonitoring": "Artist Monitoring",
"ArtistName": "Artist Name",
"ArtistNameHelpText": "The name of the artist/album to exclude (can be anything meaningful)",
@ -193,7 +199,7 @@
"ContinuingNoAdditionalAlbumsAreExpected": "No additional albums are expected",
"ContinuingOnly": "Continuing Only",
"CopyToClipboard": "Copy to clipboard",
"CopyUsingHardlinksHelpText": "Hardlinks allow Lidarr to import seeding torrents to the the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume",
"CopyUsingHardlinksHelpText": "Hardlinks allow Lidarr to import seeding torrents to the the artist folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume",
"CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Lidarr's rename function as a work around.",
"CouldntFindAnyResultsForTerm": "Couldn't find any results for '{0}'",
"CountAlbums": "{albumCount} albums",
@ -222,6 +228,7 @@
"Date": "Date",
"DateAdded": "Date Added",
"Dates": "Dates",
"Deceased": "Deceased",
"DefaultDelayProfileHelpText": "This is the default profile. It applies to all artist that don't have an explicit profile.",
"DefaultLidarrTags": "Default Lidarr Tags",
"DefaultMetadataProfileIdHelpText": "Default Metadata Profile for artists detected in this folder",
@ -523,6 +530,7 @@
"ItsEasyToAddANewArtistJustStartTypingTheNameOfTheArtistYouWantToAdd": "It's easy to add a new artist, just start typing the name of the artist you want to add.",
"Label": "Label",
"Language": "Language",
"Large": "Large",
"LastAlbum": "Last Album",
"LastDuration": "Last Duration",
"LastExecution": "Last Execution",
@ -604,6 +612,7 @@
"MonitorAlbum": "Monitor Album",
"MonitorAlbumExistingOnlyWarning": "This is a one off adjustment of the monitored setting for each album. Use the option under Artist/Edit to control what happens for newly added albums",
"MonitorArtist": "Monitor Artist",
"MonitorArtists": "Monitor Artists",
"MonitorExistingAlbums": "Monitor Existing Albums",
"MonitorNewAlbums": "Monitor New Albums",
"MonitorNewItems": "Monitor New Albums",
@ -954,6 +963,7 @@
"SkipFreeSpaceCheckWhenImportingHelpText": "Use when Lidarr is unable to detect free space of your root folder during file import",
"SkipRedownload": "Skip Redownload",
"SkipRedownloadHelpText": "Prevents Lidarr from trying download alternative releases for the removed items",
"Small": "Small",
"SmartReplace": "Smart Replace",
"SomeResultsAreHiddenByTheAppliedFilter": "Some results are hidden by the applied filter",
"SorryThatAlbumCannotBeFound": "Sorry, that album cannot be found.",
@ -1087,6 +1097,7 @@
"UpdateCheckStartupTranslocationMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.",
"UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.",
"UpdateMechanismHelpText": "Use Lidarr's built-in updater or a script",
"UpdateMonitoring": "Update Monitoring",
"UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process",
"UpdateSelected": "Update Selected",
"Updates": "Updates",

Loading…
Cancel
Save