Coolstory: How the Request System works on the Mikulski_Radio (Liquidsoap, Twitch, Donation Alerts) – Mikulski
Site Overlay

Coolstory: How the Request System works on the Mikulski_Radio (Liquidsoap, Twitch, Donation Alerts)

A consistent and, if possible, fascinating story about how it turned out without coding skills and in a short time to add a track ordering system to Mikulski_Radio.

Donation-Alerts-in-twitch-chat

The path begins with a js script that parses donation notifications from the DonationAlerts service to the Twitch chat.
Setup and installation are very simple: the repository is copied and the settings file is edited (settings.js), which specifies the necessary platform tokens.
Then with the command “node bot.js” the bot is activated.
The experience gained from installing Discobot (broadcasting radio to Discord voice chat) taught me how to use the pm2 utility to run and manage js scripts – it is very convenient to monitor and run many applications in the background with this manager. The “pm2 start” command bot.js “.

Это изображение имеет пустой атрибут alt; его имя файла - 01_pm2_log-1024x489.jpg
PM2 LOG
PM2 LIST


It is worth noting that the “npm install” command does not need to be entered for installation, otherwise module dependencies will break due to their version conflicts (I lost a lot of time on this error).

Of the technical nuances, I was upset by the instability of the connection of the DonationAlerts web socket in the context of 24/7 operation: communication interruptions were noticed after a day or two. From a similar script DonationAlertsNotificationBot, I borrowed lines for automatic reconnection, and also added another line at the end, which I found on one of the forums, where a person solved the problem in such a way that his script could not connect to the socket.
Source code:

const io = require('socket.io-client');
const socket = io("https://socket.donationalerts.ru:443");

After upgrade:

const io = require('socket.io-client');
const socket = io("https://socket.donationalerts.ru:443", {
                    reconnection: true,
                    reconnectionDelay: 1000,
                    reconnectionDelayMax: 5000,
                    reconnectionAttempts: Infinity},
                    { transports: ["websocket"] }); 

Parts to output to the log:

//donation alerts
socket.on('connect', function(data){
    console.log('Connected to Donation Alerts successfully!')
});
socket.on("connect_error", (err) => {
    console.error(`DA connect_error ${err.message}`);
    process.exit()
});

 socket.on('reconnect_attempt', () => {
                console.log('DA:' + ' Trying to reconnect to service')
            });

            socket.on('disconnect', () => {
                console.log('DA:' + ' Socket disconnected from service')
            });

However, even after upgrading the code and despite the messages in the reconnection log, the problem did not disappear: similarly, after a few days, the DonationAlerts socket did not transmit the event. At the moment, I continue to track this moment on the active duplicate of the bot. In the meantime, to increase reliability, I decided to restart the script every hour through the crontab utility, which gives the command “pm2 restart id process” to the console at a given interval. And it ensures smooth operation.

Это изображение имеет пустой атрибут alt; его имя файла - 03_crontab_e.jpg
CRONTAB -E

An alternative solution for reconnecting the DA socket

In general, after studying some information, it turned out that I was not the only one who faced problems reconnecting the socket connection through the socket-io module. Since the version of this module in this project is pretty old (and installing a more recent version breaks dependencies, as I noted above), as well as the fact that the solution (as I understood, for each disconnection, it is necessary to create a new connection) requires a more subtle understanding of programming, I came up with another idea.
I noticed that pm2 automatically restarts the script when it stops working for some reason. So, I decided to stop the script every time a discount occurs, adding the process.exit line to the block with the catch of the discount:

            socket.on('disconnect', () => {
                console.log(' Socket disconnected from DA_Main');
                process.exit(0);
            });

Thus, when the socket disconnects, the script stops its process, and pm2 starts it again. And no longer need to restart every hour via crontab.


There are no problems with connecting to Twitch, the module built into the script tmi.js (Twitch Messaging Interface) automatically resumes the chat connection when there are interruptions.


Considering the lines that are responsible for transmitting the message from the alert to the chat..

socket.on('donation', function(msg){
  donate = JSON.parse(msg)
  client.action(options.channels[0]," | "+ donate.username + " - " + "[" + donate.amount + " " +donate.currency+"]" + '\n' + donate.message)
}); 

..I was thinking that it would be great to still save some of the information to a text file. And already in Liquidsoap via the function file.getter (conditionally: constant reading of a given file) to output the last donation on the stream.
I quickly Googled that there is a function fs.WriteFile, but I managed to achieve operability only with a hint from SpaceMelodyLab:

socket.on('donation', function(msg){
  donate = JSON.parse(msg)
    client.action(options.channels[0]," DonationAlerts.com/r/Mikulski : "+ donate.username + " donated " + donate.amount + " " + donate.currency + " and said: " + "'" + donate.message + "'")
  fs.writeFile('/Donation-Alerts-in-twitch-chat/lastdonate.txt', "Latest Tipper: " + donate.username + " - " + donate.amount + " " + donate.currency, 'utf8', function(err) {if (err) return console.log(err);
  console.log('Lastdonate Updated!')});

Meanwhile…

In order to shorten the text, I will not go into a detailed description of the activities that preceded the next step. But a few words are worth saying:

  • Initially, I made audioinput in Liquidsoap from my radio broadcast from the Shoutcast server (back in the summer I set it up purely out of curiosity). As a result, I rebuilt the system the other way around. Now Liquidsoap distributes two streams in parallel: one video (actually, stream) and the second only audio in Shoutcast. This step made it easier to update the playlist, made it possible to add a “prediction” of the next track, and also opened up the potential for requests.
  • It took a lot of time to solve the problem with incorrect Cyrillic display from a text file. I went through the text encodings in the file, tried to assign the encoding inside Liquidsoap, changed fonts, tried to change the language of the OS… But everything was decided almost by accident. In the documentation, I noticed that Liquidsoap uses the first library it came across to graphically display text (I came to the conclusion that following the list in alphabetical order). In general, I removed the camlimages plugin, installed gd – and everything worked!

Request Queue

I assumed that Liquidsoap provided the ability to queue tracks for playback on request. And after some time in The Liquidsoap book, I came across a chapter where this is described and several different methods for implementation are given. After playing with them, I decided on the simplest and most effective:

def on_request()
fname = string.trim(file.contents("/radio/request.txt"))
log.important("Playing #{fname}.")
queue.push.uri(fname)
end
file.write(data=string_of(time()), "/radio/request.txt")
file.watch("/radio/request.txt", on_request)

What’s going on here:

  • The on_request function is created.
  • The name variable is assigned, which “takes out” the contents of the file (request.txt , in this case), storing the full path to the audio file.
  • The contents of fname are output to the log.
  • A track taken from the path specified in the fname (that is, from a file request.txt) is put in the playback queue.
  • The on_request function is closed.
  • The file where the path to the track is stored (request.txt ) is overwritten with unnecessary data. Just a temporary value in milliseconds. This is necessary so that when the script is run, this file definitely exists.
  • Indicates that you need to monitor changes in the file request.txt and when they happen – call the on_request function.
    This function will look into the file and if the correct path to the file is specified there, it will put it in the queue. If there is no file on the specified path, then a message will appear in the log that such a file does not exist and Liquidsoap will continue to work calmly.

Well, since I had already learned how to save the text of a message from a donation to a text file, the next move was already clear. In the script bot.js added lines that are saved in a file request.txt text “/path/to/folder/Mikulski – [message from donation].mp3”:


socket.on('donation', function(msg){
  donate = JSON.parse(msg)
      fs.writeFile('/radio/request.txt', "/music/Mikulski - " + donate.message + ".mp3", 'utf8', function(err) {if (err) return console.log(err);
  console.log('Request Accepted!')});
});

The name of the track is written in the donation message, in request.txt the full path to the track is saved and it gets in the queue.
According to the same principle, in bot.js added functions that save information to files next-song.txt (“Next, a request from [Username] – [message from donation]”) and donation.txt (in a larger font, “[Username] donated [Amount] [Currency]! Thank you!”).

Requests from the Twitch Chat

It would seem that you can stop there: the price of the request is minimal, the cross-platform is maximum, and it’s simply a miracle that it turned out to be implemented at all.
But I was haunted by the desire to encourage generous boosters (they already spend a decent amount on me per month) and the fact that the bot for Twitch, in fact, already exists – just add a command tied to a VIP badge, distribute badges to boosters and that’s it!
However, I immediately rejected the idea of VIP badges, because I planned to build a more flexible system. In addition, most of them have already been distributed, and it is somehow indecent to select and redistribute them.
The Technical Specifications was formulated by itself: the command can only be activated by specific users, a cooldown to eliminate a possible mess and save the text of the message to a file after !sr.
I was counting on a serene walk for a couple of hours, but problems arose literally at every stage. For some reason, I assumed that the cooldown and reading of the command arguments were provided and embedded in tmi.js, and you will have to urinate only with reference to a specific Username.
It turned out that all these points need to be programmed manually. With Google and with grief in half, I stuck on this quest for 12 hours…

It turned out to be easy to let the bot know which user to listen to on which command to listen to:

client.on('message', (channel, tags, message, self) => {
if(self) return; 
if ('!sr' && tags.username == 'mikulski_') { 
    client.say(channel, `@${tags.username} , your groovy request has been accepted for processing!`);}
};

There was a lot of hassle with the selection of the right copy paste to install the cooldown. At the same time, I wanted to make the cooldown apply only to the user who used the command, and not to the team itself as a whole. But it did not work out:

var block = false;

client.on('message', (channel, tags, message, self) => {
if(self) return;
 if (command === 'sr' && tags.username == 'mikulski_radio') { 
  if (!block) {
    client.say(channel, `@${tags.username} , your groovy request has been accepted for processing!`);

 block = true
 setTimeout(() => {
  block = false;} , (60 * 3000))
 }
 else {
  client.say(channel, `@${tags.username} Please wait a couple of minutes before the next request! <3`)
 }
};

The most extravaganza was with the “extraction” of an argument from a message in order to save it as a request to a text file.
First, I used the guide, which suggested taking the argument using regExp regular expressions. And I considered this a working option until it turned out in the “combat test” that the script crashes from almost any chat message (even not related to the command) with numbers or symbols. Fortunately, pm2 automatically restarted it. I had to urgently look for another way.
The slice and split method proved to be much better: the first first element of the message (!sr) is cut off, the remaining elements are collected in a heap and all this is declared as a variable to denote the chat command:

client.on('message', (channel, tags, message, self) => {
if(self) return;

const argument = message.slice(1).split(' ');
const command = argument.shift().toLowerCase();

However, the following catch was expected here: if there is more than one word in the message (for example, Where Are You), then at the output of the function .split(‘ ‘) – these words were separated by commas (Where, Are,You).
Another function was needed, which, after slice.split, converts the data into a “string” (otherwise, an error in the script will occur on .replace), and then commas are replaced with spaces from this “string”.:

const arg = message.slice(1).split(' ');
const command = arg.shift().toLowerCase();
const argument = arg.toString().replace(/,/g," ");

In the end, the command/respone block itself looks like this:

//Mikulski_
 if (command === 'sr' && tags.username == 'mikulski_') { 
  if (!block) {
    client.say(channel, `@${tags.username} , your groovy request has been accepted for processing!`);
 fs.writeFile('/radio/request.txt', "/music/Mikulski - " + argument + ".mp3", function(err) {if (err) return console.log(err)});
 console.log(' Boosty Request updated!');
 fs.writeFile('/radio/next-song.txt', "Request by Booster "  + tags.username + " : " + argument + " ", function(err) {if (err) return console.log(err)});
 console.log(' Boosty Requester updated!');
 block = true
 setTimeout(() => {
  block = false;} , (60 * 3000))
 }
 else {
  client.say(channel, `@${tags.username} Please wait a couple of minutes before the next request! <3`)
 }
};

Well, for each new user / booster, I copy this block, changing only the nickname. Here, of course, it would be worth optimizing by creating variables or even calling data from a separate file. But I have already suffered so much with this that I don’t have the strength and desire to master these techniques yet.

SpaceMelodyLab taught me how to create arrays with a list of allowed users so that I don’t have to duplicate the entire block for each subscriber. I also added the bot’s reaction if a person not from the list tries to use the command:

const users = ['user1', 'user2', 'user3'];

if (command === 'sr' && users.indexOf(tags.username) === -1) {
    client.say(channel, `@${tags.username} , to use this command, you must be a Boosty subscriber (Mini-Merchpack Tier) - https://boosty.to/mikulski`);
    console.log(`${tags.username} tried to request without subscription`);
}

 if (command === 'sr' && users.indexOf(tags.username) >=0) { 
  if (!block) {
    client.say(channel, `@${tags.username} , your groovy request has been accepted for processing!`);
 fs.writeFile('/radio/request', "/radio/music/Mikulski - " + argument + ".mp4", function(err) {if (err) return console.log(err)});
 console.log(' Boosty Request updated!');
 fs.writeFile('/radio/next-song', "Boosty Request by "  + tags.username + " : " + argument + " ", function(err) {if (err) return console.log(err)});
 console.log(' Boosty Requester updated!');
 block = true
 setTimeout(() => {
  block = false;} , (10 * 1000))
 }
 else {
  client.say(channel, `@${tags.username} Please wait a little before the next request! <3`)
 }
}
});

Telegram Alerts

Well, as the cherry on the cake, the script underwent another upgrade, which I lacked: it was tracking requests through Telegram alerts.
To do this, you will need the channel-telegram-bot module. I recommend creating a new folder, enter the module installation command in it:

npm i channel-telegram-bot

Then copy the ‘channel-telegram-bot’ and ‘telebot’ folders from the node_modules subfolder to the node_modules directory of the main bot. Why is that? In theory, the ‘npm i’ (or npm install) command should add modules without changing existing ones. But for some reason I don’t understand, after this action, some other modules were deleted from me. Therefore, it is better to be safe.

The code is added to the top of the script:

const channelBot = require('channel-telegram-bot');
const telegram = 'токен_телеграм_бота';

And below, by analogy with saving a donation message to a file, a block is created:

socket.on('donation', function(msg){
  donate = JSON.parse(msg)
  channelBot.sendMessage('@телеграм_канал_или_чат',"Request: "+ donate.username + " donated " + donate.amount + " " + donate.currency + " Сообщение: " + "'" + donate.message + "'", telegram)
    console.log('Radio Request sent to the Telegram!')

Where the address (via @) of the telegram channel or chat is written, where the notification should be sent, the message itself and the telegram variable in which the telegram bot token is stored.