Compare commits

...

11 Commits

Author SHA1 Message Date
Bogdan ff69211b9d
Merge bb1b6072b7 into 416d505316 2024-04-23 20:53:38 +01:00
Bogdan 416d505316 Bump dotnet to 6.0.29 2024-04-22 07:58:44 +03:00
Bogdan 4816f35256 Bump typescript eslint plugin and parser 2024-04-21 12:45:16 +03:00
Josh McKinney e42e0a72eb Add dev container workspace
Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace

(cherry picked from commit d6278fced49b26be975c3a6039b38a94f700864b)

Closes #4756
2024-04-21 12:05:07 +03:00
Bogdan db9e62f79d Convert store selectors to Typescript
Closes #3937
2024-04-21 12:04:13 +03:00
Bogdan bc69fa4842 Bump frontend dependencies 2024-04-21 11:11:55 +03:00
Bogdan 86dad72c49 Bump version to 2.3.1 2024-04-21 09:15:31 +03:00
Bogdan 4a8d6c367d Bump skipping spotify tests 2024-04-20 18:37:21 +03:00
Bogdan c1926f8758 Fixed: Skip move when source and destination are the same
Co-authored-by: Qstick <qstick@gmail.com>
2024-04-20 17:56:04 +03:00
Bogdan 7820bcf91f Bump SixLabors.ImageSharp to 3.1.4 2024-04-19 08:00:59 +03:00
Mark McDowall bb1b6072b7 New: Added UI for parsing release names
(cherry picked from commit 85e285598106346099ceae676599c5cb4b789c92)
2024-02-07 00:24:28 +02:00
64 changed files with 2900 additions and 1694 deletions

View File

@ -0,0 +1,13 @@
// This file is used to open the backend and frontend in the same workspace, which is necessary as
// the frontend has vscode settings that are distinct from the backend
{
"folders": [
{
"path": ".."
},
{
"path": "../frontend"
}
],
"settings": {}
}

View File

@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '2.3.0' majorVersion: '2.3.1'
minorVersion: $[counter('minorVersion', 1076)] minorVersion: $[counter('minorVersion', 1076)]
lidarrVersion: '$(majorVersion).$(minorVersion)' lidarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(lidarrVersion)' buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.417' dotnetVersion: '6.0.421'
nodeVersion: '20.X' nodeVersion: '20.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'

View File

@ -28,7 +28,8 @@ module.exports = {
globals: { globals: {
expect: false, expect: false,
chai: false, chai: false,
sinon: false sinon: false,
JSX: true
}, },
parserOptions: { parserOptions: {

View File

@ -11,7 +11,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound'; import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch'; import Switch from 'Components/Router/Switch';
import AddNewItemConnector from 'Search/AddNewItemConnector'; import AddNewItemConnector from 'Search/AddNewItemConnector';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector'; import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
@ -184,7 +184,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/customformats" path="/settings/customformats"
component={CustomFormatSettingsConnector} component={CustomFormatSettingsPage}
/> />
<Route <Route

View File

@ -1,3 +1,4 @@
import ParseAppState from 'App/State/ParseAppState';
import AlbumAppState from './AlbumAppState'; import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState'; import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import CalendarAppState from './CalendarAppState'; import CalendarAppState from './CalendarAppState';
@ -5,6 +6,7 @@ import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState'; import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState'; import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState'; import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState'; import TagsAppState from './TagsAppState';
import TrackFilesAppState from './TrackFilesAppState'; import TrackFilesAppState from './TrackFilesAppState';
import TracksAppState from './TracksAppState'; import TracksAppState from './TracksAppState';
@ -57,11 +59,13 @@ interface AppState {
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState; history: HistoryAppState;
parse: ParseAppState;
queue: QueueAppState; queue: QueueAppState;
settings: SettingsAppState; settings: SettingsAppState;
tags: TagsAppState; tags: TagsAppState;
trackFiles: TrackFilesAppState; trackFiles: TrackFilesAppState;
tracksSelection: TracksAppState; tracksSelection: TracksAppState;
system: SystemAppState;
} }
export default AppState; export default AppState;

View File

@ -0,0 +1,35 @@
import Album from 'Album/Album';
import ModelBase from 'App/ModelBase';
import { AppSectionItemState } from 'App/State/AppSectionState';
import Artist from 'Artist/Artist';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
export interface ArtistTitleInfo {
title: string;
}
export interface ParsedAlbumInfo {
albumTitle: string;
artistName: string;
artistTitleInfo: ArtistTitleInfo;
discography: boolean;
quality: QualityModel;
releaseGroup?: string;
releaseHash: string;
releaseTitle: string;
releaseTokens: string;
}
export interface ParseModel extends ModelBase {
title: string;
parsedAlbumInfo: ParsedAlbumInfo;
artist?: Artist;
albums: Album[];
customFormats?: CustomFormat[];
customFormatScore?: number;
}
type ParseAppState = AppSectionItemState<ParseModel>;
export default ParseAppState;

View File

@ -1,5 +1,6 @@
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState, AppSectionSaveState,
AppSectionSchemaState, AppSectionSchemaState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
@ -46,7 +47,7 @@ export interface RootFolderAppState
AppSectionSaveState {} AppSectionSaveState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>; export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionState<UiSettings>; export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
@ -57,7 +58,7 @@ interface SettingsAppState {
notifications: NotificationAppState; notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState; qualityProfiles: QualityProfilesAppState;
rootFolders: RootFolderAppState; rootFolders: RootFolderAppState;
uiSettings: UiSettingsAppState; ui: UiSettingsAppState;
} }
export default SettingsAppState; export default SettingsAppState;

View File

@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@ -1,12 +1,32 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
export interface Tag extends ModelBase { export interface Tag extends ModelBase {
label: string; label: string;
} }
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {} export interface TagDetail extends ModelBase {
label: string;
autoTagIds: number[];
delayProfileIds: number[];
downloadClientIds: [];
importListIds: number[];
indexerIds: number[];
notificationIds: number[];
restrictionIds: number[];
artistIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState; export default TagsAppState;

View File

@ -164,6 +164,10 @@ const links = [
{ {
title: () => translate('LogFiles'), title: () => translate('LogFiles'),
to: '/system/logs/files' to: '/system/logs/files'
},
{
title: 'Parse Testing',
to: '/system/parse'
} }
] ]
} }

