Set track title by using media tags
This commit is contained in:
parent
883379eff5
commit
103b12ebcc
|
@ -1788,6 +1788,11 @@
|
||||||
"jest-diff": "^24.3.0"
|
"jest-diff": "^24.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/jsmediatags": {
|
||||||
|
"version": "3.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsmediatags/-/jsmediatags-3.9.1.tgz",
|
||||||
|
"integrity": "sha512-BtCtPS+6yhG2bEO3ExjsjEX19KGrfKykTDhHhZgrwBJjC9caGCvabRh3wVXcmlBKFMjHPFYMtinPZ4HPJ7q3og=="
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
||||||
|
@ -8330,6 +8335,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
||||||
},
|
},
|
||||||
|
"jsmediatags": {
|
||||||
|
"version": "3.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsmediatags/-/jsmediatags-3.9.3.tgz",
|
||||||
|
"integrity": "sha512-h53yFnPYF1Y5jwr2ebcVzIIsvRpSalm0jhNiJDUztoPPHGpuHxi9YHUzdDgiw+ykiinXHd1s6HSIbudHw79zQw==",
|
||||||
|
"requires": {
|
||||||
|
"xhr2": "^0.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"json-parse-better-errors": {
|
"json-parse-better-errors": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
||||||
|
@ -15396,6 +15409,11 @@
|
||||||
"async-limiter": "~1.0.0"
|
"async-limiter": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"xhr2": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz",
|
||||||
|
"integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8="
|
||||||
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"@testing-library/react": "^9.5.0",
|
"@testing-library/react": "^9.5.0",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
"@types/jest": "^24.0.0",
|
"@types/jest": "^24.0.0",
|
||||||
|
"@types/jsmediatags": "^3.9.1",
|
||||||
"@types/node": "^12.12.55",
|
"@types/node": "^12.12.55",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
"@types/w3c-web-usb": "^1.0.4",
|
"@types/w3c-web-usb": "^1.0.4",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
|
"jsmediatags": "^3.9.3",
|
||||||
"lint-staged": "^10.2.13",
|
"lint-staged": "^10.2.13",
|
||||||
"netmd-js": "^1.0.11",
|
"netmd-js": "^1.0.11",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
|
|
|
@ -17,6 +17,9 @@ import ToggleButton from '@material-ui/lab/ToggleButton';
|
||||||
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
|
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
|
||||||
import { TransitionProps } from '@material-ui/core/transitions';
|
import { TransitionProps } from '@material-ui/core/transitions';
|
||||||
import { Typography } from '@material-ui/core';
|
import { Typography } from '@material-ui/core';
|
||||||
|
import Select from '@material-ui/core/Select';
|
||||||
|
import Input from '@material-ui/core/Input';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
const Transition = React.forwardRef(function Transition(
|
const Transition = React.forwardRef(function Transition(
|
||||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||||
|
@ -31,7 +34,7 @@ const useStyles = makeStyles(theme => ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
formControl: {
|
formControl: {
|
||||||
minWidth: 120,
|
minWidth: 60,
|
||||||
},
|
},
|
||||||
toggleButton: {
|
toggleButton: {
|
||||||
minWidth: 40,
|
minWidth: 40,
|
||||||
|
@ -41,29 +44,56 @@ const useStyles = makeStyles(theme => ({
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
|
rightBlock: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
titleFormControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ConvertDialog = (props: { files: File[] }) => {
|
export const ConvertDialog = (props: { files: File[] }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
let { visible, format } = useShallowEqualSelector(state => state.convertDialog);
|
let { visible, format, titleSource, titleFormat } = useShallowEqualSelector(state => state.convertDialog);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
dispatch(convertDialogActions.setVisible(false));
|
dispatch(convertDialogActions.setVisible(false));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChangeFormat = useCallback(
|
||||||
(ev: React.MouseEvent<HTMLElement>, newFormat: string) => {
|
(ev, newFormat) => {
|
||||||
dispatch(convertDialogActions.setFormat(newFormat));
|
if (newFormat === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(convertDialogActions.setFormat(newFormat as string));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeTitleSource = useCallback(
|
||||||
|
(ev, newTitleSource) => {
|
||||||
|
if (newTitleSource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(convertDialogActions.setTitleSource(newTitleSource));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeTitleFormat = useCallback(
|
||||||
|
(event: React.ChangeEvent<{ value: any }>) => {
|
||||||
|
dispatch(convertDialogActions.setTitleFormat(event.target.value));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleConvert = useCallback(() => {
|
const handleConvert = useCallback(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
dispatch(convertAndUpload(props.files, format));
|
dispatch(convertAndUpload(props.files, format, titleSource, titleFormat));
|
||||||
}, [dispatch, props, format]);
|
}, [dispatch, props, format, titleSource, titleFormat]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -76,11 +106,11 @@ export const ConvertDialog = (props: { files: File[] }) => {
|
||||||
>
|
>
|
||||||
<DialogTitle id="convert-dialog-slide-title">Upload Settings</DialogTitle>
|
<DialogTitle id="convert-dialog-slide-title">Upload Settings</DialogTitle>
|
||||||
<DialogContent className={classes.dialogContent}>
|
<DialogContent className={classes.dialogContent}>
|
||||||
<FormControl className={classes.formControl}>
|
<FormControl>
|
||||||
<Typography component="label" variant="caption" color="textSecondary">
|
<Typography component="label" variant="caption" color="textSecondary">
|
||||||
Mode
|
Recording Mode
|
||||||
</Typography>
|
</Typography>
|
||||||
<ToggleButtonGroup value={format} exclusive onChange={handleChange} size="small">
|
<ToggleButtonGroup value={format} exclusive onChange={handleChangeFormat} size="small">
|
||||||
<ToggleButton className={classes.toggleButton} value="SP">
|
<ToggleButton className={classes.toggleButton} value="SP">
|
||||||
SP
|
SP
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
@ -92,6 +122,31 @@ export const ConvertDialog = (props: { files: File[] }) => {
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<div className={classes.rightBlock}>
|
||||||
|
<FormControl className={classes.formControl}>
|
||||||
|
<Typography component="label" variant="caption" color="textSecondary">
|
||||||
|
Track title
|
||||||
|
</Typography>
|
||||||
|
<ToggleButtonGroup value={titleSource} exclusive onChange={handleChangeTitleSource} size="small">
|
||||||
|
<ToggleButton className={classes.toggleButton} value="file">
|
||||||
|
Filename
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton className={classes.toggleButton} value="media">
|
||||||
|
Media tags
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</FormControl>
|
||||||
|
{titleSource === 'media' ? (
|
||||||
|
<FormControl className={classes.titleFormControl}>
|
||||||
|
<Select value={titleFormat} color="secondary" input={<Input />} onChange={handleChangeTitleFormat}>
|
||||||
|
<MenuItem value={`title`}>Title</MenuItem>
|
||||||
|
<MenuItem value={`album-title`}>Album - Title</MenuItem>
|
||||||
|
<MenuItem value={`artist-title`}>Artist - Title</MenuItem>
|
||||||
|
<MenuItem value={`artist-album-title`}>Artist - Album - Title</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
|
|
@ -167,7 +167,11 @@ export const Main = (props: {}) => {
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ onDrop, accept: `audio/*`, noClick: true });
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: [`audio/*`, `video/mp4`],
|
||||||
|
noClick: true,
|
||||||
|
});
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import serviceRegistry from '../services/registry';
|
||||||
import { Wireformat, getTracks } from 'netmd-js';
|
import { Wireformat, getTracks } from 'netmd-js';
|
||||||
import { AnyAction } from '@reduxjs/toolkit';
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
import { getAvailableCharsForTrackTitle, framesToSec, sleepWithProgressCallback, sleep } from '../utils';
|
import { getAvailableCharsForTrackTitle, framesToSec, sleepWithProgressCallback, sleep } from '../utils';
|
||||||
|
import jsmediatags from 'jsmediatags';
|
||||||
|
import { TitleSourceType, TitleFormatType } from './convert-dialog-feature';
|
||||||
|
|
||||||
export function pair() {
|
export function pair() {
|
||||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
|
@ -192,7 +194,41 @@ export const WireformatDict: { [k: string]: Wireformat } = {
|
||||||
LP4: Wireformat.lp4,
|
LP4: Wireformat.lp4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertAndUpload(files: File[], format: string) {
|
async function getTrackNameFromMediaTags(file: File, titleFormat: TitleFormatType) {
|
||||||
|
const fileData = await file.arrayBuffer();
|
||||||
|
return await new Promise<string>((resolve, reject) => {
|
||||||
|
jsmediatags.read(new Blob([fileData]), {
|
||||||
|
onSuccess: data => {
|
||||||
|
const title = data.tags.title ?? 'Unknown Title';
|
||||||
|
const artist = data.tags.artist ?? 'Unknown Artist';
|
||||||
|
const album = data.tags.album ?? 'Unknown Album';
|
||||||
|
switch (titleFormat) {
|
||||||
|
case 'title': {
|
||||||
|
resolve(title);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'artist-title': {
|
||||||
|
resolve(`${artist} - ${title}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'album-title': {
|
||||||
|
resolve(`${album} - ${title}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'artist-album-title': {
|
||||||
|
resolve(`${artist} - ${album} - ${title}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertAndUpload(files: File[], format: string, titleSource: TitleSourceType, titleFormat: TitleFormatType) {
|
||||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
const { audioExportService, netmdService } = serviceRegistry;
|
const { audioExportService, netmdService } = serviceRegistry;
|
||||||
const wireformat = WireformatDict[format];
|
const wireformat = WireformatDict[format];
|
||||||
|
@ -275,6 +311,14 @@ export function convertAndUpload(files: File[], format: string) {
|
||||||
const { file, data } = item;
|
const { file, data } = item;
|
||||||
|
|
||||||
let title = file.name;
|
let title = file.name;
|
||||||
|
if (titleSource === 'media') {
|
||||||
|
try {
|
||||||
|
title = await getTrackNameFromMediaTags(file, titleFormat);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const extStartIndex = title.lastIndexOf('.');
|
const extStartIndex = title.lastIndexOf('.');
|
||||||
if (extStartIndex > 0) {
|
if (extStartIndex > 0) {
|
||||||
title = title.substring(0, extStartIndex);
|
title = title.substring(0, extStartIndex);
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { enableBatching } from 'redux-batched-actions';
|
import { enableBatching } from 'redux-batched-actions';
|
||||||
|
import { savePreference, loadPreference } from '../utils';
|
||||||
|
|
||||||
|
export type TitleSourceType = 'file' | 'media';
|
||||||
|
export type TitleFormatType = 'title' | 'album-title' | 'artist-title' | 'artist-album-title';
|
||||||
|
|
||||||
export interface ConvertDialogFeature {
|
export interface ConvertDialogFeature {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
format: string;
|
format: string;
|
||||||
|
titleSource: TitleSourceType;
|
||||||
|
titleFormat: TitleFormatType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ConvertDialogFeature = {
|
const initialState: ConvertDialogFeature = {
|
||||||
visible: false,
|
visible: false,
|
||||||
format: `LP2`,
|
format: `LP2`,
|
||||||
|
titleSource: loadPreference('trackTitleSource', 'file') as TitleSourceType,
|
||||||
|
titleFormat: loadPreference('trackTitleFormat', 'title') as TitleFormatType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const slice = createSlice({
|
const slice = createSlice({
|
||||||
|
@ -21,6 +29,14 @@ const slice = createSlice({
|
||||||
setFormat: (state, action: PayloadAction<string>) => {
|
setFormat: (state, action: PayloadAction<string>) => {
|
||||||
state.format = action.payload;
|
state.format = action.payload;
|
||||||
},
|
},
|
||||||
|
setTitleSource: (state, action: PayloadAction<TitleSourceType>) => {
|
||||||
|
state.titleSource = action.payload;
|
||||||
|
savePreference('trackTitleSource', state.titleSource);
|
||||||
|
},
|
||||||
|
setTitleFormat: (state, action: PayloadAction<TitleFormatType>) => {
|
||||||
|
state.titleFormat = action.payload;
|
||||||
|
savePreference('trackTitleFormat', state.titleFormat);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue