Building TheRadio.Ai

Introduction

Welcome to TheRadio.Ai, a pioneering platform designed to transform how music enthusiasts create and manage their own radio stations. Our vision is to empower users to curate personalized stations using songs they’ve purchased through the application, managed by an AI DJ. This blog post will provide an in-depth look at our project, its features, and the journey towards our launch goals.

Project Overview

Vision and Goals

TheRadio.Ai aims to democratize radio station creation by allowing anyone to become a DJ. Users can create stations with their purchased songs, managed by an AI DJ that operates based on pre-engineered algorithms and user instructions. Our goal is to launch with at least five stations, each featuring over 100 songs, ensuring a diverse and rich listening experience.

Key Features

Personalized Radio Station Creation

Users can create and manage their own radio stations using songs they've purchased through the platform. Once a station has at least 100 songs, it can be shared publicly. Visitors can tune in and listen to the music being played in real-time.

From Humble Beginnings... Watch the very first moments of the creation of theRadio.Ai.

Follow this blog post to watch the application come to live before your very eyes! 

AI DJ Management

The AI DJ operates based on algorithms and user instructions, managing the station’s operations seamlessly. This includes song selection, ensuring variety, and maintaining a dynamic and engaging playlist.

Real-time Interaction and Voting

Listeners can interact with the station by voting for songs they want to hear next. The "Request Next" and "Buy" options allow users to influence the playlist dynamically.

Music Purchase Options

Songs can be purchased individually, by album, or within crates, which are collections of albums curated by artists or labels. This flexibility supports varied user preferences and enhances the music discovery experience.

Technical Implementation

Codebase Structure

The project's codebase is meticulously organized to ensure efficient development and maintenance. Here’s a look at the file structure:

.

├── ActivatingAndTestingServer.md

├── favicon.ico

├── Handling shout outs, push stream promotions.txt

├── package-lock.json

├── package.json

├── README.md

├── server.js

├── tempIndex.html

└── node_modules/

Front-end Development

Our front-end is built using HTML, CSS, and JavaScript, with Bootstrap for responsive design. Key components include:

index.html

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <meta name="description" content="Lake County Underground Radio - Discover and support underground artists from Lake County.">

    <title id="dynamic-title">Lake County Underground Radio</title>

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">

    <link rel="stylesheet" href="css/styles.css">

    <link rel="icon" href="favicon.ico" type="image/x-icon">

    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">

</head>

<body>

    <div class="container py-4 custom-container">

        <header class="header">

            <img src="TheRadioAiLogo.png" alt="Lake County Underground Radio Logo" class="logo">

            <div class="header-content">

                <h1>Lake County Underground Radio</h1>

            </div>

        </header>


        <div class="preset-button-container">

            <div class="preset-button" id="preset1">Preset 1</div>

            <div class="preset-button" id="preset2">Preset 2</div>

            <div class="preset-button" id="preset3">Preset 3</div>

            <div class="preset-button" id="preset4">Preset 4</div>

            <div class="preset-button" id="preset5">Preset 5</div>

        </div>


        <div class="media-player-wrapper d-flex justify-content-center">

            <div id="media-player-container">

                <video id="video-player" controls autoplay controlsList="nodownload">

                    Your browser does not support the video tag.

                </video>

            </div>

        </div>


        <div class="song-details d-flex align-items-center justify-content-center">

            <h3 id="current-song" class="mx-3">Loading...</h3>

        </div>

        <div class="button-row d-flex justify-content-center flex-wrap">

            <button class="btn-custom btn-primary me-3" id="open-chat">Live Chat</button>

            <button class="btn-custom btn-primary me-3">Save</button>

            <button class="btn-custom btn-success ms-3">Buy</button>

            <button class="btn-custom btn-primary ms-3" id="open-chart">Songs on Station</button>

        </div>


        <div id="chat-overlay" class="overlay">

            <div class="overlay-content">

                <button id="close-chat" class="close-btn">&times;</button>

                <h2>Live Chat</h2>

                <div id="chat-messages" class="chat-messages"></div>

                <input type="text" id="chat-input" placeholder="Type your message...">

                <button id="send-chat" class="btn-custom btn-primary">Send</button>

            </div>

        </div>


        <div id="chart-overlay" class="overlay">

            <div class="overlay-content">

                <button id="close-chart" class="close-btn">&times;</button>

                <h2>Songs on Station</h2>

                <div class="d-flex justify-content-between mb-3">

                    <div class="d-flex">

                        <div class="me-3">

                            <input type="text" id="search-input" class="form-control" placeholder="Search by name, genre, BPM, or artist">

                        </div>

                        <div class="dropdown">

                            <button class="btn btn-secondary dropdown-toggle" type="button" id="columnsDropdown" data-bs-toggle="dropdown" aria-expanded="false">

                                Columns

                            </button>

                            <ul class="dropdown-menu" aria-labelledby="columnsDropdown">

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="points" checked> Points</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="title" checked> Song Title</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="artist" checked> Artist Name</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="preview" checked> <i class="fas fa-play"></i> Preview</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="save" checked> <i class="fas fa-save"></i> Save</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="buy" checked> <i class="fas fa-shopping-cart"></i> Buy</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="request" checked> <i class="fas fa-vote-yea"></i> Request Next</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="label"> Label</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="location"> Location</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="bpm"> BPM</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="date"> Date</a></li>

                                <li><a class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="genre"> Genre</a></li>

                            </ul>

                        </div>

                    </div>

                </div>

                <div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">

                    <table class="table table-striped" id="songs-table">

                        <thead>

                            <tr>

                                <th data-column="points">Points <span class="sort-arrow"></span></th>

                                <th data-column="title">Song Title <span class="sort-arrow"></span></th>

                                <th data-column="artist">Artist Name <span class="sort-arrow"></span></th>

                                <th data-column="preview"><i class="fas fa-play"></i> Preview</th>

                                <th data-column="save"><i class="fas fa-save"></i> Save</th>

                                <th data-column="buy"><i class="fas fa-shopping-cart"></i> Buy</th>

                                <th data-column="request"><i class="fas fa-vote-yea"></i> Request Next</th>

                                <th data-column="label" class="d-none">Label</th>

                                <th data-column="location" class="d-none">Location</th>

                                <th data-column="bpm" class="d-none">BPM</th>

                                <th data-column="date" class="d-none">Date</th>

                                <th data-column="genre" class="d-none">Genre</th>

                            </tr>

                        </thead>

                        <tbody>

                            <!-- Dynamically filled rows will appear here -->

                        </tbody>

                    </table>

                    <div class="text-end mt-3">

                        <button id="see-all-btn" class="btn btn-secondary">... See All</button>

                    </div>

                </div>

            </div>

        </div>

    </div>


    <button id="back-to-top" class="back-to-top"><i class="fas fa-arrow-up"></i></button>


    <div class="bottom-nav">

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-play"></i>

            <span class="bottom-nav-text">Now Playing</span>

        </a>

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-music"></i>

            <span class="bottom-nav-text">Music</span>

        </a>

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-users"></i>

            <span class="bottom-nav-text">Artists</span>

        </a>

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-broadcast-tower"></i>

            <span class="bottom-nav-text">Stations</span>

        </a>

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-rss"></i>

            <span class="bottom-nav-text">Feed</span>

        </a>

        <a href="#" class="bottom-nav-link">

            <i class="fas fa-user"></i>

            <span class="bottom-nav-text">Me</span>

        </a>

    </div>


    <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/js/all.min.js"></script>

    <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>

    <script src="scripts/app.js"></script>

</body>

</html>

app.js 

