auto-audio-recorder/client/actions.js
2019-09-18 23:05:44 -04:00

212 lines
7.2 KiB
JavaScript

const child_process = require('child_process');
const request = require('request');
const fs = require('fs');
const LOG = require('../logger/logger.js')('action');
let Actions = {};
// Notes:
// While it may be nice to be able to use the webview tag
// in electron, it probably does not play DRM controlled
// content (i.e. Spotify) so this needs to connect to a
// firefox extension. This can be done by running a web
// server and sending messages to the client from the
// firefox client over said server.
//
// An alternative to this may be to use this:
// https://github.com/castlabs/electron-releases/
// Recording A Song Outline
// Song starts playing on firefox browser
// firefox extension sends web request to the recording client (song started (name, artists, album name, album art (small)))
// recording client starts recording (using gstreamer, song.guid + .wav)
// recording client downloads basic album art
// firefox extension looks up the better album art image
// firefox extension sends web request to the recording client (new song data with same guid)
// recording client downloads song full album art
// Song finishes playing on firefox browser
// firefox extension sends web request to the recording client (song stopped)
// recording client stops recording
// ffmpeg conversion begins to convert song.guid + .wav -> song.filename + .mp3 with album art and mp3 metadata
// Key Objects
// Requirements:
// gstreamer: gst-launch-1.0
// ffmpeg: ffmpeg
/* Song
* {
* guid: string ("11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"),
* filename: string ("Low-11bf5b37"),
* name: string ("Low")
* artists: list ([ "Michael", "Eric" ])
* duration: string ("3:15")
* album: {
* name: string ("Best Yoinks 2019")
* art: string ("https://spotify.com/art/872364")
* artFull: string ("https://spotify.com/art/2398423428374") [optional]
* }
* }
*/
// Removes Windows special characters from the song name for the file name
Actions.getSongFilename = function(song) {
return song.name.replace(/[\\\/:*?\"<>|]+/g, '') + ' ' + song.guid;
}
// uses a logger to log the output of a process
Actions.registerLogger = function(process, log) {
process.stdout.on('data', (chunk) => {
log.debug(chunk.toString('utf-8'));
});
process.stderr.on('data', (chunk) => {
log.error(chunk.toString('utf-8'));
});
}
// Starts a process and handles general errors
Actions.spawnProcess = function(name, command, args) {
let process = child_process.spawn(command, args);
LOG.info(`spawned ${name} ${process.pid}`);
process.on('close', (code, signal) => {
LOG.silly(`${name} ${process.pid} closed, code: ${code}, signal: ${signal}`);
});
process.on('exit', (code, signal) => {
LOG.debug(`${name} ${process.pid} exited, code: ${code}, signal: ${signal}`);
});
process.on('disconnect', () => {
LOG.silly(`${name} ${process.pid} disconnected from parent`);
});
process.on('error', (err) => {
LOG.silly(`${name} ${process.pid} error`, err);
});
return process;
}
/**
* Starts recording audio output
* @param {Song} song The song to start recording (used for file name)
* @returns the child process that was spawned
*/
Actions.startRecording = function(song) {
let device = 'alsa_output.pci-0000_00_1f.3.analog-stereo.monitor';
let output = '../intermediate/' + song.guid + '.wav';
// -v for verbose output
let recording_process = Actions.spawnProcess(
'recording process',
'gst-launch-1.0',
[ '-v', 'pulsesrc', 'device', '=', device, '!', 'wavenc', '!', 'filesink', 'location', '=', output ]
);
return recording_process;
}
/**
* Stops the recording process
* @param {*} recordingProcess The recording child process
*/
Actions.stopRecording = async function(recordingProcess) {
return new Promise((resolve, reject) => {
LOG.debug(`killing process ${recordingProcess.pid}`);
recordingProcess.on('exit', (code, signal) => {
resolve();
});
recordingProcess.kill('SIGINT');
});
}
/**
* Downloads the album art for a song. If song.album.art_full exists, uses that, else song.album.art
* @param {Song} song The song to download album art for
*/
Actions.downloadAlbumArt = async function(song) {
return new Promise((resolve, reject) => {
try {
let url = song.album.artFull || song.album.art || null;
if (url) {
let writeStream = fs.createWriteStream('../intermediate/' + song.guid + '.jpg'); // Assume .jpg image
writeStream.on('finish', () => { resolve(); });
request.get(url).pipe(writeStream);
} else {
fs.copyFile('../resources/default-album-art.jpg', '../intermediate/' + song.guid + '.jpg', (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
} catch (e) {
reject(e);
}
});
}
Actions.convertToMp3 = function(song) {
if (
!fs.existsSync('../intermediate/' + song.guid + '.wav') ||
!fs.existsSync('../intermediate/' + song.guid + '.jpg')
) {
LOG.warn(`Cannot convert ${song.name} to mp3: [${song.guid}.wav/${song.guid}.jpg] does not exist`);
return;
}
// Album Art: https://stackoverflow.com/questions/18710992/how-to-add-album-art-with-ffmpeg
let conversionProcess = Actions.spawnProcess(
'conversion process',
'ffmpeg', [
'-hide_banner',
'-i', '../intermediate/' + song.guid + '.wav',
'-f', 'mp3',
'../intermediate/' + song.guid + '.mp3'
]
);
return conversionProcess;
}
Actions.addMetadata = function(song) {
if (!fs.existsSync('../intermediate/' + song.guid + '.mp3')) {
LOG.warn(`Cannot add metadata to ${song.name}: [${song.guid}.mp3] does not exist`);
return;
}
let metadataProcess = Actions.spawnProcess(
'metadata process',
'ffmpeg', [
'-hide_banner',
'-i', '../intermediate/' + song.guid + '.mp3',
'-i', '../intermediate/' + song.guid + '.jpg',
'-map', '0:0',
'-map', '1:0',
'-c', 'copy',
'-id3v2_version', '3',
'-metadata:s:v', 'title=\'Album Cover\'',
'-metadata:s:v', 'comment=\'Cover (front)\'',
'-metadata', `title=\'${song.name}\'`,
'-metadata', `author=\'${song.artists.join(' / ')}\'`,
'-metadata', `album=\'${song.album.name}\'`,
'-b:a', '192k',
'../audio/' + song.filename + '.mp3'
]
);
return metadataProcess;
}
Actions.removeIntermediate = function(song) {
fs.unlink('../intermediate/' + song.guid + '.wav', (err) => {
if (err) {
LOG.error(`error removing ${song.guid}.wav`, err);
}
});
fs.unlink('../intermediate/' + song.guid + '.jpg', (err) => {
if (err) {
LOG.error(`error removing ${song.guid}.jpg`, err);
}
});
fs.unlink('../intermediate/' + song.guid + '.mp3', (err) => {
if (err) {
LOG.error(`error removing ${song.guid}.mp3`, err);
}
});
}
module.exports = Actions;