mirror of https://github.com/lidarr/Lidarr
Compare commits
11 Commits
b22626dd12
...
ff69211b9d
Author | SHA1 | Date |
---|---|---|
Bogdan | ff69211b9d | |
Bogdan | 416d505316 | |
Bogdan | 4816f35256 | |
Josh McKinney | e42e0a72eb | |
Bogdan | db9e62f79d | |
Bogdan | bc69fa4842 | |
Bogdan | 86dad72c49 | |
Bogdan | 4a8d6c367d | |
Bogdan | c1926f8758 | |
Bogdan | 7820bcf91f | |
Mark McDowall | bb1b6072b7 |
|
@ -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": {}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
|
@ -28,7 +28,8 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
expect: false,
|
expect: false,
|
||||||
chai: false,
|
chai: false,
|
||||||
sinon: false
|
sinon: false,
|
||||||
|
JSX: true
|
||||||
},
|
},
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,8 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
||||||
|
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
|
||||||
|
|
||||||
const createDeepEqualSelector = createSelectorCreator(
|
|
||||||
defaultMemoize,
|
|
||||||
_.isEqual
|
|
||||||
);
|
|
||||||
|
|
||||||
export default createDeepEqualSelector;
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||||
|
|
||||||
|
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
|
||||||
|
|
||||||
|
export default createDeepEqualSelector;
|
|
@ -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));
|
||||||
}
|
}
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
42
package.json
42
package.json
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
Loading…
Reference in New Issue