2019-09-19 03:05:44 +00:00
|
|
|
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);
|
2022-12-17 22:05:37 +00:00
|
|
|
LOG.info(`spawned ${name} ${process.pid}`, { name: name, command: command, args: args });
|
2019-09-19 03:05:44 +00:00
|
|
|
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) {
|
2022-12-17 22:05:37 +00:00
|
|
|
// pacmd list-sinks
|
|
|
|
//let device = 'alsa_output.pci-0000_00_1f.3.analog-stereo.monitor';
|
|
|
|
let device = 'alsa_output.pci-0000_00_1b.0.analog-stereo.monitor';
|
2019-09-19 03:05:44 +00:00
|
|
|
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',
|
2019-09-27 16:57:49 +00:00
|
|
|
'-b:a', '192k',
|
2019-09-19 03:05:44 +00:00
|
|
|
'../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;
|
|
|
|
}
|
2019-09-27 16:57:49 +00:00
|
|
|
LOG.debug(`adding metadata: title: ${song.name}, artist: ${song.artists.join(' / ')}, album: ${song.album.name}`);
|
2019-09-19 03:05:44 +00:00
|
|
|
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',
|
2019-09-27 16:57:49 +00:00
|
|
|
'-metadata:s:v', 'title=Album Cover',
|
|
|
|
'-metadata:s:v', 'comment=Cover (front)',
|
|
|
|
'-metadata', `title=${song.name}`,
|
|
|
|
'-metadata', `artist=${song.artists.join(' / ')}`,
|
|
|
|
'-metadata', `album=${song.album.name}`,
|
2019-09-19 03:05:44 +00:00
|
|
|
'../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;
|