Skip to content

Instantly share code, notes, and snippets.

@Ekiserrepe
Created November 1, 2024 11:58
Show Gist options
  • Save Ekiserrepe/021d7230fd2f2ed53ad508eb2afdaf32 to your computer and use it in GitHub Desktop.
Save Ekiserrepe/021d7230fd2f2ed53ad508eb2afdaf32 to your computer and use it in GitHub Desktop.
//20241101 10_42
require('dotenv').config();
const { OBSWebSocket } = require('obs-websocket-js');
const xrpl = require('@transia/xrpl');
const fs = require('fs');
const path = require('path');
const ffmpeg = require('fluent-ffmpeg');
const axios = require('axios');
const obs = new OBSWebSocket();
let tracklist = [];
let overlayLines = [];
const songsFolderPath = `${__dirname}/music_folder`;
const maxOverlayLines = 10;
const screenHeight = 1080;
const fontSize = 24;
const overlayHeight = maxOverlayLines * fontSize;
let isPlaying = false;
let client;
let remainingMinutes = 0;
let counterInterval;
const xummUrl = 'https://xumm.app/api/v1/platform/account-meta/';
const tracklistFilePath = `${__dirname}/tracklist.txt`;
//const overlayFilePath = `${__dirname}/overlay.txt`;
// Load tracklist and overlay from files on startup
function loadPersistentData() {
if (fs.existsSync(tracklistFilePath)) {
const tracklistData = fs.readFileSync(tracklistFilePath, 'utf8');
tracklist = tracklistData.split('\n').filter(Boolean).map(Number);
}
}
// Save tracklist to file
function saveTracklist() {
fs.writeFileSync(tracklistFilePath, tracklist.join('\n'));
}
// Connect to OBS
async function connectToOBS() {
try {
await obs.connect(`ws://${process.env.OBS_ADDRESS}`, process.env.OBS_PASSWORD);
console.log('Connected to OBS');
await clearOverlay();
await setupImages();
await setupCounter();
await setupSongCounterOverlay();
} catch (err) {
console.error('Error connecting to OBS:', err);
}
}
// Fetch account alias for a given address
async function fetchAccountAlias(address) {
const url = `${xummUrl}${address}`;
const options = { method: 'GET', headers: { accept: 'application/json', 'Content-Type': 'application/json' } };
try {
const res = await axios(url, options);
const data = res.data;
if (data.xummProfile?.accountAlias) return data.xummProfile.accountAlias;
if (data.thirdPartyProfiles && data.thirdPartyProfiles.length > 0) {
for (const profile of data.thirdPartyProfiles) {
if (profile.accountAlias) return profile.accountAlias;
}
}
return address;
} catch (error) {
console.error('Error fetching account alias:', error);
return address;
}
}
// Clear overlay on startup
async function clearOverlay() {
overlayLines = [];
try {
await obs.call('SetInputSettings', {
inputName: 'OverlayText',
inputSettings: { text: '' },
overlay: true
});
console.log('Overlay cleared on startup.');
} catch (error) {
console.error('Error clearing the overlay:', error);
}
}
// Set up images and overlays in OBS
async function setupImages() {
const backgroundPath = path.join(__dirname, '/assets/background.png');
const qrPath = path.join(__dirname, '/assets/qr.png');
const infoText = `1 XAH = 1 SONG\nUSE MEMO FIELD TO CHOOSE ONE SONG\nSELECT FROM 1 TO 100 NUMBERS AS MEMO FIELD\nYOU CAN ADD MORE THAN ONE SONG\nSEND ANY AMOUNT\nEX: 10 XAH = 10 SONGS\nANY FRACTIONAL AMOUNT WILL BE SENT BACK\nEX: IF YOU SEND 34.24 XAH, 0.24 XAH WILL BE SENT BACK `;
// BackgroundImage configuration
if (fs.existsSync(backgroundPath)) {
try {
await obs.call('GetInputSettings', { inputName: 'BackgroundImage' });
console.log("Background image source already exists. Updating settings.");
await obs.call('SetInputSettings', {
inputName: 'BackgroundImage',
inputSettings: { file: backgroundPath },
overlay: true
});
} catch (error) {
if (error.code === 601) {
console.log("Creating new BackgroundImage source.");
await obs.call('CreateInput', {
sceneName: 'Scene',
inputName: 'BackgroundImage',
inputKind: 'image_source',
inputSettings: { file: backgroundPath }
});
} else {
console.error('Error setting up background image:', error);
}
}
const { sceneItemId: bgSceneItemId } = await obs.call('GetSceneItemId', {
sceneName: 'Scene',
sourceName: 'BackgroundImage'
});
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: bgSceneItemId,
sceneItemTransform: {
positionX: 1300,
positionY: screenHeight - 100,
alignment: 5
}
});
} else {
console.error("Background image not found at path:", backgroundPath);
}
// QRImage configuration
if (fs.existsSync(qrPath)) {
try {
await obs.call('GetInputSettings', { inputName: 'QRImage' });
console.log("QR image source already exists. Updating settings.");
await obs.call('SetInputSettings', {
inputName: 'QRImage',
inputSettings: { file: qrPath },
overlay: true
});
} catch (error) {
if (error.code === 601) {
console.log("Creating new QRImage source.");
await obs.call('CreateInput', {
sceneName: 'Scene',
inputName: 'QRImage',
inputKind: 'image_source',
inputSettings: { file: qrPath }
});
} else {
console.error('Error setting up QR image:', error);
}
}
const { sceneItemId: qrSceneItemId } = await obs.call('GetSceneItemId', {
sceneName: 'Scene',
sourceName: 'QRImage'
});
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: qrSceneItemId,
sceneItemTransform: {
positionX: (1920 - 150) / 2,
positionY: screenHeight - 200,
alignment: 5
}
});
} else {
console.error("QR image not found at path:", qrPath);
}
// Create and position the InfoText overlay
try {
await obs.call('GetInputSettings', { inputName: 'InfoText' });
console.log("InfoText overlay source already exists. Updating settings.");
await obs.call('SetInputSettings', {
inputName: 'InfoText',
inputSettings: { text: infoText, font: { face: "Arial", size: 30, style:"Bold" } }
});
} catch (error) {
if (error.code === 601) {
console.log("Creating new InfoText overlay source.");
await obs.call('CreateInput', {
sceneName: 'Scene',
inputName: 'InfoText',
inputKind: 'text_gdiplus',
inputSettings: { text: infoText, font: { face: "Arial", size: 30, style: "Bold" } }
});
} else {
console.error('Error setting up InfoText overlay:', error);
}
}
const { sceneItemId: infoSceneItemId } = await obs.call('GetSceneItemId', {
sceneName: 'Scene',
sourceName: 'InfoText'
});
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: infoSceneItemId,
sceneItemTransform: {
positionX: 0, // Positioned to the right of the QR
positionY: 575,
alignment: 5
}
});
console.log('InfoText overlay configured successfully.');
}
// Set up dynamic song counter overlay
async function setupSongCounterOverlay() {
try {
await obs.call('GetInputSettings', { inputName: 'SongCounter' });
await obs.call('SetInputSettings', {
inputName: 'SongCounter',
inputSettings: { text: `Remaining Songs: ${tracklist.length} songs`, font: { face: "Arial", size: 24 } }
});
} catch (error) {
if (error.code === 601) {
await obs.call('CreateInput', {
sceneName: 'Scene',
inputName: 'SongCounter',
inputKind: 'text_gdiplus',
inputSettings: { text: `Remaining Songs: ${tracklist.length} songs`, font: { face: "Arial", size: 24 } }
});
} else {
console.error('Error setting up SongCounter overlay:', error);
}
}
const { sceneItemId: counterSceneItemId } = await obs.call('GetSceneItemId', {
sceneName: 'Scene',
sourceName: 'SongCounter'
});
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: counterSceneItemId,
sceneItemTransform: {
positionX: 1302, // Position the counter on the screen
positionY: 930, // Below the time counter
alignment: 5
}
});
updateSongCounterOverlay();
}
// Update song counter display
async function updateSongCounterOverlay() {
try {
await obs.call('SetInputSettings', {
inputName: 'SongCounter',
inputSettings: { text: `Remaining Songs: ${tracklist.length} songs` }
});
} catch (error) {
console.error('Error updating SongCounter overlay:', error);
}
}
// Set up time counter overlay
async function setupCounter() {
try {
await obs.call('GetInputSettings', { inputName: 'TimeCounter' });
console.log("TimeCounter overlay source already exists. Updating settings.");
await obs.call('SetInputSettings', {
inputName: 'TimeCounter',
inputSettings: { text: `Remaining Time: ${remainingMinutes} minutes`, font: { face: "Arial", size: 24 } }
});
} catch (error) {
if (error.code === 601) {
console.log("Creating new TimeCounter overlay source.");
await obs.call('CreateInput', {
sceneName: 'Scene',
inputName: 'TimeCounter',
inputKind: 'text_gdiplus',
inputSettings: { text: `Remaining Time: ${remainingMinutes} minutes`, font: { face: "Arial", size: 24 } }
});
} else {
console.error('Error setting up TimeCounter overlay:', error);
}
}
const { sceneItemId: counterSceneItemId } = await obs.call('GetSceneItemId', {
sceneName: 'Scene',
sourceName: 'TimeCounter'
});
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: counterSceneItemId,
sceneItemTransform: {
positionX: 1302, // Position the counter on the screen
positionY: 958,
alignment: 5
}
});
// Start the counter update interval
updateCounter();
counterInterval = setInterval(decrementCounter, 60000); // Update every minute
}
// Update the counter display
async function updateCounter() {
try {
await obs.call('SetInputSettings', {
inputName: 'TimeCounter',
inputSettings: { text: `Remaining Time: ${remainingMinutes} minutes` }
});
console.log(`Counter updated to: ${remainingMinutes} minutes`);
} catch (error) {
console.error('Error updating TimeCounter overlay:', error);
}
}
// Decrement the counter by one minute
function decrementCounter() {
if (remainingMinutes > 0) {
remainingMinutes--;
updateCounter();
} else {
clearInterval(counterInterval); // Stop the interval if no time is remaining
}
}
// Calculate remaining time
async function calculateRemainingTime() {
remainingMinutes = 0;
for (const song of tracklist) {
const trackPath = `${songsFolderPath}/${song}.mp3`;
const duration = await new Promise((resolve) => {
ffmpeg.ffprobe(trackPath, (err, metadata) => {
resolve(!err && metadata ? Math.ceil(metadata.format.duration / 60) : 0);
});
});
remainingMinutes += duration;
}
updateCounter();
}
// Update and save overlay lines
async function updateOverlay(text) {
overlayLines.unshift(text);
if (overlayLines.length > maxOverlayLines) overlayLines.pop();
//saveOverlay();
try {
await obs.call('SetInputSettings', {
inputName: 'OverlayText',
inputSettings: { text: overlayLines.join('\n'), font: { face: "Arial", size: fontSize }, alignment: 1 },
overlay: true
});
const { sceneItemId } = await obs.call('GetSceneItemId', { sceneName: 'Scene', sourceName: 'OverlayText' });
await obs.call('SetSceneItemTransform', {
sceneName: 'Scene',
sceneItemId: sceneItemId,
sceneItemTransform: { positionX: 0, positionY: screenHeight - overlayHeight, alignment: 5 }
});
} catch (error) {
console.error('Error updating the overlay:', error);
}
}
// Play the song and save progress
async function playSong(songNumber) {
const trackPath = `${songsFolderPath}/${songNumber}.mp3`;
try {
ffmpeg.ffprobe(trackPath, (err, metadata) => {
if (err) {
console.error('Error getting song duration:', err);
return;
}
const durationMs = metadata.format.duration * 1000;
obs.call('SetInputSettings', { inputName: 'MusicChannel', inputSettings: { local_file: trackPath }, overlay: true });
console.log(`Playing song ${songNumber} with duration ${Math.round(durationMs / 1000)} seconds`);
isPlaying = true;
updateOverlay(`Now playing: song ${songNumber}`);
//saveOverlay();
setTimeout(() => {
isPlaying = false;
updateSongCounterOverlay();
}, durationMs);
});
} catch (error) {
console.error('Error changing song or getting duration:', error);
}
}
// Add song to tracklist and save it
async function addSongToTracklist(songNumber, sender) {
const alias = await fetchAccountAlias(sender);
tracklist.push(songNumber);
console.log(`Tracklist updated: ${tracklist.join(', ')}`);
updateOverlay(`${alias} added song ${songNumber}`);
saveTracklist();
updateSongCounterOverlay();
//calculateRemainingTime();
}
// Start listening and manage sequential playback
function startTracklistListener() {
setInterval(() => {
if (!isPlaying && tracklist.length > 0) {
const nextSong = tracklist.shift();
playSong(nextSong);
saveTracklist();
calculateRemainingTime();
}
}, 2000);
}
// Check and reconnect to Xahau every 30 seconds if disconnected
async function checkXahauConnection() {
setInterval(async () => {
if (!client || !client.isConnected()) {
console.log("Client disconnected from Xahau. Attempting to reconnect...");
await connectToXahau();
} else {
console.log("Client is connected to Xahau.");
}
}, 30000);
}
// Connect to Xahau and subscribe
async function connectToXahau(retries = 3) {
client = new xrpl.Client(process.env.XRPL_WSS);
try {
await client.connect();
console.log("Connected to Xahau WebSocket");
updateOverlay("Connected to Xahau");
await subscribeToAccount();
} catch (error) {
console.error(`Error connecting to Xahau, retries left: ${retries}`, error);
if (retries > 0) {
setTimeout(() => connectToXahau(retries - 1), 3000);
} else {
console.error("Could not connect to Xahau after multiple attempts.");
}
}
}
// Subscribe to account on Xahau
async function subscribeToAccount() {
try {
await client.request({ command: 'subscribe', accounts: [process.env.XRPL_ACCOUNT] });
console.log("Successfully subscribed to account:", process.env.XRPL_ACCOUNT);
client.on('transaction', async (tx) => {
if (tx.transaction?.TransactionType === 'Payment' && tx.transaction?.Destination === process.env.XRPL_ACCOUNT) {
handlePayment(tx.transaction);
}
});
} catch (error) {
console.error("Error subscribing to account:", error);
setTimeout(subscribeToAccount, 5000);
}
}
// Process incoming Payment
async function handlePayment(transaction) {
const { Account: sender, Amount, Memos } = transaction;
console.log("Transaction received:", transaction);
if (typeof Amount === 'string' && parseInt(Amount) >= 1000000) {
const memoHex = Memos?.[0]?.Memo?.MemoData;
const memoText = memoHex ? Buffer.from(memoHex, 'hex').toString('utf-8') : null;
const memoValue = memoText && !isNaN(memoText) ? parseInt(memoText) : null;
// Rule for the specific account
if (sender === 'rf1NrYAsv92UPDd8nyCG4A3bez7dhYE61r') {
if (memoValue && memoValue >= 1 && memoValue <= 100) {
console.log(`Adding ${memoValue} songs to the tracklist from specific account ${sender}`);
for (let i = 0; i < memoValue; i++) {
addSongToTracklist(Math.floor(Math.random() * 100) + 1, sender);
}
} else {
const amountValue = Math.floor(parseInt(Amount) / 1000000);
console.log(`Adding ${amountValue} songs based on Amount due to invalid or empty memo`);
for (let i = 0; i < amountValue; i++) {
addSongToTracklist(Math.floor(Math.random() * 100) + 1, sender);
}
}
} else {
// General rule for other accounts
if (parseInt(Amount) === 1000000 && memoValue && memoValue >= 1 && memoValue <= 100) {
addSongToTracklist(memoValue, sender);
} else {
const amountValue = Math.floor(parseInt(Amount) / 1000000);
const randomSongs = new Set();
while (randomSongs.size < amountValue && randomSongs.size < 100) {
const randomSong = Math.floor(Math.random() * 100) + 1;
randomSongs.add(randomSong);
}
randomSongs.forEach(async song => await addSongToTracklist(song, sender));
}
}
await calculateRemainingTime(); // Actualiza el tiempo después de añadir canciones
}
}
// Initialize connections and tracklist listener
(async () => {
loadPersistentData(); // Load saved tracklist and overlay
await connectToOBS();
await connectToXahau();
startTracklistListener();
checkXahauConnection();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment