Open In App

Music Playlist App using MERN Stack

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

This tutorial will create a Music Playlist App using the MERN (MongoDB, Express.js, React.js, Node.js) stack. The app allows users to manage playlists, add songs, and play their favorite tracks. We’ll cover front and backend development, integrating features like song uploading, playlist creation, and playback functionality.

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

imresizer-1710245089584

Prerequisites

Approach to create Music Playlist App:

  • Create a Node.js project with Express.js.
  • Connect to MongoDB using mongoose.
  • Implement RESTful API endpoints for managing playlists and songs.
  • Initialize a React.js project.
  • Design components to display playlists and songs.
  • Use Axios to fetch data from the backend and handle user interactions.
  • Implement file upload functionality using Multer and GridFS.
  • Apply CSS to style the app and make it visually appealing.

Steps to Create the BackEnd:

Step 1: Set Up Backend Server using following commands:-

mkdir music-playlist-app-backend
cd music-playlist-app-backend
npm init -y

Step 2: Install all the necessary dependencies:-

npm install express mongoose cors multer multer-gridfs-storage

Project Structure(Backend):

Screenshot-2024-03-12-173020

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

"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"gridfs-stream": "^1.1.1",
"mongoose": "^8.2.1",
"multer": "^1.4.5-lts.1",
"multer-gridfs-storage": "^5.0.2",
"shortid": "^2.2.16",
"uuid": "^9.0.1"
}

Example: Create `server.js` and write the below code.

Javascript
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
const multer = require('multer');
const { GridFsStorage } = require('multer-gridfs-storage');
const { GridFSBucket, ObjectId } = require('mongodb');
const shortid = require('shortid');

require('dotenv').config();

const app = express();

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

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

// Initialize GridFS
let gfs;

const conn = mongoose.connection;
conn.once('open', () => {
    gfs = new GridFSBucket(conn.db, {
        bucketName: 'uploads' // Specify your bucket name here
    });
});

// Create storage engine using GridFS
const storage = new GridFsStorage({
    url: 'mongodb://localhost:27017/musicdatabase',
    file: (req, file) => {
        return {
            filename: file.originalname,
            bucketName: 'uploads' // Bucket name in MongoDB
        };
    }
});

// Set up multer to handle file uploads
const upload = multer({ storage });

// Route to handle file uploads
app.post('/api/upload', upload.single('audioFile'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: 'No file uploaded' });
    }
    console.log('Uploaded file:', req.file);
    res.json({ fileId: req.file.id });
});

// Playlist Model
const PlaylistSchema = new mongoose.Schema({
    name: { type: String, required: true },
    songs: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Song' }],
    playlistCode: {
        type: String,
        required: true,
        unique: true,
        default: () => shortid.generate()
    }
});

const Playlist = mongoose.model('Playlist', PlaylistSchema);

// 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 },
    fileId: { type: mongoose.Schema
                            .Types.ObjectId, ref: 'uploads.files' }
    // Reference to GridFS file
}));

// 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/songs/:songId/audio', async (req, res) => {
    try {
        const songId = req.params.songId;

        // Ensure the songId is a valid ObjectId
        if (!ObjectId.isValid(songId)) {
            return res.status(404).json({ error: 'Invalid song ID' });
        }

        // Find the song in MongoDB
        const song = await Song.findById(songId);
        if (!song) {
            return res.status(404).json({ error: 'Song not found' });
        }

        // Set the appropriate Content-Type header
        res.set('Content-Type', 'audio/wav');
        // Modify the Content-Type as per your file format

        // Stream the audio file from GridFS
        const downloadStream = gfs.openDownloadStream(song.fileId);
        // Assuming fileId is the ID of the audio file in GridFS
        downloadStream.pipe(res);
    } 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' });
    }
});

