Reviewed-on: #34
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,5 +6,6 @@ node_modules/
|
||||
*.log
|
||||
*.txt
|
||||
*.env
|
||||
*.wav
|
||||
!requirements.txt
|
||||
*testOP25Dir/
|
||||
@@ -10,6 +10,7 @@ const { closeProcessWrapper } = require("../utilities/utilities");
|
||||
|
||||
// Global vars
|
||||
let pythonProcess;
|
||||
let recordingProcess;
|
||||
|
||||
/**
|
||||
* Get Status of the discord process
|
||||
@@ -81,4 +82,78 @@ exports.leaveServer = async (req, res) => {
|
||||
pythonProcess = await closeProcessWrapper(pythonProcess);
|
||||
|
||||
return res.sendStatus(202);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a recording of what the bot is listening to, if it's currently connected
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
exports.startRecording = async (req, res) => {
|
||||
log.INFO("Starting recording")
|
||||
//if (pythonProcess === undefined) return res.sendStatus(204);
|
||||
if (!recordingProcess === undefined) return res.sendStatus(202);
|
||||
const deviceId = process.env.AUDIO_DEVICE_ID;
|
||||
const filename = "./recordings/" + new Date().toJSON().slice(0,10) + ".wav";
|
||||
|
||||
// Joining the server to record
|
||||
log.INFO("Start recording: ", deviceId, filename);
|
||||
if (process.platform === "win32") {
|
||||
log.DEBUG("Starting Windows Python");
|
||||
recordingProcess = await spawn('python', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/").toString() });
|
||||
}
|
||||
else {
|
||||
log.DEBUG("Starting Linux Python");
|
||||
recordingProcess = await spawn('python3', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/") });
|
||||
}
|
||||
|
||||
await this.getProcessOutput(recordingProcess);
|
||||
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the recording if the bot is currently recording
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
exports.stopRecording = async (req, res) => {
|
||||
log.INFO("Stopping recording the server");
|
||||
if (!recordingProcess) return res.sendStatus(202)
|
||||
|
||||
recordingProcess = await closeProcessWrapper(recordingProcess);
|
||||
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the output of a running process
|
||||
*
|
||||
* @param {*} runningProcess
|
||||
* @returns
|
||||
*/
|
||||
exports.getProcessOutput = async (runningProcess) => {
|
||||
let fullOutput;
|
||||
runningProcess.stdout.setEncoding('utf8');
|
||||
runningProcess.stdout.on("data", (data) => {
|
||||
botLog.VERBOSE("From Process: ", data);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
runningProcess.stderr.on('data', (data) => {
|
||||
botLog.VERBOSE(`stderr: ${data}`);
|
||||
fullOutput += data.toString();
|
||||
});
|
||||
|
||||
runningProcess.on('close', (code) => {
|
||||
log.DEBUG(`child process exited with code ${code}`);
|
||||
log.VERBOSE("Full output from bot: ", fullOutput);
|
||||
});
|
||||
|
||||
runningProcess.on("error", (code, signal) => {
|
||||
log.ERROR("Error from the process: ", code, signal);
|
||||
});
|
||||
}
|
||||
150
Client/pdab/recorder.py
Normal file
150
Client/pdab/recorder.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import pyaudio
|
||||
import wave, logging, threading, time, queue, signal, argparse
|
||||
from os import path, makedirs
|
||||
|
||||
logging.basicConfig(format="%(asctime)s: %(message)s", level=logging.INFO,datefmt="%H:%M:%S")
|
||||
|
||||
class DiscordRecorder:
|
||||
def __init__(self, DEVICE_ID, CHUNK = 1024, FORMAT = pyaudio.paInt16, CHANNELS = 2, RATE = 48000, FILENAME = "./recs/radio.wav"):
|
||||
self.pa_instance = pyaudio.PyAudio()
|
||||
|
||||
self.DEVICE_ID = DEVICE_ID
|
||||
self.CHUNK = CHUNK
|
||||
self.FORMAT = FORMAT
|
||||
self.CHANNELS = CHANNELS
|
||||
self.RATE = RATE
|
||||
|
||||
self.FILENAME = FILENAME
|
||||
self._check_file_path_exists()
|
||||
|
||||
self.queued_frames = queue.Queue()
|
||||
|
||||
self.stop_threads = threading.Event()
|
||||
|
||||
self.recording_thread = None
|
||||
self.saving_thread = None
|
||||
|
||||
self.running_stream = None
|
||||
|
||||
# Wrapper to check if the given filepath (not file itself) exists
|
||||
def _check_file_path_exists(self):
|
||||
if not path.exists(path.dirname(self.FILENAME)):
|
||||
makedirs(path.dirname(self.FILENAME), exist_ok=True)
|
||||
|
||||
# Wrapper for the recorder thread; Adds new data to the queue
|
||||
def _recorder(self):
|
||||
logging.info("* Recording Thread Starting")
|
||||
while True:
|
||||
data = self.running_stream.read(self.CHUNK)
|
||||
self.queued_frames.put(data)
|
||||
|
||||
# check for stop
|
||||
if self.stop_threads.is_set():
|
||||
break
|
||||
|
||||
# Wrapper for saver thread; Saves the queue to the file
|
||||
def _saver(self):
|
||||
logging.info("* Saving Thread Starting")
|
||||
while True:
|
||||
if not self.queued_frames.empty():
|
||||
dequeued_frames = []
|
||||
for i in range(self.queued_frames.qsize()):
|
||||
dequeued_frames.append(self.queued_frames.get())
|
||||
|
||||
if not path.isfile(self.FILENAME):
|
||||
wf = wave.open(self.FILENAME, 'wb')
|
||||
wf.setnchannels(self.CHANNELS)
|
||||
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
|
||||
wf.setframerate(self.RATE)
|
||||
wf.writeframes(b''.join(dequeued_frames))
|
||||
wf.close()
|
||||
else:
|
||||
read_file = wave.open(self.FILENAME, 'rb')
|
||||
read_file_data = read_file.readframes(read_file.getnframes())
|
||||
read_file.close()
|
||||
|
||||
wf = wave.open(self.FILENAME, 'wb')
|
||||
wf.setnchannels(self.CHANNELS)
|
||||
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
|
||||
wf.setframerate(self.RATE)
|
||||
|
||||
wf.writeframes(read_file_data)
|
||||
wf.writeframes(b''.join(dequeued_frames))
|
||||
wf.close()
|
||||
|
||||
# check for stop
|
||||
if self.stop_threads.is_set():
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
# Start the recording function
|
||||
def start_recording(self):
|
||||
logging.info("* Recording")
|
||||
|
||||
self.running_stream = self.pa_instance.open(
|
||||
input_device_index=self.DEVICE_ID,
|
||||
format=self.FORMAT,
|
||||
channels=self.CHANNELS,
|
||||
rate=self.RATE,
|
||||
input=True,
|
||||
frames_per_buffer=self.CHUNK
|
||||
)
|
||||
|
||||
self.recording_thread = threading.Thread(target=self._recorder)
|
||||
self.recording_thread.start()
|
||||
|
||||
self.saving_thread = threading.Thread(target=self._saver)
|
||||
self.saving_thread.start()
|
||||
|
||||
# Stop the recording function
|
||||
def stop_recording(self):
|
||||
self.stop_threads.set()
|
||||
self.recording_thread.join()
|
||||
self.saving_thread.join()
|
||||
self.running_stream.stop_stream()
|
||||
self.running_stream.close()
|
||||
self.pa_instance.terminate()
|
||||
|
||||
logging.info("* Done recording")
|
||||
|
||||
|
||||
class GracefulExitCatcher:
|
||||
def __init__(self, stop_callback):
|
||||
self.stop = False
|
||||
|
||||
# The function to run when the exit signal is caught
|
||||
self.stop_callback = stop_callback
|
||||
|
||||
# Update what happens when these signals are caught
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
||||
|
||||
def exit_gracefully(self, *args):
|
||||
logging.info("* Stop signal caught...")
|
||||
|
||||
# Stop the main loop
|
||||
self.stop = True
|
||||
|
||||
# Run the given callback function
|
||||
self.stop_callback()
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
|
||||
parser.add_argument("filename", type=str, help="The filepath/filename of the output file")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.debug("Arguments:", args)
|
||||
|
||||
recorder = DiscordRecorder(args.deviceId, FILENAME=args.filename)
|
||||
|
||||
exit_catcher = GracefulExitCatcher(recorder.stop_recording)
|
||||
|
||||
recorder.start_recording()
|
||||
|
||||
while not exit_catcher.stop:
|
||||
time.sleep(1)
|
||||
@@ -27,4 +27,20 @@ router.post('/join', botController.joinServer);
|
||||
*/
|
||||
router.post('/leave', botController.leaveServer);
|
||||
|
||||
/** POST bot start recording
|
||||
* If the bot is in a channel, it will start to record what it hears
|
||||
*
|
||||
* The status of the bot: 200 = starting to record, 202 = already recording, 204 = not in a server, 500 + JSON = encountered error
|
||||
* @returns status
|
||||
*/
|
||||
router.post('/startRecording', botController.startRecording);
|
||||
|
||||
/** POST bot stop recording
|
||||
* If the bot is recording, it will stop recording
|
||||
*
|
||||
* The status of the bot: 200 = will stop the recording, 204 = not currently recording, 500 + JSON = encountered error
|
||||
* @returns status
|
||||
*/
|
||||
router.post('/stopRecording', botController.stopRecording);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
37
Server/commands/start-record.js
Normal file
37
Server/commands/start-record.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "start-record");
|
||||
const { getAllConnections } = require("../utilities/mysqlHandler");
|
||||
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('start-record')
|
||||
.setDescription('Starts recording all bots online'),
|
||||
example: "start-record",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
await interaction.reply(`Ok, ${interaction.member}. **Recording** will begin shorting.`);
|
||||
// Get nodes online
|
||||
getAllConnections((connections) => {
|
||||
for (const connection of connections){
|
||||
const reqOptions = new requestOptions("/bot/startRecording", "POST", connection.node.ip, connection.node.port);
|
||||
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
|
||||
log.VERBOSE("Response Object from node: ", connection, responseObj);
|
||||
if (!responseObj || !responseObj.statusCode == 202 || !responseObj.statusCode == 204) return false;
|
||||
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
|
||||
// Bot is recording
|
||||
await interaction.channel.send(`**${connection.clientObject.name} is now recording**`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
37
Server/commands/stop-record.js
Normal file
37
Server/commands/stop-record.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "stop-record");
|
||||
const { getAllConnections } = require("../utilities/mysqlHandler");
|
||||
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('stop-record')
|
||||
.setDescription('Starts recording all bots online'),
|
||||
example: "stop-record",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
await interaction.reply(`Ok, ${interaction.member}. **Recording** will stop shorting.`);
|
||||
// Get nodes online
|
||||
getAllConnections((connections) => {
|
||||
for (const connection of connections){
|
||||
const reqOptions = new requestOptions("/bot/stopRecording", "POST", connection.node.ip, connection.node.port);
|
||||
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
|
||||
log.VERBOSE("Response Object from node: ", connection, responseObj);
|
||||
if (!responseObj || !responseObj.statusCode == 204) return false;
|
||||
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
|
||||
// Bot is recording
|
||||
await interaction.channel.send(`**${connection.clientObject.name} has stopped recording**`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction) {
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
log.VERBOSE("Interaction for command: ", command);
|
||||
log.VERBOSE("Interaction created for command: ", command);
|
||||
|
||||
// Execute autocomplete if the user is checking autocomplete
|
||||
if (interaction.isAutocomplete()) {
|
||||
|
||||
Reference in New Issue
Block a user