document.addEventListener("DOMContentLoaded", () => {

    let mediaFiles = [];

    const mediaPlayer = document.getElementById('video-player');

    const currentSongDisplay = document.getElementById('current-song');

    const tableBody = document.querySelector('#songs-table tbody');

    const presetButtons = document.querySelectorAll('.preset-button');

    const dynamicTitle = document.getElementById('dynamic-title');

    const backToTopButton = document.getElementById('back-to-top');

    const searchInput = document.getElementById('search-input');

    const columnsDropdown = document.querySelectorAll('.column-toggle');

    const seeAllBtn = document.getElementById('see-all-btn');

    const tableHeaders = document.querySelectorAll('#songs-table th');

    let votedThisSession = false;

    let stationTitle = "Lake County Underground Radio";

    let lastPlayedSongs = [];

    const playPermissionPeriod = 5;

    let isShowingAll = false;

    let sortColumn = 'points';

    let sortDirection = 'desc';


    function fetchMediaFiles() {

        fetch('http://localhost:3000/media')

            .then(response => response.json())

            .then(data => {

                mediaFiles = data;

                console.log("Media files fetched:", mediaFiles);

                updateSongsTable();

                if (mediaFiles.length > 0) {

                    playNextSong();

                }

            })

            .catch(error => console.error('Error fetching media files:', error));

    }


    function updateSongsTable() {

        let filteredFiles = mediaFiles.filter(file => {

            const searchTerm = searchInput.value.toLowerCase();

            return (

                file.title.toLowerCase().includes(searchTerm) ||

                file.artist.toLowerCase().includes(searchTerm) ||

                file.genre.toLowerCase().includes(searchTerm) ||

                file.bpm.toString().includes(searchTerm)

            );

        });


        filteredFiles.sort((a, b) => {

            if (sortDirection === 'asc') {

                return a[sortColumn] > b[sortColumn] ? 1 : -1;

            } else {

                return a[sortColumn] < b[sortColumn] ? 1 : -1;

            }

        });


        if (!isShowingAll && filteredFiles.length > 40) {

            filteredFiles = filteredFiles.slice(0, 40);

            seeAllBtn.style.display = 'block';

        } else {

            seeAllBtn.style.display = 'none';

        }


        tableBody.innerHTML = '';

        filteredFiles.forEach((file, index) => {

            const row = tableBody.insertRow();

            fillRowWithData(row, file);

        });

    }


    function fillRowWithData(row, songInfo) {

        row.insertCell(0).textContent = songInfo.points || 100;

        row.insertCell(1).textContent = songInfo.title;

        row.insertCell(2).textContent = songInfo.artist;


        const previewButton = document.createElement('button');

        previewButton.className = 'btn-custom btn-secondary';

        previewButton.innerHTML = '<i class="fas fa-play"></i><span>Preview</span>';

        row.insertCell(3).appendChild(previewButton);


        const saveButton = document.createElement('button');

        saveButton.className = 'btn-custom btn-primary';

        saveButton.innerHTML = '<i class="fas fa-save"></i><span>Save</span>';

        saveButton.addEventListener('click', () => handleSave(songInfo));

        row.insertCell(4).appendChild(saveButton);


        const buyButton = document.createElement('button');

        buyButton.className = 'btn-custom btn-success';

        buyButton.innerHTML = '<i class="fas fa-shopping-cart"></i><span>Buy</span>';

        buyButton.addEventListener('click', () => handleBuy(songInfo));

        row.insertCell(5).appendChild(buyButton);


        const requestBtn = document.createElement('button');

        requestBtn.className = 'btn-custom btn-info';

        requestBtn.innerHTML = votedThisSession ? '<span>Voted</span>' : '<i class="fas fa-vote-yea"></i><span>Request Next</span>';

        requestBtn.disabled = votedThisSession;

        requestBtn.addEventListener('click', () => handleVote(songInfo, requestBtn));

        row.insertCell(6).appendChild(requestBtn);


        const labelCell = row.insertCell(7);

        labelCell.textContent = 'Independent';

        labelCell.classList.add('d-none');


        const locationCell = row.insertCell(8);

        locationCell.textContent = songInfo.location || 'Unknown';

        locationCell.classList.add('d-none');


        const bpmCell = row.insertCell(9);

        bpmCell.textContent = songInfo.bpm || 'N/A';

        bpmCell.classList.add('d-none');


        const dateCell = row.insertCell(10);

        dateCell.textContent = songInfo.date || 'Unknown';

        dateCell.classList.add('d-none');


        const genreCell = row.insertCell(11);

        genreCell.textContent = songInfo.genre || 'Unknown';

        genreCell.classList.add('d-none');

    }


    function handleSave(song) {

        console.log("Saving song:", song);

        fetch('http://localhost:3000/updatePoints', {

            method: 'POST',

            headers: { 'Content-Type': 'application/json' },

            body: JSON.stringify({ file: song.file, points: 10 })

        })

        .then(response => response.json())

        .then(data => {

            if (data.error) {

                console.error('Save error:', data.error);

                alert(data.error);

            } else {

                song.points = data.points;

                console.log("Updated song after save:", song);

                updateSongsTable();

            }

        })

        .catch(error => {

            console.error('Error saving:', error);

            alert('Failed to save. Try again.');

        });

    }


    function handleBuy(song) {

        console.log("Buying song:", song);

        fetch('http://localhost:3000/updatePoints', {

            method: 'POST',

            headers: { 'Content-Type': 'application/json' },

            body: JSON.stringify({ file: song.file, points: 100 })

        })

        .then(response => response.json())

        .then(data => {

            if (data.error) {

                console.error('Buy error:', data.error);

                alert(data.error);

            } else {

                song.points = data.points;

                console.log("Updated song after buy:", song);

                updateSongsTable();

            }

        })

        .catch(error => {

            console.error('Error buying:', error);

            alert('Failed to buy. Try again.');

        });

    }


    function handleVote(songInfo, button) {

        if (!votedThisSession) {

            console.log("Voting for song:", songInfo);

            fetch('http://localhost:3000/vote', {

                method: 'POST',

                headers: { 'Content-Type': 'application/json' },

                body: JSON.stringify({ file: songInfo.file, points: 25 })

            })

            .then(response => response.json())

            .then(data => {

                if (data.error) {

                    console.error('Vote error:', data.error);

                    alert(data.error);

                } else {

                    votedThisSession = true;

                    button.innerHTML = '<span>Voted</span>';

                    disableAllVoteButtons();

                    alert('Vote registered for ' + songInfo.title);

                    songInfo.points = data.points;

                    console.log("Updated song after vote:", songInfo);

                    updateSongsTable();

                }

            })

            .catch(error => {

                console.error('Error voting:', error);

                alert('Failed to register vote. Try again.');

            });

        }

    }


    function disableAllVoteButtons() {

        const voteButtons = document.querySelectorAll('#songs-table .btn-custom.btn-info');

        voteButtons.forEach(button => {

            button.disabled = true;

        });

    }


    function resetVoteButtons() {

        const voteButtons = document.querySelectorAll('#songs-table .btn-custom.btn-info');

        voteButtons.forEach(button => {

            button.disabled = false;

            button.innerHTML = '<i class="fas fa-vote-yea"></i><span>Request Next</span>';

        });

        votedThisSession = false;

    }


    function weightedRandomSelection() {

        let totalPoints = mediaFiles.reduce((sum, song) => sum + song.points, 0);

        let randomPoint = Math.random() * totalPoints;


        let selectedMedia;

        for (let song of mediaFiles) {

            if (randomPoint < song.points && !lastPlayedSongs.includes(mediaFiles.indexOf(song))) {

                selectedMedia = song;

                break;

            }

            randomPoint -= song.points;

        }


        if (!selectedMedia) {

            lastPlayedSongs.shift();

            selectedMedia = mediaFiles.find(song => !lastPlayedSongs.includes(mediaFiles.indexOf(song)));

        }


        return selectedMedia;

    }


    function playNextSong() {

        let selectedMedia = weightedRandomSelection();


        // Add 50 points to the song for being played

        fetch('http://localhost:3000/updatePoints', {

            method: 'POST',

            headers: { 'Content-Type': 'application/json' },

            body: JSON.stringify({ file: selectedMedia.file, points: 50 })

        })

        .then(response => response.json())

        .then(data => {

            if (data.error) {

                console.error('Error updating points for play:', data.error);

            } else {

                selectedMedia.points = data.points;

                console.log("Updated song after play:", selectedMedia);

                updateSongsTable();

            }

        })

        .catch(error => {

            console.error('Error updating points for play:', error);

        });


        if (lastPlayedSongs.length >= playPermissionPeriod) {

            lastPlayedSongs.shift();

        }

        lastPlayedSongs.push(mediaFiles.indexOf(selectedMedia));


        mediaPlayer.src = `http://localhost:3000/media/${decodeURIComponent(selectedMedia.file)}`;

        mediaPlayer.load();

        mediaPlayer.play();


        currentSongDisplay.textContent = `${selectedMedia.artist} - ${selectedMedia.title}`;

        updateTabTitle(selectedMedia);

        votedThisSession = false;  // Reset the vote session

        resetVoteButtons();  // Reset vote buttons state


        updateActivePresetButtons(selectedMedia);

    }


    function updateTabTitle(songInfo) {

        dynamicTitle.textContent = `Lake County Underground Radio: ${songInfo.artist} - ${songInfo.title}`;

    }


    function handlePresetClick(event) {

        const presetButton = event.currentTarget;

        const currentSong = currentSongDisplay.textContent;

        presetButtons.forEach(button => {

            button.classList.remove('active');

            button.textContent = button.id.replace('preset', 'Preset ');

        });

        presetButton.classList.add('active');

        presetButton.innerHTML = `<span class="scrolling-text">${currentSong}</span>`;

        updateTabTitle({ title: currentSong.split(' - ')[1], artist: currentSong.split(' - ')[0] });

    }


    function updateActivePresetButtons(songInfo) {

        presetButtons.forEach(button => {

            if (button.classList.contains('active')) {

                button.innerHTML = `<span class="scrolling-text">${stationTitle}: ${songInfo.title} - ${songInfo.artist}</span>`;

            }

        });

    }


    function handleColumnToggle(event) {

        const columnClass = event.target.dataset.column;

        const columnCells = document.querySelectorAll(`[data-column="${columnClass}"], .${columnClass}-cell`);

        columnCells.forEach(cell => {

            cell.classList.toggle('d-none', !event.target.checked);

        });

    }


    function handleSort(event) {

        const column = event.currentTarget.dataset.column;

        if (sortColumn === column) {

            sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';

        } else {

            sortColumn = column;

            sortDirection = 'asc';

        }

        updateSortArrows();

        updateSongsTable();

    }


    function updateSortArrows() {

        tableHeaders.forEach(header => {

            const column = header.dataset.column;

            const arrow = header.querySelector('.sort-arrow');

            if (column === sortColumn) {

                header.classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');

                arrow.style.display = 'inline-block';

            } else {

                header.classList.remove('sort-asc', 'sort-desc');

                arrow.style.display = 'none';

            }

        });

    }


    presetButtons.forEach(button => {

        button.addEventListener('click', handlePresetClick);

    });


    mediaPlayer.onended = playNextSong;


    window.addEventListener('scroll', () => {

        if (window.pageYOffset > 100) {

            backToTopButton.style.display = 'flex';

        } else {

            backToTopButton.style.display = 'none';

        }

    });


    backToTopButton.addEventListener('click', () => {

        window.scrollTo({ top: 0, behavior: 'smooth' });

    });


    searchInput.addEventListener('input', updateSongsTable);


    columnsDropdown.forEach(toggle => {

        toggle.addEventListener('change', handleColumnToggle);

    });


    tableHeaders.forEach(header => {

        header.addEventListener('click', handleSort);

    });


    seeAllBtn.addEventListener('click', () => {

        isShowingAll = !isShowingAll;

        updateSongsTable();

    });


    fetchMediaFiles();


    const chatOverlay = document.getElementById('chat-overlay');

    const chartOverlay = document.getElementById('chart-overlay');

    const openChatBtn = document.getElementById('open-chat');

    const openChartBtn = document.getElementById('open-chart');

    const closeChatBtn = document.getElementById('close-chat');

    const closeChartBtn = document.getElementById('close-chart');


    openChatBtn.addEventListener('click', () => {

        chatOverlay.style.display = 'flex';

    });


    closeChatBtn.addEventListener('click', () => {

        chatOverlay.style.display = 'none';

    });


    openChartBtn.addEventListener('click', () => {

        chartOverlay.style.display = 'flex';

    });


    closeChartBtn.addEventListener('click', () => {

        chartOverlay.style.display = 'none';

    });

});

Back-end Development

The back-end is powered by Node.js and Express.js, ensuring robust server-side functionality. Key components include:

server.js

const express = require('express');

const session = require('express-session');

const bodyParser = require('body-parser');

const path = require('path');

const fs = require('fs');

const cors = require('cors');

const WebSocket = require('ws');


const app = express();

const PORT = 3000;

const wss = new WebSocket.Server({ port: 8080 });


app.use(cors());

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));

app.use(express.static('public'));


app.use('/media', express.static(path.join(__dirname, 'src', 'media')));


app.get('/favicon.ico', (req, res) => res.status(204));


app.use(session({

    secret: 'your-unique-secret-key',

    saveUninitialized: true,

    resave: false,

    cookie: { secure: false, httpOnly: true }

}));


let songsData = [];

const mediaDir = path.join(__dirname, 'src', 'media');


async function loadSongsData() {

    try {

        const files = await fs.promises.readdir(mediaDir);

        songsData = files.map(file => {

            const parts = file.split(' - ');

            const artist = parts.shift();

            const title = parts.join(' - ').replace(/\.(mp3|mp4)$/, '');

            return { 

                file: encodeURIComponent(file), 

                artist, 

                title, 

                votes: 0, 

                points: 100, 

                bonusPoints: 0,

                lastUpdated: Date.now(),

                addedAt: Date.now()  

            };

        });

        console.log('Song data loaded:', songsData);

    } catch (error) {

        console.error('Error loading song data:', error);

    }

}


app.post('/vote', (req, res) => {

    const { file, points } = req.body;

    console.log(`Vote request received for file: ${file} with points: ${points}`);

    const song = songsData.find(song => song.file === file);


    if (!song) {

        return res.status(404).json({ error: 'Song not found' });

    }


    if (req.session.voted) {

        return res.status(409).json({ error: 'Already voted in this session' });

    }


    song.votes++;

    song.points += points;  

    song.lastUpdated = Date.now();  

    req.session.voted = true;


    req.session.save(err => {

        if (err) {

            console.error('Error saving session:', err);

            return res.status(500).json({ error: 'Failed to save session' });

        }

        console.log(`Vote registered for ${song.title}. Points: ${song.points}`);

        res.status(200).json({ points: song.points });

    });

});


app.post('/updatePoints', (req, res) => {

    const { file, points } = req.body;

    console.log(`Update points request received for file: ${file} with points: ${points}`);

    const song = songsData.find(song => song.file === file);


    if (!song) {

        return res.status(404).json({ error: 'Song not found' });

    }


    song.points += points;

    song.lastUpdated = Date.now();  


    req.session.save(err => {

        if (err) {

            console.error('Error saving session:', err);

            return res.status(500).json({ error: 'Failed to save session' });

        }

        console.log(`Points updated for ${song.title}. Points: ${song.points}`);

        res.status(200).json({ points: song.points });

    });

});