// Update the /api/playlists POST endpoint
app.post('/api/playlists', async (req, res) => {
    try {
        const { name, songs } = req.body;

        // Find the ObjectId values of the 
        // songs specified by their titles
        const existingSongs = await Song.find({ title:{$in: songs} });
        const songIds = existingSongs.map(song => song._id);

        // Validate if all songs were found
        if (existingSongs.length !== songs.length) {
            const missingSongs =
                songs.filter(
                    song => !existingSongs.find(
                        existingSong => existingSong.title === song)
                );
            return res.status(400)
                .json(
                    {
                        error: `One or more songs not found: 
                                ${missingSongs.join(', ')}`
                    });
        }

        // Create the playlist with the provided data
        const playlist = new Playlist({ name, songs: songIds });

        // Save the playlist to the database
        await playlist.save();

        // Return the created playlist
        res.json(playlist);
    } catch (err) {
        if (err.code === 11000 && err.keyPattern
            && err.keyPattern.songcode) {
            return res.status(400)
                .json({ error: 'Duplicate songcode found' });
        } else {
            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);
        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}`));

Start the backend server with the following command:

node server.js

Steps to Create the Frontend:

Step 1: Create a directory named “Songs” for keeping audio.

Step 2: Create FrontEnd using React:-

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

Step 3: Install the required dependencies.

npm install axios

Project Structure(Frontend):

Screenshot-2024-03-12-173105

The updated dependencies in package.json file of Frontend 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-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}

Example: Create the required files and write the following code.

CSS
/* PlaylistList.css */
.playlist-container {
  background-color: #f2f2f2;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.playlist-title {
  color: #f96d00;
  font-size: 24px;
  margin-bottom: 10px;
}

.playlist-item {
  color: #333;
  font-size: 16px;
  margin-bottom: 8px;
}
/* SongList.css */
.song-container {
  background-color: #f2f2f2;
  padding: 20px;
  border-radius: 8px;
}

.song-title {
  color: #f96d00;
  font-size: 24px;
  margin-bottom: 10px;
}

.song-item {
  color: #333;
  font-size: 16px;
  margin-bottom: 8px;
}
/* App.css */

body {
  font-family: Arial, sans-serif;
  background-color: #f96d00; /* Update background color */
  margin: 0;
  padding: 0;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  color: #f2f2f2; /* Update text color */
}

ul {
  list-style: none;
  padding: 0;
}

li {
  margin-bottom: 10px;
}

input[type="text"] {
  width: 200px;
  padding: 8px;
  border: none;
  border-radius: 4px;
  margin-right: 10px;
}

button {
  background-color: #f2f2f2; /* Update background color */
  color: #f96d00; /* Update text color */
  border: none;
  padding: 10px 20px;
  cursor: pointer;
  border-radius: 4px;
}

button:hover {
  background-color: #f96d00; /* Update background color */
  color: #f2f2f2; /* Update text color */
}

.song-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.now-playing {
  margin-left: auto;
}

.play-pause-button {
  margin-left: 10px; /* Adjust as needed */
}
Javascript
// App.js
import React from 'react';
import PlaylistList from './Playlist';
import SongList from './Songs';
import './App.css';

function App() {
    return (
        <div className="container">
            <h1>Music Playlist App</h1>
            <PlaylistList />
            <SongList />
        </div>
    );
}

export default App;
Javascript
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css'; // Import CSS file

function PlaylistList() {
    const [playlists, setPlaylists] = useState([]);
    const [newPlaylistName, setNewPlaylistName] = useState('');
    const [newPlaylistSongs, setNewPlaylistSongs] = useState([]);
    const [hoveredPlaylist, setHoveredPlaylist] = useState(null);
    // State to track the hovered playlist ID

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

    const handleCreatePlaylist = () => {
        axios.post('http://localhost:5000/api/playlists', {
            name: newPlaylistName,
            songs: newPlaylistSongs,
            // Include songs array in the request body
            user: 'user_id',
            // Replace 'user_id' with the actual user ID
            playlistCode: 'your_playlist_code'
            // Replace 'your_playlist_code' with 
            // the actual playlist code
        })
            .then(response => {
                setPlaylists([...playlists, response.data]);
                setNewPlaylistName('');
                setNewPlaylistSongs([]);
            })
            .catch(error => {
                console.error('Error creating playlist:', error);
            });
    };

    // Function to handle playlist hover
    const handleMouseEnter = (playlistId) => {
        setHoveredPlaylist(playlistId);
    };

    // Function to handle leaving playlist hover
    const handleMouseLeave = () => {
        setHoveredPlaylist(null);
    };

    return (
        <div className="playlist-container">
            <h2 className="playlist-title">Playlists</h2>
            <ul>
                {playlists.map(playlist => (
                    <li
                        key={playlist._id}
                        className="playlist-item"
                        onMouseEnter={() => handleMouseEnter(playlist._id)}
                        // Set the hovered playlist ID
                        onMouseLeave={handleMouseLeave}
                        // Clear the hovered playlist ID when leaving
                        style={
                            {
                                fontSize: "large",
                                fontWeight: "bolder",
                                color: "#3a4750"
                            }}>
                        {playlist.name}
                        {/* Display the songs when the playlist is hovered */}
                        {
                            hoveredPlaylist === playlist._id &&
                            playlist.songs && Array.isArray(playlist.songs) && (
                                <div className="song-list-container"
                                    style={
                                        {
                                            backgroundColor: "#e3e3e3",
                                            padding: "2%",
                                            margin: "1%"
                                        }}> {/* Container for the song list */}
                                    <h3 className="song-list-title">
                                        Songs
                                    </h3>
                                    <ul className="song-list">
                                        {
                                            playlist.songs.map(song => (
                                                <li key={song._id}>
                                                    {song.title}
                                                </li>
                                            ))
                                        }
                                    </ul>
                                </div>
                            )
                        }
                    </li>
                ))}
            </ul>
            <input
                type="text"
                placeholder="Enter playlist name"
                value={newPlaylistName}
                onChange={e => setNewPlaylistName(e.target.value)} />
            <input
                type="text"
                placeholder="Enter songs (separated by commas)"
                value={newPlaylistSongs.join(',')}
                onChange={e => setNewPlaylistSongs(e.target.value.split(','))} />
            <button onClick={handleCreatePlaylist}>
                Create Playlist
            </button>
        </div>
    );
}

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

function Songs() {
    const [songs, setSongs] = useState([]);
    const [currentSong, setCurrentSong] = useState(null);
    // State to keep track of the currently playing song
    const [audio, setAudio] = useState(null);
    // State to keep track of the audio element

    useEffect(() => {
        axios.get('http://localhost:5000/api/songs')
            .then(response => {
                setSongs(response.data);
            })
            .catch(error => {
                console.error('Error fetching songs:', error);
            });
    }, []);

    const playSong = (song) => {
        if (currentSong === null || currentSong._id !== song._id) {
            if (audio) {
                audio.pause(); // Pause the current song if any
            }
            const newAudio =
                new Audio(`
                    http://localhost:5000/api/songs/${song._id}/audio`);
            setCurrentSong(song);
            setAudio(newAudio);
            newAudio.play(); // Play the new song
        } else {
            if (audio.paused) {
                audio.play(); // If paused, resume playing
            } else {
                audio.pause(); // If playing, pause
            }
        }
    };

    return (
        <div className="song-container">
            <h2 className="song-title">Songs</h2>
            <ul>
                {
                    songs.map(song => (
                        <li key={song._id} className="song-item" 
                            onClick={() => playSong(song)}>
                            <span>{song.title} - {song.artist}</span>
                            {currentSong && currentSong._id === song._id && (
                                <span className="now-playing">
                                    Now playing: {currentSong.title}
                                </span>
                            )}
                        </li>
                    ))
                }
            </ul>
            {currentSong && (
                <div>
                    <button onClick={() => playSong(currentSong)}>
                        {audio && !audio.paused ? 'Pause' : 'Play'}
                    </button>
                </div>
            )}
        </div>
    );
}

export default Songs;

Start the Frontend Application with the following command:

npm start

Output:

  • Browser Output

679y-ezgifcom-video-to-gif-converter

  • Data saved in Database:
18mar

Db Data



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads