Set track title by using media tags

This commit is contained in:
Stefano Brilli 2020-09-06 17:35:59 +02:00
parent 883379eff5
commit 103b12ebcc
6 changed files with 151 additions and 12 deletions

18
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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();

View File

@ -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);

View File

@ -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);
},
}, },
}); });