app.post('/reset', (req, res) => {

    songsData.forEach(song => {

        song.votes = 0;

        song.points = 100;

        song.bonusPoints = 0;

        song.lastUpdated = Date.now();  

    });

    req.session.voted = false;


    req.session.save(err => {

        if (err) {

            return res.status(500).send('Failed to reset session');

        }

        console.log('Session and song points reset.');

        res.send('Session and votes reset');

    });

});


app.get('/media', (req, res) => {

    res.status(200).json(songsData.map(song => ({

        file: song.file,

        title: song.title,

        artist: song.artist,

        points: song.points,

        bonusPoints: song.bonusPoints,

        location: 'Unknown',

        bpm: 120,

        date: '2024-01-01',

        genre: 'Unknown'

    })));

});


function deprecatePoints() {

    const now = Date.now();

    songsData.forEach(song => {

        const hoursSinceAdded = (now - song.addedAt) / (1000 * 60 * 60);

        const daysSinceAdded = Math.floor(hoursSinceAdded / 24);

        if (daysSinceAdded > 0) {

            const depreciationFactor = Math.pow(0.99, daysSinceAdded);

            song.points *= depreciationFactor;

            song.points = Math.max(Math.floor(song.points), 0);  

            song.addedAt += daysSinceAdded * 24 * 60 * 60 * 1000;  

            console.log(`Depreciated points for ${song.title}. New points: ${song.points}`);

        }

    });

}


setInterval(deprecatePoints, 24 * 60 * 60 * 1000);


app.listen(PORT, () => {

    console.log(`Server running on port ${PORT}`);

    loadSongsData();

});


wss.on('connection', ws => {

    console.log('Client connected');

    ws.on('message', message => {

        console.log(`Received: ${message}`);

        const chatMessage = JSON.parse(message);

        wss.clients.forEach(client => {

            if (client.readyState === WebSocket.OPEN) {

                client.send(JSON.stringify(chatMessage));

            }

        });

    });

    ws.on('close', () => {

        console.log('Client disconnected');

    });

});

Standard Operating Procedures

Spotlight Video Creation

To maintain consistency and professionalism, we follow a standardized script for introducing new songs:

Hello, everyone! Thanks for tuning in to The Radio dot a-eye. Today, we're excited to spotlight a brand-new track called "<Song Title>" by <Artist Name> from <Label/Production>. This exclusive preview showcases the incredible work being done at The Radio dot a-eye.

Livestream Event Setup

Livestream titles and descriptions are crafted to be engaging and informative, ensuring viewers know what to expect and how to join:

Title: TheRadio.Ai: Exclusive Spotlight on "<Song Title>" by <Artist Name>!

Description: Join us LIVE for an exclusive spotlight on "<Song Title>" by <Artist Name> from <Label/Production>! Tune in LIVE (start from the beginning): <Live Link>

Social Media Engagement

We create concise and engaging blurbs for social media platforms to attract viewers to our livestreams:

Enhancements and Iterative Improvements

We continually enhance the platform based on feedback and iterative improvements. Key enhancements include:

Moving to the Cloud

To enhance scalability, reliability, and performance, TheRadio.Ai will transition to a cloud infrastructure utilizing AWS services. Here’s a high-level plan for this migration:

Infrastructure as Code (IaC)

We'll use Terraform to define and manage our cloud infrastructure. This includes provisioning EC2 instances, setting up S3 for storage, and configuring security groups.

Containerization with Docker and Kubernetes

Docker will be used to containerize our application, ensuring consistency across different environments. Kubernetes (EKS) will manage container orchestration, providing auto-scaling and load balancing.

AWS Services Integration

Monitoring and Logging

Using CloudWatch for monitoring application performance and logging. Integration with Splunk and Grafana for advanced visualization and alerting.

CI/CD Pipeline

Implementing a continuous integration and continuous deployment pipeline using AWS CodePipeline and CodeBuild to automate the build, test, and deployment process.

Conclusion

TheRadio.Ai is set to revolutionize the way we experience and manage radio stations. With a blend of user creativity and AI management, our platform offers a unique, interactive, and engaging experience for both station creators and listeners. Stay tuned as we continue to develop and refine our platform, bringing innovative music experiences to life.

Implementing TheRadio.Ai: Detailed Technical Guide for Cloud Infrastructure and AI Integration

Introduction

In this blog post, we will dive into the detailed technical aspects of implementing TheRadio.Ai's infrastructure. We'll cover the integration of various AWS services, containerization, AI management, real-time interaction, and the CI/CD pipeline. This guide will serve as a comprehensive reference for our development team and as an insightful read for anyone interested in the technical journey behind TheRadio.Ai.

Project Phase Overview

Goal: To build a robust, scalable, and efficient infrastructure for TheRadio.Ai, leveraging cloud technologies, containerization, AI, and real-time interaction capabilities.

Technology Stack and Skills Required

Detailed Implementation Plan

Infrastructure as Code (IaC) with Terraform

Step 1: Define and Manage Cloud Infrastructure

Terraform Example:

provider "aws" {

  region = "us-east-1"

}


resource "aws_vpc" "main" {

  cidr_block = "10.0.0.0/16"

}


resource "aws_subnet" "subnet1" {

  vpc_id     = aws_vpc.main.id

  cidr_block = "10.0.1.0/24"

}


resource "aws_security_group" "allow_web" {

  vpc_id = aws_vpc.main.id


  ingress {

    from_port   = 80

    to_port     = 80

    protocol    = "tcp"

    cidr_blocks = ["0.0.0.0/0"]

  }


  egress {

    from_port   = 0

    to_port     = 0

    protocol    = "-1"

    cidr_blocks = ["0.0.0.0/0"]

  }

}

Containerization with Docker and Kubernetes (EKS)

Step 2: Dockerize the Application

Dockerfile Example for Node.js Backend:

# Use an official Node.js runtime as a parent image

FROM node:14


# Set the working directory

WORKDIR /usr/src/app


# Copy the package.json and package-lock.json files

COPY package*.json ./


# Install dependencies

RUN npm install


# Copy the rest of the application code

COPY . .


# Expose the port the app runs on

EXPOSE 3000


# Run the application

CMD ["node", "server.js"]

Step 3: Deploy Containers with Kubernetes (EKS)

Kubernetes Deployment Example:

apiVersion: apps/v1

kind: Deployment

metadata:

  name: theradio-backend

spec:

  replicas: 3

  selector:

    matchLabels:

      app: theradio-backend

  template:

    metadata:

      labels:

        app: theradio-backend

    spec:

      containers:

      - name: theradio-backend

        image: <AWS_ECR_IMAGE_URL>

        ports:

        - containerPort: 3000

AI DJ Management and Real-time Interaction

Step 4: Implement AI DJ Management