View File

@ -32,6 +32,7 @@ import {
faBookReader as fasBookReader, faBookReader as fasBookReader,
faBroadcastTower as fasBroadcastTower, faBroadcastTower as fasBroadcastTower,
faBug as fasBug, faBug as fasBug,
faCalculator as fasCalculator,
faCalendarAlt as fasCalendarAlt, faCalendarAlt as fasCalendarAlt,
faCaretDown as fasCaretDown, faCaretDown as fasCaretDown,
faCheck as fasCheck, faCheck as fasCheck,
@ -187,6 +188,7 @@ export const PAGE_PREVIOUS = fasBackward;
export const PAGE_NEXT = fasForward; export const PAGE_NEXT = fasForward;
export const PAGE_LAST = fasFastForward; export const PAGE_LAST = fasFastForward;
export const PARENT = fasLevelUpAlt; export const PARENT = fasLevelUpAlt;
export const PARSE = fasCalculator;
export const PAUSED = fasPause; export const PAUSED = fasPause;
export const PENDING = farClock; export const PENDING = farClock;
export const PROFILE = fasUser; export const PROFILE = fasUser;

View File

@ -0,0 +1,45 @@
.inputContainer {
display: flex;
margin-bottom: 10px;
}
.inputIconContainer {
width: 58px;
height: 46px;
border: 1px solid var(--inputBorderColor);
border-right: none;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--inputIconContainerBackgroundColor);
text-align: center;
line-height: 46px;
}
.input {
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
font-size: 18px;
}
.clearButton {
border: 1px solid var(--inputBorderColor);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}

12
frontend/src/Parse/Parse.css.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'clearButton': string;
'helpText': string;
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,109 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props';
import { clear, fetch } from 'Store/Actions/parseActions';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import ParseResult from './ParseResult';
import parseStateSelector from './parseStateSelector';
import styles from './Parse.css';
function Parse() {
const { isFetching, error, item } = useSelector(parseStateSelector());
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const onInputChange = useCallback(
({ value }: { value: string }) => {
const trimmedValue = value.trim();
setTitle(value);
if (trimmedValue === '') {
dispatch(clear());
} else {
dispatch(fetch({ title: trimmedValue }));
}
},
[setTitle, dispatch]
);
const onClearPress = useCallback(() => {
setTitle('');
dispatch(clear());
}, [setTitle, dispatch]);
useEffect(
() => {
return () => {
dispatch(clear());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<PageContent title={translate('Parse')}>
<PageContentBody>
<div className={styles.inputContainer}>
<div className={styles.inputIconContainer}>
<Icon name={icons.PARSE} size={20} />
</div>
<TextInput
className={styles.input}
name="title"
value={title}
placeholder="eg. Artist.Name-Album.Title[FLAC]-RlsGroup"
autoFocus={true}
onChange={onInputChange}
/>
<Button className={styles.clearButton} onPress={onClearPress}>
<Icon name={icons.REMOVE} size={20} />
</Button>
</div>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalErrorParsing')}
</div>
<div>{getErrorMessage(error)}</div>
</div>
) : null}
{!isFetching && title && !error && !item.parsedAlbumInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedAlbumInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalHelpText')}
</div>
<div>{translate('ParseModalHelpTextDetails')}</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default Parse;

View File

@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ParseModalContent from './ParseModalContent';
interface ParseModalProps {
isOpen: boolean;
onModalClose: () => void;
}
function ParseModal(props: ParseModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ParseModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ParseModal;

View File

@ -0,0 +1,45 @@
.inputContainer {
display: flex;
margin-bottom: 10px;
}
.inputIconContainer {
width: 58px;
height: 46px;
border: 1px solid var(--inputBorderColor);
border-right: none;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--inputIconContainerBackgroundColor);
text-align: center;
line-height: 46px;
}
.input {
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;
font-size: 18px;
}
.clearButton {
border: 1px solid var(--inputBorderColor);
border-left: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.message {
margin-top: 30px;
text-align: center;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}

View File

@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'clearButton': string;
'helpText': string;
'input': string;
'inputContainer': string;
'inputIconContainer': string;
'message': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,122 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { icons } from 'Helpers/Props';
import { clear, fetch } from 'Store/Actions/parseActions';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import ParseResult from './ParseResult';
import parseStateSelector from './parseStateSelector';
import styles from './ParseModalContent.css';
interface ParseModalContentProps {
onModalClose: () => void;
}
function ParseModalContent(props: ParseModalContentProps) {
const { onModalClose } = props;
const { isFetching, error, item } = useSelector(parseStateSelector());
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const onInputChange = useCallback(
({ value }: { value: string }) => {
const trimmedValue = value.trim();
setTitle(value);
if (trimmedValue === '') {
dispatch(clear());
} else {
dispatch(fetch({ title: trimmedValue }));
}
},
[setTitle, dispatch]
);
const onClearPress = useCallback(() => {
setTitle('');
dispatch(clear());
}, [setTitle, dispatch]);
useEffect(
() => {
return () => {
dispatch(clear());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('TestParsing')}</ModalHeader>
<ModalBody>
<div className={styles.inputContainer}>
<div className={styles.inputIconContainer}>
<Icon name={icons.PARSE} size={20} />
</div>
<TextInput
className={styles.input}
name="title"
value={title}
placeholder="eg. Artist.Name-Album.Title[FLAC]-RlsGroup"
autoFocus={true}
onChange={onInputChange}
/>
<Button className={styles.clearButton} onPress={onClearPress}>
<Icon name={icons.REMOVE} size={20} />
</Button>
</div>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalErrorParsing')}
</div>
<div>{getErrorMessage(error)}</div>
</div>
) : null}
{!isFetching && title && !error && !item.parsedAlbumInfo ? (
<div className={styles.message}>
{translate('ParseModalUnableToParse')}
</div>
) : null}
{!isFetching && !error && item.parsedAlbumInfo ? (
<ParseResult item={item} />
) : null}
{title ? null : (
<div className={styles.message}>
<div className={styles.helpText}>
{translate('ParseModalHelpText')}
</div>
<div>{translate('ParseModalHelpTextDetails')}</div>
</div>
)}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default ParseModalContent;

View File

@ -0,0 +1,8 @@
.container {
display: flex;
flex-wrap: wrap;
}
.column {
flex: 0 0 50%;
}

View File

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

View File

@ -0,0 +1,160 @@
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
import AlbumFormats from 'Album/AlbumFormats';
import AlbumTitleLink from 'Album/AlbumTitleLink';
import { ParseModel } from 'App/State/ParseAppState';
import ArtistNameLink from 'Artist/ArtistNameLink';
import FieldSet from 'Components/FieldSet';
import translate from 'Utilities/String/translate';
import ParseResultItem from './ParseResultItem';
import styles from './ParseResult.css';
interface ParseResultProps {
item: ParseModel;
}
function ParseResult(props: ParseResultProps) {
const { item } = props;
const { customFormats, customFormatScore, albums, parsedAlbumInfo, artist } =
item;
const {
releaseTitle,
artistName,
albumTitle,
releaseGroup,
discography,
quality,
} = parsedAlbumInfo;
const sortedAlbums = _.sortBy(albums, (item) =>
moment(item.releaseDate).unix()
);
return (
<div>
<FieldSet legend={translate('Release')}>
<ParseResultItem
title={translate('ReleaseTitle')}
data={releaseTitle}
/>
<ParseResultItem title={translate('ArtistName')} data={artistName} />
<ParseResultItem title={translate('AlbumTitle')} data={albumTitle} />
<ParseResultItem
title={translate('ReleaseGroup')}
data={releaseGroup ?? '-'}
/>
</FieldSet>
<FieldSet legend={translate('AlbumInfo')}>
<div className={styles.container}>
<div className={styles.column}>
<ParseResultItem
title={translate('Discography')}
data={discography ? translate('True') : translate('False')}
/>
</div>
</div>
</FieldSet>
<FieldSet legend={translate('Quality')}>
<div className={styles.container}>
<div className={styles.column}>
<ParseResultItem
title={translate('Quality')}
data={quality.quality.name}
/>
<ParseResultItem
title={translate('Proper')}
data={
quality.revision.version > 1 && !quality.revision.isRepack
? translate('True')
: '-'
}
/>
<ParseResultItem
title={translate('Repack')}
data={quality.revision.isRepack ? translate('True') : '-'}
/>
</div>
<div className={styles.column}>
<ParseResultItem
title={translate('Version')}
data={
quality.revision.version > 1 ? quality.revision.version : '-'
}
/>
<ParseResultItem
title={translate('Real')}
data={quality.revision.real ? translate('True') : '-'}
/>
</div>
</div>
</FieldSet>
<FieldSet legend={translate('Details')}>
<ParseResultItem
title={translate('MatchedToArtist')}
data={
artist ? (
<ArtistNameLink
foreignArtistId={artist.foreignArtistId}
artistName={artist.artistName}
/>
) : (
'-'
)
}
/>
<ParseResultItem
title={translate('MatchedToAlbums')}
data={
sortedAlbums.length ? (
<div>
{sortedAlbums.map((album) => {
return (
<div key={album.id}>
<AlbumTitleLink
foreignAlbumId={album.foreignAlbumId}
title={album.title}
disambiguation={album.disambiguation}
/>
</div>
);
})}
</div>
) : (
'-'
)
}
/>
<ParseResultItem
title={translate('CustomFormats')}
data={
customFormats?.length ? (
<AlbumFormats formats={customFormats} />
) : (
'-'
)
}
/>
<ParseResultItem
title={translate('CustomFormatScore')}
data={customFormatScore}
/>
</FieldSet>
</div>
);
}
export default ParseResult;

View File

@ -0,0 +1,21 @@
.item {
display: flex;
}
.title {
margin-right: 20px;
width: 250px;
text-align: right;
font-weight: bold;
}
@media (max-width: $breakpointSmall) {
.item {
display: block;
margin-bottom: 10px;
}
.title {
text-align: left;
}
}

View File

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

View File

@ -0,0 +1,20 @@
import React, { ReactNode } from 'react';
import styles from './ParseResultItem.css';
interface ParseResultItemProps {
title: string;
data: string | number | ReactNode;
}
function ParseResultItem(props: ParseResultItemProps) {
const { title, data } = props;
return (
<div className={styles.item}>
<div className={styles.title}>{title}</div>
<div>{data}</div>
</div>
);
}
export default ParseResultItem;

View File

@ -0,0 +1,31 @@
import React, { Fragment, useCallback, useState } from 'react';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import ParseModal from 'Parse/ParseModal';
import translate from 'Utilities/String/translate';
function ParseToolbarButton() {
const [isParseModalOpen, setIsParseModalOpen] = useState(false);
const onOpenParseModalPress = useCallback(() => {
setIsParseModalOpen(true);
}, [setIsParseModalOpen]);
const onParseModalClose = useCallback(() => {
setIsParseModalOpen(false);
}, [setIsParseModalOpen]);
return (
<Fragment>
<PageToolbarButton
label={translate('TestParsing')}
iconName={icons.PARSE}
onPress={onOpenParseModalPress}
/>
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
</Fragment>
);
}
export default ParseToolbarButton;

View File

@ -0,0 +1,12 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import ParseAppState from 'App/State/ParseAppState';
export default function parseStateSelector() {
return createSelector(
(state: AppState) => state.parse,
(parse: ParseAppState) => {
return parse;
}
);
}

View File

@ -1,33 +0,0 @@
import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
class CustomFormatSettingsConnector extends Component {
//
// Render
render() {
return (
<PageContent title={translate('CustomFormatSettings')}>
<SettingsToolbarConnector
showSave={false}
/>
<PageContentBody>
<DndProvider backend={HTML5Backend}>
<CustomFormatsConnector />
</DndProvider>
</PageContentBody>
</PageContent>
);
}
}
export default CustomFormatSettingsConnector;

View File

@ -0,0 +1,42 @@
import React, { Fragment } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
function CustomFormatSettingsPage() {
return (
<PageContent title={translate('CustomFormatsSettings')}>
<SettingsToolbarConnector
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
showSave={false}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<ParseToolbarButton />
</Fragment>
}
/>
<PageContentBody>
{/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */}
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<DndProvider backend={HTML5Backend}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<CustomFormatsConnector />
</DndProvider>
</PageContentBody>
</PageContent>
);
}
export default CustomFormatSettingsPage;

View File

@ -134,6 +134,7 @@ const historyShape = {
}; };
SettingsToolbarConnector.propTypes = { SettingsToolbarConnector.propTypes = {
showSave: PropTypes.bool,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
history: PropTypes.shape(historyShape).isRequired, history: PropTypes.shape(historyShape).isRequired,
onSavePress: PropTypes.func, onSavePress: PropTypes.func,

View File

@ -13,6 +13,7 @@ import * as history from './historyActions';
import * as interactiveImportActions from './interactiveImportActions'; import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions'; import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions'; import * as organizePreview from './organizePreviewActions';
import * as parse from './parseActions';
import * as paths from './pathActions'; import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions'; import * as providerOptions from './providerOptionActions';
import * as queue from './queueActions'; import * as queue from './queueActions';
@ -41,6 +42,7 @@ export default [
oAuth, oAuth,
organizePreview, organizePreview,
retagPreview, retagPreview,
parse,
paths, paths,
providerOptions, providerOptions,
queue, queue,

View File

@ -0,0 +1,109 @@
import { Dispatch } from 'redux';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import AppState from 'App/State/AppState';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
interface FetchPayload {
title: string;
}
//
// Variables
export const section = 'parse';
let parseTimeout: number | null = null;
let abortCurrentRequest: (() => void) | null = null;
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
item: {},
};
//
// Actions Types
export const FETCH = 'parse/fetch';
export const CLEAR = 'parse/clear';
//
// Action Creators
export const fetch = createThunk(FETCH);
export const clear = createAction(CLEAR);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH]: function (
_getState: () => AppState,
payload: FetchPayload,
dispatch: Dispatch
) {
if (parseTimeout) {
clearTimeout(parseTimeout);
}
parseTimeout = window.setTimeout(async () => {
if (abortCurrentRequest) {
abortCurrentRequest();
}
const { request, abortRequest } = createAjaxRequest({
url: '/parse',
data: {
title: payload.title,
},
});
try {
const data = await request;
dispatch(
batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null,
}),
])
);
} catch (error) {
dispatch(
set({
section,
isAdding: false,
isAdded: false,
addError: error,
})
);
}
abortCurrentRequest = abortRequest;
}, 300);
},
});
//
// Reducers
export const reducers = createHandleActions(
{
[CLEAR]: createClearReducer(section, defaultState),
},
defaultState,
section
);

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createAllArtistSelector() { function createAllArtistSelector() {
return createSelector( return createSelector(
(state) => state.artist, (state: AppState) => state.artist,
(artist) => { (artist) => {
return artist.items; return artist.items;
} }

View File

@ -1,18 +1,19 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector'; import createAllArtistSelector from './createAllArtistSelector';
function createArtistCountSelector() { function createArtistCountSelector() {
return createSelector( return createSelector(
createAllArtistSelector(), createAllArtistSelector(),
(state) => state.artist.error, (state: AppState) => state.artist.error,
(state) => state.artist.isFetching, (state: AppState) => state.artist.isFetching,
(state) => state.artist.isPopulated, (state: AppState) => state.artist.isPopulated,
(artists, error, isFetching, isPopulated) => { (artists, error, isFetching, isPopulated) => {
return { return {
count: artists.length, count: artists.length,
error, error,
isFetching, isFetching,
isPopulated isPopulated,
}; };
} }
); );

View File

@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command'; import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector'; import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name, contraints = {}) { function createCommandExecutingSelector(name: string, contraints = {}) {
return createSelector( return createSelector(createCommandSelector(name, contraints), (command) => {
createCommandSelector(name, contraints), return isCommandExecuting(command);
(command) => { });
return isCommandExecuting(command);
}
);
} }
export default createCommandExecutingSelector; export default createCommandExecutingSelector;

View File

@ -1,14 +0,0 @@
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name, contraints = {}) {
return createSelector(
createCommandsSelector(),
(commands) => {
return findCommand(commands, { name, ...contraints });
}
);
}
export default createCommandSelector;

View File

@ -0,0 +1,11 @@
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name: string, contraints = {}) {
return createSelector(createCommandsSelector(), (commands) => {
return findCommand(commands, { name, ...contraints });
});
}
export default createCommandSelector;

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createCommandsSelector() { function createCommandsSelector() {
return createSelector( return createSelector(
(state) => state.commands, (state: AppState) => state.commands,
(commands) => { (commands) => {
return commands.items; return commands.items;
} }

View File

@ -1,9 +0,0 @@
import _ from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
_.isEqual
);
export default createDeepEqualSelector;

View File

@ -0,0 +1,6 @@
import { isEqual } from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
export default createDeepEqualSelector;

View File

@ -1,9 +1,10 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command'; import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() { function createExecutingCommandsSelector() {
return createSelector( return createSelector(
(state) => state.commands.items, (state: AppState) => state.commands.items,
(commands) => { (commands) => {
return commands.filter((command) => isCommandExecuting(command)); return commands.filter((command) => isCommandExecuting(command));
} }

View File

@ -1,13 +1,15 @@
import _ from 'lodash'; import { some } from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector'; import createAllArtistSelector from './createAllArtistSelector';
function createExistingArtistSelector() { function createExistingArtistSelector() {
return createSelector( return createSelector(
(state, { foreignArtistId }) => foreignArtistId, (_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
foreignArtistId,
createAllArtistSelector(), createAllArtistSelector(),
(foreignArtistId, artist) => { (foreignArtistId, artist) => {
return _.some(artist, { foreignArtistId }); return some(artist, { foreignArtistId });
} }
); );
} }

View File

@ -1,15 +0,0 @@
import { createSelector } from 'reselect';
function createMetadataProfileSelector() {
return createSelector(
(state, { metadataProfileId }) => metadataProfileId,
(state) => state.settings.metadataProfiles.items,
(metadataProfileId, metadataProfiles) => {
return metadataProfiles.find((profile) => {
return profile.id === metadataProfileId;
});
}
);
}
export default createMetadataProfileSelector;

View File

@ -0,0 +1,17 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createMetadataProfileSelector() {
return createSelector(
(_: AppState, { metadataProfileId }: { metadataProfileId: number }) =>
metadataProfileId,
(state: AppState) => state.settings.metadataProfiles.items,
(metadataProfileId, metadataProfiles) => {
return metadataProfiles.find(
(profile) => profile.id === metadataProfileId
);
}
);
}
export default createMetadataProfileSelector;

View File

@ -1,24 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
import createAllArtistSelector from './createAllArtistSelector';
function createProfileInUseSelector(profileProp) {
return createSelector(
(state, { id }) => id,
createAllArtistSelector(),
(state) => state.settings.importLists.items,
(id, artist, lists) => {
if (!id) {
return false;
}
if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) {
return true;
}
return false;
}
);
}
export default createProfileInUseSelector;

View File

@ -0,0 +1,25 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ImportList from 'typings/ImportList';
import createAllArtistSelector from './createAllArtistSelector';
function createProfileInUseSelector(profileProp: string) {
return createSelector(
(_: AppState, { id }: { id: number }) => id,
createAllArtistSelector(),
(state: AppState) => state.settings.importLists.items,
(id, artists, lists) => {
if (!id) {
return false;
}
return (
artists.some((a) => a[profileProp as keyof Artist] === id) ||
lists.some((list) => list[profileProp as keyof ImportList] === id)
);
}
);
}
export default createProfileInUseSelector;

View File

@ -1,26 +0,0 @@
import { createSelector } from 'reselect';
export function createQualityProfileSelectorForHook(qualityProfileId) {
return createSelector(
(state) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles.find((profile) => {
return profile.id === qualityProfileId;
});
}
);
}
function createQualityProfileSelector() {
return createSelector(
(state, { qualityProfileId }) => qualityProfileId,
(state) => state.settings.qualityProfiles.items,
(qualityProfileId, qualityProfiles) => {
return qualityProfiles.find((profile) => {
return profile.id === qualityProfileId;
});
}
);
}
export default createQualityProfileSelector;

View File

@ -0,0 +1,24 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createQualityProfileSelectorForHook(qualityProfileId: number) {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
}
);
}
function createQualityProfileSelector() {
return createSelector(
(_: AppState, { qualityProfileId }: { qualityProfileId: number }) =>
qualityProfileId,
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfileId, qualityProfiles) => {
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
}
);
}
export default createQualityProfileSelector;

View File

@ -1,21 +1,16 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createQueueItemSelector() { function createQueueItemSelector() {
return createSelector( return createSelector(
(state, { albumId }) => albumId, (_: AppState, { albumId }: { albumId: number }) => albumId,
(state) => state.queue.details.items, (state: AppState) => state.queue.details.items,
(albumId, details) => { (albumId, details) => {
if (!albumId || !details) { if (!albumId || !details) {
return null; return null;
} }
return details.find((item) => { return details.find((item) => item.albumId === albumId);
if (item.album) {
return item.album.id === albumId;
}
return false;
});
} }
); );
} }

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createSystemStatusSelector() { function createSystemStatusSelector() {
return createSelector( return createSelector(
(state) => state.system.status, (state: AppState) => state.system.status,
(status) => { (status) => {
return status.item; return status.item;
} }

View File

@ -1,9 +1,10 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createTagDetailsSelector() { function createTagDetailsSelector() {
return createSelector( return createSelector(
(state, { id }) => id, (_: AppState, { id }: { id: number }) => id,
(state) => state.tags.details.items, (state: AppState) => state.tags.details.items,
(id, tagDetails) => { (id, tagDetails) => {
return tagDetails.find((t) => t.id === id); return tagDetails.find((t) => t.id === id);
} }

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createTagsSelector() { function createTagsSelector() {
return createSelector( return createSelector(
(state) => state.tags.items, (state: AppState) => state.tags.items,
(tags) => { (tags) => {
return tags; return tags;
} }

View File

@ -1,9 +1,10 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createTrackFileSelector() { function createTrackFileSelector() {
return createSelector( return createSelector(
(state, { trackFileId }) => trackFileId, (_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId,
(state) => state.trackFiles, (state: AppState) => state.trackFiles,
(trackFileId, trackFiles) => { (trackFileId, trackFiles) => {
if (!trackFileId) { if (!trackFileId) {
return; return;

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createUISettingsSelector() { function createUISettingsSelector() {
return createSelector( return createSelector(
(state) => state.settings.ui, (state: AppState) => state.settings.ui,
(ui) => { (ui) => {
return ui.item; return ui.item;
} }

View File

@ -0,0 +1,31 @@
interface SystemStatus {
appData: string;
appName: string;
authentication: string;
branch: string;
buildTime: string;
instanceName: string;
isAdmin: boolean;
isDebug: boolean;
isDocker: boolean;
isLinux: boolean;
isNetCore: boolean;
isOsx: boolean;
isProduction: boolean;
isUserInteractive: boolean;
isWindows: boolean;
migrationVersion: number;
mode: string;
osName: string;
osVersion: string;
packageUpdateMechanism: string;
runtimeName: string;
runtimeVersion: string;
sqliteVersion: string;
startTime: string;
startupPath: string;
urlBase: string;
version: string;
}
export default SystemStatus;

View File

@ -34,9 +34,9 @@
"@microsoft/signalr": "6.0.25", "@microsoft/signalr": "6.0.25",
"@sentry/browser": "7.51.2", "@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2", "@sentry/integrations": "7.51.2",
"@types/node": "18.16.14", "@types/node": "18.19.31",
"@types/react": "18.2.6", "@types/react": "18.2.79",
"@types/react-dom": "18.2.4", "@types/react-dom": "18.2.25",
"ansi-colors": "4.1.3", "ansi-colors": "4.1.3",
"classnames": "2.3.2", "classnames": "2.3.2",
"clipboard": "2.0.11", "clipboard": "2.0.11",
@ -87,47 +87,47 @@
"redux-thunk": "2.3.0", "redux-thunk": "2.3.0",
"reselect": "4.1.8", "reselect": "4.1.8",
"stacktrace-js": "2.0.2", "stacktrace-js": "2.0.2",
"typescript": "4.9.5" "typescript": "5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.11", "@babel/core": "7.24.4",
"@babel/eslint-parser": "7.22.11", "@babel/eslint-parser": "7.24.1",
"@babel/plugin-proposal-export-default-from": "7.22.5", "@babel/plugin-proposal-export-default-from": "7.24.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.14", "@babel/preset-env": "7.24.4",
"@babel/preset-react": "7.22.5", "@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.22.11", "@babel/preset-typescript": "7.24.1",
"@types/lodash": "4.14.197", "@types/lodash": "4.14.195",
"@types/react-lazyload": "3.2.0", "@types/react-lazyload": "3.2.0",
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1", "@types/react-text-truncate": "0.14.1",
"@types/react-window": "1.8.5", "@types/react-window": "1.8.5",
"@types/redux-actions": "2.6.2", "@types/redux-actions": "2.6.2",
"@typescript-eslint/eslint-plugin": "5.59.7", "@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "5.59.7", "@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1", "babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24", "babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.32.1", "core-js": "3.37.0",
"css-loader": "6.7.3", "css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1", "css-modules-typescript-loader": "4.0.1",
"eslint": "8.45.0", "eslint": "8.57.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "8.10.0",
"eslint-plugin-filenames": "1.3.2", "eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.29.1",
"eslint-plugin-json": "3.1.0", "eslint-plugin-json": "3.1.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-simple-import-sort": "12.1.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0", "filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1", "html-webpack-plugin": "5.5.1",
"loader-utils": "^3.2.1", "loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5", "mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23", "postcss": "8.4.38",
"postcss-color-function": "4.1.0", "postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0", "postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4", "postcss-mixins": "9.0.4",

View File

@ -101,8 +101,8 @@
<!-- Standard testing packages --> <!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'"> <ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" /> <PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
</ItemGroup> </ItemGroup>

View File

@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.ImportListTests
} }
[Test] [Test]
[Ignore("Pending mapping fixes", Until = "2024-04-20 00:00:00Z")] [Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
public void map_artist_should_work() public void map_artist_should_work()
{ {
UseRealHttp(); UseRealHttp();
@ -159,7 +159,7 @@ namespace NzbDrone.Core.Test.ImportListTests
} }
[Test] [Test]
[Ignore("Pending mapping fixes", Until = "2024-04-20 00:00:00Z")] [Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
public void map_album_should_work() public void map_album_should_work()
{ {
UseRealHttp(); UseRealHttp();

View File

@ -3,13 +3,13 @@
<TargetFrameworks>net6.0</TargetFrameworks> <TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.151" />
<PackageReference Include="Diacritical.Net" Version="1.0.4" /> <PackageReference Include="Diacritical.Net" Version="1.0.4" />
<PackageReference Include="Polly" Version="8.3.1" /> <PackageReference Include="Polly" Version="8.3.1" />
<PackageReference Include="System.Text.Json" Version="6.0.9" /> <PackageReference Include="System.Text.Json" Version="6.0.9" />
<PackageReference Include="System.Memory" Version="4.5.5" /> <PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.25" /> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.29" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
@ -23,10 +23,9 @@
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" /> <PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" /> <PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" />
<PackageReference Include="Kveer.XmlRPC" Version="1.2.0" />
<PackageReference Include="Npgsql" Version="7.0.6" /> <PackageReference Include="Npgsql" Version="7.0.6" />
<PackageReference Include="SpotifyAPI.Web" Version="5.1.1" /> <PackageReference Include="SpotifyAPI.Web" Version="5.1.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="Equ" Version="2.3.0" /> <PackageReference Include="Equ" Version="2.3.0" />
<PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="MonoTorrent" Version="2.0.7" />
</ItemGroup> </ItemGroup>

View File

@ -53,6 +53,7 @@
"AlbumCount": "Album Count", "AlbumCount": "Album Count",
"AlbumDetails": "Album Details", "AlbumDetails": "Album Details",
"AlbumHasNotAired": "Album has not aired", "AlbumHasNotAired": "Album has not aired",
"AlbumInfo": "Album Info",
"AlbumIsDownloading": "Album is downloading", "AlbumIsDownloading": "Album is downloading",
"AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}", "AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}",
"AlbumIsNotMonitored": "Album is not monitored", "AlbumIsNotMonitored": "Album is not monitored",
@ -467,6 +468,7 @@
"FailedDownloadHandling": "Failed Download Handling", "FailedDownloadHandling": "Failed Download Handling",
"FailedLoadingSearchResults": "Failed to load search results, please try again.", "FailedLoadingSearchResults": "Failed to load search results, please try again.",
"FailedToLoadQueue": "Failed to load Queue", "FailedToLoadQueue": "Failed to load Queue",
"False": "False",
"FileDateHelpText": "Change file date on import/rescan", "FileDateHelpText": "Change file date on import/rescan",
"FileManagement": "File Management", "FileManagement": "File Management",
"FileNameTokens": "File Name Tokens", "FileNameTokens": "File Name Tokens",
@ -671,6 +673,8 @@
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
"MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?", "MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?",
"MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.", "MassSearchCancelWarning": "This cannot be cancelled once started without restarting {appName} or disabling all of your indexers.",
"MatchedToAlbums": "Matched to Albums",
"MatchedToArtist": "Matched to Artist",
"MaximumLimits": "Maximum Limits", "MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size", "MaximumSize": "Maximum Size",
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.", "MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",
@ -830,6 +834,11 @@
"PackageVersion": "Package Version", "PackageVersion": "Package Version",
"PageSize": "Page Size", "PageSize": "Page Size",
"PageSizeHelpText": "Number of items to show on each page", "PageSizeHelpText": "Number of items to show on each page",
"Parse": "Parse",
"ParseModalErrorParsing": "Error parsing, please try again.",
"ParseModalHelpText": "Enter a release title in the input above",
"ParseModalHelpTextDetails": "{appName} will attempt to parse the title and show you details about it",
"ParseModalUnableToParse": "Unable to parse the provided title, please try again.",
"Password": "Password", "Password": "Password",
"PasswordConfirmation": "Password Confirmation", "PasswordConfirmation": "Password Confirmation",
"PastDays": "Past Days", "PastDays": "Past Days",
@ -976,6 +985,7 @@
"RenameTracksHelpText": "{appName} will use the existing file name if renaming is disabled", "RenameTracksHelpText": "{appName} will use the existing file name if renaming is disabled",
"Renamed": "Renamed", "Renamed": "Renamed",
"Reorder": "Reorder", "Reorder": "Reorder",
"Repack": "Repack",
"Replace": "Replace", "Replace": "Replace",
"ReplaceExistingFiles": "Replace Existing Files", "ReplaceExistingFiles": "Replace Existing Files",
"ReplaceIllegalCharacters": "Replace Illegal Characters", "ReplaceIllegalCharacters": "Replace Illegal Characters",
@ -1148,6 +1158,7 @@
"TestAllClients": "Test All Clients", "TestAllClients": "Test All Clients",
"TestAllIndexers": "Test All Indexers", "TestAllIndexers": "Test All Indexers",
"TestAllLists": "Test All Lists", "TestAllLists": "Test All Lists",
"TestParsing": "Test Parsing",
"TheAlbumsFilesWillBeDeleted": "The album's files will be deleted.", "TheAlbumsFilesWillBeDeleted": "The album's files will be deleted.",
"TheArtistFolderStrongpathstrongAndAllOfItsContentWillBeDeleted": "The artist folder '{0}' and all of its content will be deleted.", "TheArtistFolderStrongpathstrongAndAllOfItsContentWillBeDeleted": "The artist folder '{0}' and all of its content will be deleted.",
"Theme": "Theme", "Theme": "Theme",
@ -1187,6 +1198,7 @@
"TrackStatus": "Track status", "TrackStatus": "Track status",
"TrackTitle": "Track Title", "TrackTitle": "Track Title",
"Tracks": "Tracks", "Tracks": "Tracks",
"True": "True",
"Type": "Type", "Type": "Type",
"URLBase": "URL Base", "URLBase": "URL Base",
"Ui": "UI", "Ui": "UI",

View File

@ -1,6 +1,7 @@
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
@ -56,6 +57,12 @@ namespace NzbDrone.Core.Music
{ {
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath); _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
} }
if (sourcePath.PathEquals(destinationPath))
{
_logger.ProgressInfo("{0} is already in the specified location '{1}'.", artist, destinationPath);
return;
}
} }
try try

View File

@ -6,6 +6,7 @@ namespace NzbDrone.Core.Parser.Model
{ {
public class ParsedAlbumInfo public class ParsedAlbumInfo
{ {
public string ReleaseTitle { get; set; }
public string AlbumTitle { get; set; } public string AlbumTitle { get; set; }
public string ArtistName { get; set; } public string ArtistName { get; set; }
public string AlbumType { get; set; } public string AlbumType { get; set; }
@ -18,7 +19,6 @@ namespace NzbDrone.Core.Parser.Model
public string ReleaseGroup { get; set; } public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; } public string ReleaseHash { get; set; }
public string ReleaseVersion { get; set; } public string ReleaseVersion { get; set; }
public string ReleaseTitle { get; set; }
[JsonIgnore] [JsonIgnore]
public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>(); public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.29" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" /> <ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" />

View File

@ -7,7 +7,7 @@
<PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NLog" Version="5.2.0" /> <PackageReference Include="NLog" Version="5.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="RestSharp" Version="106.15.0" /> <PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" /> <PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" /> <PackageReference Include="System.IO.Abstractions" Version="17.0.24" />

3315
yarn.lock

File diff suppressed because it is too large Load Diff