Open In App

Music Streaming Platform using MERN Stack

Last Updated : 22 Mar, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In this tutorial, we’ll walk through the process of creating a music streaming platform using the MERN stack. This project will allow users to browse playlists, view songs within each playlist, search for specific songs, and play them. We’ll cover the basics of setting up a MongoDB database, creating RESTful APIs with Node.js and Express, and building a frontend interface using React.

Output Preview: Let us have a look at how the final output will look like.

Screenshot-2024-03-16-223452

Prerequisites:

Approach to create Music Streaming Platform:

  • Create a MongoDB database to store playlists and song data.
  • Implement RESTful APIs using Express.js to handle CRUD operations for playlists and songs.
  • Develop a React frontend to interact with the backend APIs, allowing users to browse playlists, view songs, and search for specific songs.
  • Use Axios to make asynchronous requests from the frontend to the backend.

Steps to Create the Project:

Step 1: Create a Node.js backend with Express.js to handle API requests and interact with the database.

mkdir music-streaming-backend
cd music-streaming-backend
npm init -y
  • Create a folder named “songs” and add songs to it.

Step 2: Make server.js to create routes and models:-

Step 3: Build a React frontend :-

npx create-react-app music-streaming-frontend
cd music-streaming-frontend

Step 4: Create interface for browsing playlists, viewing songs, and searching for songs. (PlayList.js, Search.js):-

Step 5: Add style.css to add interaction and formatting:

Frontend Project Structure:

Screenshot-2024-03-07-125240

Backend Project Structure:Screenshot-2024-03-07-125215

Frontend: The updated dependencies in package.json file will look like:

"dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.6.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.22.2",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
}

Backend: The updated dependencies in package.json file will look like:

"dependencies": {
    "body-parser": "^1.20.2",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.18.3",
    "mongoose": "^8.2.1"
}

Example: Code example for the backend:

JavaScript
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
require('dotenv').config();

const app = express();

// Middleware
app.use(cors());
app.use(bodyParser.json());

// MongoDB Connection
mongoose.connect('mongodb://localhost:27017/musicstreamingdatabase', {
    useNewUrlParser: true,
    useUnifiedTopology: true
})
    .then(() => console.log('Connected to MongoDB'))
    .catch(err => console.error('Failed to connect to MongoDB', err));

// Playlist Model
// Playlist Model
const Playlist =
    mongoose.model('Playlist', new mongoose.Schema({
        name: { type: String, required: true },
        playlistCode:
        {
            type: String,
            required: true,
            unique: true
        }, // New field
        songs:
            [{ type: String }],
        // Store song titles directly
        user:
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User'
        },
        collaborators:
            [
                {
                    type: mongoose.Schema.Types.ObjectId,
                    ref: 'User'
                }]
    }));



// Song Model
// Song Model
const Song = mongoose.model('Song', new mongoose.Schema({
    title: { type: String, required: true },
    artist: { type: String, required: true },
    songcode: { type: String, required: true, unique: true },
    album: String,
    duration: { type: Number, required: true },
    audioUrl: { type: String, required: true }
    // Add audioUrl field for storing song file URL
}));


// Routes
// Playlists
app.get('/api/playlists', async (req, res) => {
    try {
        const playlists =
            await Playlist.find().populate('songs');
        res.json(playlists);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Server error' });
    }
});

app.get('/api/playlists/:playlistName/songs', async (req, res) => {
    try {
        const playlistName =
            req.params.playlistName;
        const playlist =
            await Playlist.findOne({ name: playlistName }).populate('songs');
        if (!playlist) {
            return res.status(404).json({ error: 'Playlist not found' });
        }
        res.json(playlist.songs);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Server error' });
    }
});




app.post('/api/playlists', async (req, res) => {
    try {
        const { name, songs, user } = req.body;
        const playlist =
            new Playlist({ name, songs, user });
        await playlist.save();
        res.json(playlist);
    } catch (err) {
        console.error(err);
        res.status(500)
            .json({ error: 'Server error' });
    }
});

// Collaborative Playlists
app.post(
    '/api/playlists/:playlistId/collaborators', async (req, res) => {
        try {
            const { userId } = req.body;
            const playlist = await Playlist.findByIdAndUpdate(
                req.params.playlistId,
                { $addToSet: { collaborators: userId } },
                { new: true }
            );
            res.json(playlist);
        } catch (err) {
            console.error(err);
            res.status(500).json({ error: 'Server error' });
        }
    });

app.get('/api/playlists/collaborative/:userId', async (req, res) => {
    try {
        const playlists =
            await Playlist.find(
                {
                    collaborators: req.params.userId
                });
        res.json(playlists);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Server error' });
    }
});

// Songs
app.get('/api/songs', async (req, res) => {
    try {
        const { title, artist, album } = req.query;
        const filter = {};
        if (title) filter.title = new RegExp(title, 'i');
        if (artist) filter.artist = new RegExp(artist, 'i');
        if (album) filter.album = new RegExp(album, 'i');

        const songs =
            await Song.find(filter)
                .select('title artist album duration audioUrl');
        // Include audioUrl field
        res.json(songs);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Server error' });
    }
});


app.post('/api/songs', async (req, res) => {
    try {
        const
            {
                title,
                artist,
                album,
                duration
            } = req.body;
        const song =
            new Song({ title, artist, album, duration });
        await song.save();
        res.json(song);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Server error' });
    }
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500)
        .send('Something went wrong!');
});

// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT,
    () =>
        console.log(`Server running on port ${PORT}`)
);

Example: Code example for the frontend:

CSS
.playlist-container {
    background-color: #eeeeee;
    padding: 20px;
}

.playlist-heading {
    color: #4ecca3;
}

.all-songs {
    display: flex;
    list-style-type: none;
    flex-direction: column;
}

.all-song {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #ffffff;
    border-radius: 5px;
}

.playlist-list {
    list-style-type: none;
    padding: 0;
}

.playlist-item {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #ffffff;
    border-radius: 5px;
}

.search-container {
    background-color: #eeeeee;
    padding: 20px;
}

.search-heading {
    color: #4ecca3;
}

.search-input {
    margin-right: 10px;
    padding: 5px;
}

.search-button {
    padding: 5px 10px;
    background-color: #4ecca3;
    border: none;
    color: #ffffff;
    cursor: pointer;
}

.search-results {
    list-style-type: none;
    padding: 0;
}

.search-item {
    margin-bottom: 10px;
    padding: 10px;
    background-color: #ffffff;
    border-radius: 5px;
}
JavaScript
import React from 'react';
import {
    BrowserRouter as Router,
    Route, Routes, Link
} from 'react-router-dom';
import Playlist from './Playlist';
import Search from './Search';

function App() {
    return (
        <>
            <Playlist />
            <Search />
        </>
    );
}

export default App;
JavaScript
import React,
{
    useEffect,
    useState
} from 'react';
import axios from 'axios';
import './App.css';