AI DJ Example (Python):

import random


class AIDJ:

    def __init__(self, song_data):

        self.song_data = song_data


    def select_song(self):

        # Example algorithm for song selection

        total_points = sum(song['points'] for song in self.song_data)

        random_point = random.uniform(0, total_points)

        current_point = 0

        for song in self.song_data:

            current_point += song['points']

            if current_point >= random_point:

                return song

Step 5: Real-time Interaction with WebSockets

WebSocket Server Example (Node.js):

const WebSocket = require('ws');


const wss = new WebSocket.Server({ port: 8080 });


wss.on('connection', ws => {

  console.log('Client connected');

  

  ws.on('message', message => {

    console.log(`Received: ${message}`);

    const chatMessage = JSON.parse(message);

    wss.clients.forEach(client => {

      if (client.readyState === WebSocket.OPEN) {

        client.send(JSON.stringify(chatMessage));

      }

    });

  });


  ws.on('close', () => {

    console.log('Client disconnected');

  });

});

Logging, Monitoring, and Alerting

Step 6: Implement Logging and Monitoring

CloudWatch Configuration Example:

logs:

  - name: "/aws/containerinsights/<cluster-name>/application"

    retention: 7

  - name: "/aws/containerinsights/<cluster-name>/performance"

    retention: 7

CI/CD Pipeline

Step 7: Set Up CI/CD Pipeline

AWS CodePipeline Example:

Resources:

  MyPipeline:

    Type: "AWS::CodePipeline::Pipeline"

    Properties:

      RoleArn: !GetAtt MyPipelineRole.Arn

      ArtifactStore:

        Type: S3

        Location: !Ref ArtifactBucket

      Stages:

        - Name: Source

          Actions:

            - Name: Source

              ActionTypeId:

                Category: Source

                Owner: AWS

                Provider: CodeCommit

                Version: 1

              OutputArtifacts:

                - Name: SourceArtifact

              Configuration:

                RepositoryName: !Ref RepositoryName

                BranchName: master

        - Name: Build

          Actions:

            - Name: Build

              ActionTypeId:

                Category: Build

                Owner: AWS

                Provider: CodeBuild

                Version: 1

              InputArtifacts:

                - Name: SourceArtifact

              OutputArtifacts:

                - Name: BuildArtifact

              Configuration:

                ProjectName: !Ref CodeBuildProject

        - Name: Deploy

          Actions:

            - Name: Deploy

              ActionTypeId:

                Category: Deploy

                Owner: AWS

                Provider: ECS

                Version: 1

              InputArtifacts:

                - Name: BuildArtifact

              Configuration:

                ClusterName: !Ref EcsCluster

                ServiceName: !Ref EcsService

Additional Cloud Knowledge

Step 8: Leverage Multi-Cloud Expertise

Networking and Troubleshooting

Step 9: Enhance Networking Skills

Network Troubleshooting Example:

# Check connectivity to a remote server

ping example.com


# Trace the route to a remote server

traceroute example.com


# Analyze VPC Flow Logs for network traffic

aws ec2 describe-flow-logs --filter Name=resource-id,Values=<vpc-id>

Conclusion

This detailed technical guide outlines the comprehensive steps to implement TheRadio.Ai's infrastructure. By leveraging cloud technologies, containerization, AI management, real-time interaction, and a robust CI/CD pipeline, we aim to create a scalable, reliable, and engaging platform for personalized radio stations. This guide will serve as a valuable reference for our development team and an insightful resource for anyone following our journey.

Stay tuned for more updates as we continue to develop and refine TheRadio.Ai, bringing innovative music experiences to life.

Summary of Current Progress on TheRadio.Ai Project

Key Points and Objectives

Initial Goals:

Interview Preparation:

Project Overview:

Detailed Technical Guide Blog Posts

First Blog Post:

Second Blog Post:

Learning Path for Proficiency

Step-by-Step Educational Path:

Next Steps:

Communication Structures


Condensed Roadmap for TheRadio.Ai (1 Month Completion)

Week 1

Week 2

Week 3

Week 4

Instructions for Claude AI:


Instructions for Claude AI:

Please create a Gantt chart for the development of TheRadio.Ai project, with a condensed timeline to complete the entire project in 1 month. Below is the detailed roadmap and timeline:

Week 1

Week 2

Week 3

Week 4

Timeline Overview