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"
|
||||
}
|
||||
},
|
||||
"@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": {
|
||||
"version": "7.0.4",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"xhr2": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz",
|
||||
"integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8="
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "3.0.0",
|
||||
"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/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jsmediatags": "^3.9.1",
|
||||
"@types/node": "^12.12.55",
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"@types/w3c-web-usb": "^1.0.4",
|
||||
"clsx": "^1.1.1",
|
||||
"husky": "^4.2.5",
|
||||
"jsmediatags": "^3.9.3",
|
||||
"lint-staged": "^10.2.13",
|
||||
"netmd-js": "^1.0.11",
|
||||
"prettier": "^1.19.1",
|
||||
|
|
|
@ -17,6 +17,9 @@ import ToggleButton from '@material-ui/lab/ToggleButton';
|
|||
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
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(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
|
@ -31,7 +34,7 @@ const useStyles = makeStyles(theme => ({
|
|||
flexDirection: 'row',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
minWidth: 60,
|
||||
},
|
||||
toggleButton: {
|
||||
minWidth: 40,
|
||||
|
@ -41,29 +44,56 @@ const useStyles = makeStyles(theme => ({
|
|||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
rightBlock: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
titleFormControl: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const ConvertDialog = (props: { files: File[] }) => {
|
||||
const dispatch = useDispatch();
|
||||
const classes = useStyles();
|
||||
|
||||
let { visible, format } = useShallowEqualSelector(state => state.convertDialog);
|
||||
let { visible, format, titleSource, titleFormat } = useShallowEqualSelector(state => state.convertDialog);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(convertDialogActions.setVisible(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(ev: React.MouseEvent<HTMLElement>, newFormat: string) => {
|
||||
dispatch(convertDialogActions.setFormat(newFormat));
|
||||
const handleChangeFormat = useCallback(
|
||||
(ev, 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]
|
||||
);
|
||||
|
||||
const handleConvert = useCallback(() => {
|
||||
handleClose();
|
||||
dispatch(convertAndUpload(props.files, format));
|
||||
}, [dispatch, props, format]);
|
||||
dispatch(convertAndUpload(props.files, format, titleSource, titleFormat));
|
||||
}, [dispatch, props, format, titleSource, titleFormat]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -76,11 +106,11 @@ export const ConvertDialog = (props: { files: File[] }) => {
|
|||
>
|
||||
<DialogTitle id="convert-dialog-slide-title">Upload Settings</DialogTitle>
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<FormControl>
|
||||
<Typography component="label" variant="caption" color="textSecondary">
|
||||
Mode
|
||||
Recording Mode
|
||||
</Typography>
|
||||
<ToggleButtonGroup value={format} exclusive onChange={handleChange} size="small">
|
||||
<ToggleButtonGroup value={format} exclusive onChange={handleChangeFormat} size="small">
|
||||
<ToggleButton className={classes.toggleButton} value="SP">
|
||||
SP
|
||||
</ToggleButton>
|
||||
|
@ -92,6 +122,31 @@ export const ConvertDialog = (props: { files: File[] }) => {
|
|||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</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>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
|
|
|
@ -167,7 +167,11 @@ export const Main = (props: {}) => {
|
|||
},
|
||||
[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();
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import serviceRegistry from '../services/registry';
|
|||
import { Wireformat, getTracks } from 'netmd-js';
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { getAvailableCharsForTrackTitle, framesToSec, sleepWithProgressCallback, sleep } from '../utils';
|
||||
import jsmediatags from 'jsmediatags';
|
||||
import { TitleSourceType, TitleFormatType } from './convert-dialog-feature';
|
||||
|
||||
export function pair() {
|
||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||
|
@ -192,7 +194,41 @@ export const WireformatDict: { [k: string]: Wireformat } = {
|
|||
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) {
|
||||
const { audioExportService, netmdService } = serviceRegistry;
|
||||
const wireformat = WireformatDict[format];
|
||||
|
@ -275,6 +311,14 @@ export function convertAndUpload(files: File[], format: string) {
|
|||
const { file, data } = item;
|
||||
|
||||
let title = file.name;
|
||||
if (titleSource === 'media') {
|
||||
try {
|
||||
title = await getTrackNameFromMediaTags(file, titleFormat);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const extStartIndex = title.lastIndexOf('.');
|
||||
if (extStartIndex > 0) {
|
||||
title = title.substring(0, extStartIndex);
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
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 {
|
||||
visible: boolean;
|
||||
format: string;
|
||||
titleSource: TitleSourceType;
|
||||
titleFormat: TitleFormatType;
|
||||
}
|
||||
|
||||
const initialState: ConvertDialogFeature = {
|
||||
visible: false,
|
||||
format: `LP2`,
|
||||
titleSource: loadPreference('trackTitleSource', 'file') as TitleSourceType,
|
||||
titleFormat: loadPreference('trackTitleFormat', 'title') as TitleFormatType,
|
||||
};
|
||||
|
||||
const slice = createSlice({
|
||||
|
@ -21,6 +29,14 @@ const slice = createSlice({
|
|||
setFormat: (state, action: PayloadAction<string>) => {
|
||||
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