const Playlist = () => {
    const [playlists, setPlaylists] = useState([]);
    const [selectedPlaylist, setSelectedPlaylist] = useState(null);
    const [playlistSongs, setPlaylistSongs] = useState([]);


    useEffect(() => {
        axios
            .get("http://localhost:5000/api/playlists")
            .then((res) => {
                setPlaylists(res.data);
            })
            .catch((err) => {
                console.error("Error fetching playlists:", err);
            });
    }, []);

    const fetchSongDetails = async (playlistSongs) => {
        try {
            const response =
                await axios.get("http://localhost:5000/api/songs");
            const allSongs = response.data;
            const matchingSongs =
                playlistSongs.map((playlistSong) => {
                    return allSongs.find(
                        (song) =>
                            song.title === playlistSong);
                });
            return matchingSongs.filter(Boolean); 
            // Filter out undefined/null values
        } catch (error) {
            console.error("Error fetching song details:", error);
            return [];
        }
    };

    const handlePlaylistClick = async (playlistName) => {
        try {
            const response = await axios.get(
                `http://localhost:5000/api/playlists/${encodeURIComponent(
                    playlistName
                )}/songs`
            );
            const songs = response.data;
            const matchingSongs =
                await fetchSongDetails(songs);
            setPlaylistSongs(matchingSongs);
            setSelectedPlaylist(playlistName);
        } catch (error) {
            console.error("Error fetching playlist songs:", error);
        }
    };




    return (
        <div className="playlist-container">
            <h1 className="playlist-heading">Playlists</h1>
            <ul className="playlist-list">
                {
                    playlists.map((playlist, index) => (
                        <li
                            key={index}
                            className="playlist-item"
                            onClick={
                                () =>
                                    handlePlaylistClick(playlist.name)
                            }>
                            {playlist.name}
                        </li>
                    ))
                }
            </ul>
            {selectedPlaylist && (
                <div className="playlist-songs">
                    <h3>Songs in {selectedPlaylist}:</h3>
                    <ul>
                        {
                            playlistSongs.map((song, index) => (
                                <li key={index}>
                                    {song.title} - {song.artist}
                                </li>
                            ))
                        }
                    </ul>
                </div>
            )}
        </div>
    );
};

export default Playlist;
JavaScript
import React,
{
    useState,
    useEffect
} from 'react';
import axios from 'axios';
import './App.css';

const Search = () => {
    const [searchTerm, setSearchTerm] = useState('');
    const [searchResults, setSearchResults] = useState([]);
    const [allSongs, setAllSongs] = useState([]);
    const [audioSrc, setAudioSrc] = useState('');
    const [audioTitle, setAudioTitle] = useState('');
    const [isPlaying, setIsPlaying] = useState(false);

    useEffect(() => {
        const fetchAllSongs = async () => {
            try {
                const response =
                    await axios.get("http://localhost:5000/api/songs");
                setAllSongs(response.data);
            } catch (error) {
                console.error("Error fetching all songs:", error);
            }
        };
        fetchAllSongs();
    }, []);

    const handleSearch = () => {
        axios.get(`http://localhost:5000/api/songs?title=${searchTerm}`)
            .then(res => {
                setSearchResults(res.data);
            })
            .catch(err => {
                console.error('Error searching for songs:', err);
            });
    };

    const handleSongClick = (song) => () => {
        setAudioSrc(song.audioUrl);
        setAudioTitle(song.title);
        setIsPlaying(true);
    };

    const handleAudioEnded = () => {
        setIsPlaying(false);
    };

    const handlePauseClick = () => {
        setIsPlaying(false);
    };

    return (
        <div className="search-container">
            <h1 className="search-heading">Search</h1>
            <input
                type="text"
                className="search-input"
                value={searchTerm}
                onChange={
                    e =>
                        setSearchTerm(e.target.value)
                } />
            <button className="search-button"
                onClick={handleSearch}>
                Search
            </button>
            <ul className="search-results">
                {searchResults.map(song => (
                    <li key={song._id} className="search-item">
                        {song.title} - {song.artist}
                    </li>
                ))}
            </ul>
            <h1>All Songs</h1>
            {audioSrc && (
                <div>
                    <h2>Now Playing: {audioTitle}</h2>
                    <audio src={audioSrc}
                        autoPlay={isPlaying}
                        onEnded={handleAudioEnded} />
                    {
                        isPlaying ?
                            (<button onClick={handlePauseClick}
                                className='search-button'>Pause</button>) :
                            (<button onClick={
                                () => setIsPlaying(true)
                            }
                                className='search-button'>Play</button>)
                    }
                </div>
            )}
            <div className="all-songs">
                {allSongs.map(song => (
                    <div key={song._id}
                        className="all-song"
                        onClick={handleSongClick(song)}>
                        {song.title} - {song.artist}
                    </div>
                ))}
            </div>
        </div>
    );
};

export default Search;

Steps to Run the Application:

Frontend:

npm start

Backend:

npm run dev

Output:

5689-ezgifcom-video-to-gif-converter



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads