In this post, I decided to share with you what is “under the hood” of the latest Mikulski_Radio upgrades. As well as the progress of the process of creating these scripts: from the appearance of the idea to their implementation on the basis of the simplest tools. It is possible that for someone it will serve as a source of inspiration or a reason for laughter. For me, this will be a consolidation of the experience gained, as well as a guide for the future: if you suddenly have to restore all these algorithms again. For example, my own tutorials on installing and configuring Liquidsoap and Nginx have already come in handy more than once: when necessary, I just do everything following the instructions.
In this article, I will not dwell in great detail on some points related to the work of VPS or the features of Liquidsoap as a programming language. It is likely that for a better understanding of what is being discussed here, you should read the previous posts that relate to the creation of Mikulski_Radio:
https://mikulski.rocks/how-i-made-my-stream-radio-24-7/
https://mikulski.rocks/lofi-stream-24-7guide/
https://mikulski.rocks/how-works-request-system/
List of Boosty subscribers
Before the radio returned to broadcasting video files, the list of boosters was “sewn” into the main looped video. That is, every time there was a need to make any edits to the existing list, I had to edit this text in the video editor, render the video and upload it to the server. In general, it did not take much time and it was a blessing that Liquidsoap did not react with departures to hot file replacement (however, I still tried to replace files while switching the playlist to the news block, which created an additional inconvenience in the form of waiting). And I immediately realized that it would be more logical and convenient to use the file function in Liquidsoap “file.getter” and simply display the contents of the file. The nuance is that the text is displayed on the screen by plugins that render text into an image. The built-in native plugin is limited and cannot use any other fonts, but it can display a list. Third-party plugins “camlimages” (does not perceive Cyrillic) and “gd” (which I use to display the rest of the text) do not display the entire file, but only its first line.
At first, I decided to display the list of Boosters in a separate video: similar to switching to a news block, the playlist switches to a spare one, where the video with the list lies at a certain time interval. But then I decided to go back to displaying the list directly from the file, albeit in a not very plausible font. It only remained to figure out how to make the list not hang constantly on the screen, but appear from time to time.
The idea was to take the contents of the A-reference file (with a list inside) at a given time interval, copy its contents to file B (the contents of which are monitored by Liquidsoap for display), wait for some time and then rewrite the file in an empty line. You cannot delete the file, because Liquidsoap throws an error if it is missing, so ” ” is written to it.
In theory, this could be implemented inside Liquidsoap itself, but I decided to use nodejs to have more control over the settings: change the display intervals, directories, and so on, without having to restart Liquidsoap every time:
const fs = require('fs'); // built-in nodejs module for working with the file system
const list = '/home/boosters_updater/boosters_list' // the file-reference with a list
const boosters = '/home/radio/boosters' // the file displayed on the stream
//first, we write the emptiness to the file that goes to the stream, so that when the script restarts, nothing is exactly displayed on the screen
fs.writeFile(boosters, " ", (err) => {
if (err) {
console.log(err);
}
})
//main function:
function boosters_update() {
fs.readFile(list, "utf8", function(error,data){
if(error) throw error; //reading the reference file..
fs.writeFile(boosters, data, (err) => {
if (err) {
console.log(err); //..rewriting the displayed file with the contents of the reference
}
})
})
//the timeout of the function execution and the function itself to delete the contents of the displayed file
setTimeout(boosters_erase, 45000) //timeout 45 seconds (specified in milliseconds)
//delete function:
function boosters_erase() {
fs.writeFile(boosters, " ", (err) => {
if (err) {
console.log(err); //the file is overwritten by "emptiness"
}
})
}
}
setInterval(boosters_update, 60000 * 15) //the interval for performing the main function is set every 15 minutes.
A running line and announcements of live streams with control via a Telegram bot
I’ve been thinking about the running line for a long time, including in the context of displaying a list of boosters. But even here there were surprises related to the features of Liquidsoap:
The displayed text, in addition to coordinates, can be set the speed at which it will float across the screen, as well as the “cycle” argument, which determines whether the text will float only once (false) or will be looped (true). The thing is that the loop does not work at the end of the displayed text, but after reaching the “left border”. At the same time, this is not a literal border of the screen: the text can go beyond it and it is “somewhere there”. In general, in practice it turns out that, depending on the font size, it is possible to fit only a certain number of text characters for correct display. And if you set a negative speed with the cycle turned on, then the text will float to the right only once and will never happen again, because it will never reach the left border.
Still thinking in the context of displaying a list of Boosters, I came up with a good, but stupid idea: disable the loop and add a list of Boosters to the line at a certain interval through an additional script. In the end, I rejected this idea as unsuccessful for a number of reasons.
Также я пытался внутри Liquidsoap положить функцию отображения текста в другую функцию, которая бы ее вызывала в определенный интервал времени. Но это не привело ни к каким результатам. В лучшем случае, стрим запускался без ошибок, но текст не отображался вовсе.
I tried to find some solution on the forums dedicated to Liquidsoap and it seems like there is a crutch for this: create separate screen borders and put text in them (as far as I understand), thereby increasing the amount of information contained. But after several unsuccessful attempts to repeat it, I abandoned this idea, as it turned out to be clearly not according to my skills.
Therefore, I returned to simply displaying any information in this line that would fit into the calculated character limits. And while I was thinking about it, I decided that it would be nice to put notifications at the top of the stream that there is currently a stream on the main channel. And to make it easier to manage all this, also try to attach a Telegram bot so that it changes this information on the screen through commands.
In general, the principle is still the same: in Liquidsoap, “file.getter”, displaying the contents of files, and a third-party script slips the contents into these files. It’s just that now another Telebot module is added to the chain to interact with Telegram.
A template with several examples:
const fs = require('fs');
const TeleBot = require('telebot'); //npm install telebot
const token = 'telegram_bot_token'
const bot = new TeleBot(token); //bot with token connection
const info = '/home/radio/info'; //a file with text for a running line
//protection so that the bot responds only to a specific user:
const MY_ID = 000000; // telegram userID - unique Telegram user number
bot.mod('message', (data) => {
const msg = data.message;
const userId = msg.from.id;
if (userId !== MY_ID) {
data.message = {};
bot.sendMessage(userId, 'Action is not allowed'); //the bot will check the match by userID and if it does not match the specified one, it will issue a notification that the action is impossible
}
return data;
});
//the /say command with arguments. that is, everything that comes after the command will be written in the running line:
bot.on(/^\/say (.+)$/, (msg, props) => {
const text = props.match[1];
fs.writeFile(info, text, (err) => {
if (err) {
console.log(err);
}});
return bot.sendMessage(msg.from.id, text + ' -> is Published!')
});
//the /sociales command, which inserts the already prepared text into the file
bot.on('/socials', (msg) => {
fs.writeFile(info, "| Website: https://mikulski.rocks/ | Linktree: linktr.ee/Mikulski |", (err) => {
if (err) {
console.log(err);
}
});
return bot.sendMessage(msg.from.id, 'Socials is Published!')
});
//command /clear to clear the line
bot.on('/clear', (msg) => {
fs.writeFile(info, " ", (err) => {
if (err) {
console.log(err);
}
});
return bot.sendMessage(msg.from.id, 'Post is Cleared!')
});
//the /stream command to notify about streams on the Boosty
// with the content cleaning function 2 hours after the command is activated,
//similar to the example with the list of Boosters
bot.on('/stream', (msg) => {
fs.writeFile(info, 'Streamer is LIVE! ', (err) => {
if (err) {
console.log(err);
}});
setTimeout(go_live_erase, 60000 * 120)
function go_live_erase() {
fs.writeFile(info, " ", (err) => {
if (err) {
console.log(err);
}
})
}
return bot.sendMessage(msg.from.id, 'go_boosty -> is Published!')
});
bot.start() //launching the bot
Tracking errors in requests
For a long time there was a question about how to notify the user that an error was made in the request and Liquidsoap could not find a file with that name. It turned out that the request was accepted, the status bar displayed that the next track would be the track specified in the command. At the same time, an error appeared in the Liquidsoap log that the file was not found (not visible to the user) and another track was simply playing the next track. And only in this way it became clear that there was an error in the request or in the path to the file.
Music_Craft once noted that it would be nice to show that “something went wrong” when making requests. And I honestly tried inside Liquidsoap to find a way to catch the error “Nonexistent file or ill-formed URI” from the log in order to register it in a separate file. But nothing came of it.
But after a while, when I had already made a list of Boosters with a running line and was thinking about how to improve this, I remembered Music_Craft again and then it dawned on me:
You can save the Liquidsoap log to a separate file and check this file with a third-party script for the presence of a string containing “Nonexistent file or ill-formed URI”!
The script catches this line and saves a text error notification to a file, which is monitored by Liquidsoap for output to the screen. Then clears the log file so as not to run into this line again. And an interval of cleaning the log file is also added so that it does not grow to large volumes.
For this case, the line-reader module was needed.
const fs = require('fs');
const lineReader = require('line-reader'); //npm install line-reader
const log = '/home/radio/log/log_radio'; //the file where Liquidsoap saves the log
const request_error = '/home/radio/next-song'; //the file displayed on the stream (status bar)
//function for reading the log line by line and searching for specific content in this line
//in case of a match -> writing to the error file and clearing the log
function read() {
lineReader.eachLine(log, function(line) {
if (line.includes('Nonexistent file or ill-formed URI')) {
fs.writeFile(request_error, 'Oops! Invalid Request! File not found. ', (err) => {
if (err) {
console.log(err);
}});
fs.writeFile(log, ' ', (err) => {
if (err) {
console.log(err);
}});
console.log("Catched bad request");
}
});
}
//log clearing function
function flush () {
fs.writeFile(log, ' ', (err) => {
if (err) {
console.log(err);
}});
console.log("Log is flushed");
}
setInterval(read, 5000); //check-out interval (every 5 seconds)
setInterval(flush, 60000 * 60); //log clearing interval (every hour)
Request tracks via Telegram bot
When the catching of errors was done, I thought that why not then request tracks through Telegram. Since I’ve mastered the telebot module enough anyway to create my own simple bots.
In general, it was a small matter here. The technology was still worked out on requests through DonationAlerts and Twitch chat: https://mikulski.rocks/how-works-request-system/
In order for the bot to identify users by @username, and not by userID (which you still need to be able to find out) and how to create arrays, SpaceMelodyLab helped me a lot.
const fs = require('fs');
const TeleBot = require('telebot');
const token = 'Токен_телеграм_бота';
const bot = new TeleBot(token);
const request = '/home/radio/request'; //the file where the path to the file for the request is specified
const next_song = '/home/radio/next-song'; //file displaying the name of the next track (status bar)
const songlist = '/home/radio/songlist'; //a file with a list of tracks (for an example of creating commands)
//listing of users who can use the bot:
const users = ['user1', 'user2','user3'];
bot.mod('message', (data) => {
const msg = data.message;
const userId = msg.from.id;
const username = msg.from.username;
//const userId = msg.from.id;
//if the user is not in the list, then he receives a response that there is no access
if (users.indexOf(username) === -1) {
data.message = {};
bot.sendMessage(userId, 'Access only for subscribers');
}
return data;
});
//the /sr command with an argument
bot.on(/^\/sr (.+)/, (msg, props) => {
const text = props.match[1];
//a directory and file extension are added to the argument and saved to a file that reads Liquidsoap for the request.
fs.writeFile(request,'/home/radio/music/Mikulski - ' + text +'.mp4', (err) => {
if (err) {
console.log(err);
}});
//overwriting the status bar on the stream with a mention of the user who ordered the track and the track name
fs.writeFile(next_song, `Telegram request by ${ msg.from.first_name } - ` + text, (err) => {
if (err) {
console.log(err);
}});
console.log(`${ msg.from.first_name } ` + 'Requested ' + text);
//the bot writes a response that the request has been accepted for processing
return bot.sendMessage(msg.from.id, `${ msg.from.first_name }, ` + '\nyour Groovy request has been accepted for processing!\n|\nВаш бодрый реквест принят на обработку!');
});
//command /songlist to output a list of tracks from a file
bot.on('/songlist', (msg) => {
fs.readFile(songlist, "utf8", function(error,data){
if(error) throw error;
return bot.sendMessage(msg.from.id, data)
});
});
bot.start()