DEV Community

Mangabo Kolawole
Mangabo Kolawole Subscriber

Posted on • Edited on

Building a Music Streaming Service with Python, Golang, and React: From System Design to Coding Part 1

Streaming is an interesting topic in software engineering. Whether it is about music, video, or just simple data, applying this concept from a design, architecture, and coding perspective can become quickly complex if not thought through correctly.

In today's article, we will build a service to stream music using Golang and React. We will start by showing the incremental system design along with the corresponding code implementation for each step. By the end of this series of articles, you will learn:

  • How to build an API using Python and Golang

  • How to serve HTTP range requests

  • How to create a system design/architecture for a music streaming service

Without further ado, let's dive into the project.

If you are interested in more content covering topics like this, subscribe to my newsletter for regular updates on software programming, architecture, and tech-related insights.

Introduction to the Project

Building a music streaming service is a challenging yet rewarding exercise for developers, and I am happy to see you here following along with me. For simplicity, we will only focus on implementing the streaming part of the solution. Thus, there is no need to have an API service or other additional services at this stage.

Here are the requirements for the system design:

  1. Basic Song Retrieval: The client requests a song, and the server responds with a JSON object containing the song URL.

  2. Scalability: Efficiently handle thousands of requests with ease.

  3. Secure Streaming: Prevent direct downloads and serve data using HTTP range requests.

  4. Global Accessibility: Ensure low latency, reliability, redundancy, and security worldwide.

With these requirements in mind, let's create the codebase for this article to keep updating the codebase according to the changes implemented.

Setup the Project

For this project, we are using Django, Golang, and React. Golang provides an easy way to write performant code, while React has a lot of libraries that can quickly help us set up a frontend for this project. Django will be used for building the API that serves data about the songs in the database.

You can skip this part if you do not have enough time. Just make sure to clone the project and switch to the base branch.



git clone -b base https://github.com/koladev32/golang-react-music-streaming.git
cd golang-react-music-streaming
make setup


Enter fullscreen mode Exit fullscreen mode

If you prefer to set it up manually, follow this guide.

In your work folder, create a folder called backend. This folder will contain the code for the Django API we'll build.



cd backend
python3 -m venv venv
source venv/bin/activate


Enter fullscreen mode Exit fullscreen mode

Then, let's install the required libraries and create a Django project.



pip install django djangorestframework pillow django-cors-headers


Enter fullscreen mode Exit fullscreen mode
  • Django and Django REST Framework: These will be used to build the song model and serve the JSON information from a REST API.

  • Django Cors Headers: This will help us set up CORS configuration so the frontend can make easy requests to the backend.

  • Pillow: This is used as we are going to serve media files and static files (thumbnails) from the API.

Make sure these dependencies are registered in the requirements.txt file.



Django==5.0.7
djangorestframework==3.15.2
pillow==10.4.0
django-cors-headers==4.4.0


Enter fullscreen mode Exit fullscreen mode

Now, create a project called backend, and then a new Django application called music.



django-admin startproject backend .
django-admin startapp music


Enter fullscreen mode Exit fullscreen mode

Once this is done, run the database migrations and start the Django server. It should be running at http://localhost:8000.



python manage.py migrate
python manage.py runserver


Enter fullscreen mode Exit fullscreen mode

Setting Up the Frontend

At the root of the project, type the following command to create a new Next.js project.



npx create-next-app@latest


Enter fullscreen mode Exit fullscreen mode

After running this command, you will be presented with a prompt with different options to choose from. Here are the options I've chosen.

Once it is done, enter the frontend directory and install a media player package, react-h5-audio-player.



npm install react-h5-audio-player


Enter fullscreen mode Exit fullscreen mode

react-h5-audio-player is a customizable and accessible audio player component for React applications. It provides essential playback controls and leverages the HTML5 audio element for reliable performance. This will be useful for this project as we aim for simplicity.

Once it is done, run the frontend server with the following command:



npm run dev


Enter fullscreen mode Exit fullscreen mode

This will start a Next.js application at http://localhost:3000.

With the base project setup, we can start writing the code for the first version of the application.

Basic Song Retrieval

In the previous section, we set up the project. In this section, we will build the MVP of the product, an application that can do a basic song retrieval and play it through a web frontend.

Before doing that, let's visualize the system design for this requirement.

The system design involves clients requesting data about songs with access to the storage of the songs files, to retrieve them. The API processes these requests, interacting with a database that stores song metadata and file storage for audio files.

This system design ensures the following characteristics:

  1. Scalability: This is a simple monolithic application. Replicating the same instance of this architecture across many regions and many times to ensure reliability and using a load balancer to ensure traffic distribution is a way to go. Naturally, in our case, we are focusing on an MVP.

  2. Storage: The storage is directly on the server. This ensures quick writing when creating a song object in the database for example.

Let's now write the coding implementation.

Building the Backend

In the MVP, the backend will only serve an API capable of retrieving and listing songs. Let's start by writing the Song model.

In the music/models.py file, create a new model called Song with the following fields: title, author, duration, thumbnail, and file.



from django.db import models

class Song(models.Model):
    name = models.CharField(max_length=100)
    artist = models.CharField(max_length=100)
    duration = models.IntegerField()
    thumbnail = models.ImageField(upload_to='images/')
    file = models.FileField(upload_to='music/')


Enter fullscreen mode Exit fullscreen mode

This model defines the basic structure for our song data. Each song has a name, an artist, a duration (in seconds), a thumbnail image, and an audio file.

For this project, I am using songs from the https://freemusicarchive.org/ website. Add this script at the root of the backend project, in the same scope as manage.py.



# insert_songs.py
import os
import django

# Set up the Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()

from music.models import Song
import requests
from django.core.files.base import ContentFile, File
from django.core.files.temp import NamedTemporaryFile

# List of songs to be inserted
songs = [
    {
        "name": "Irreplaceable",
        "author": "Sapio",
        "file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/BYl1B50WKQCKbiLtECjNYumY18htQTlaqJ5MpUTt.mp3?download=1&name=Sapio%20-%20Irreplaceable.mp3",
        "thumbnail": "https://freemusicarchive.org/image/?file=track_image%2Fvu1EU9H0Pxv67II9pAOvQ28fHdAZppapGOQgYbxY.png&width=290&height=290&type=track",
        "duration": 300
    },
    {
        "name": "Soulmates (Dear Future Wife / Husband)",
        "author": "Tadz",
        "file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/vGwJswHSC5hO8wmqNvVKpoE9BaehzkkiBWLMASR7.mp3?download=1&name=Tadz%20-%20Soulmates%20%28Dear%20Future%20Wife%20%2F%20Husband%29.mp3",
        "thumbnail": "https://freemusicarchive.org/image/?file=track_image%2FVn4FvKcfzVnLfiQ4zRqarP18oSfQ19o3gG1FfynR.png&width=290&height=290&type=track",
        "duration": 300
    },
    {
        "name": "High School Crush",
        "author": "Tadz",
        "file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/3Y0b1YV4ePQ0ZwEfMUmDXOtvArQz4Nsoe2rO777W.mp3?download=1&name=Tadz%20-%20High%20School%20Crush.mp3",
        "thumbnail": "https://freemusicarchive.org/image/?file=track_image%2F0dyea9xG9zypHMyiVJ58CyQQWT8Ena2JWZKB0QH0.jpg&width=290&height=290&type=track",
        "duration": 317
    }
]


def download_file(url):
    response = requests.get(url)
    if response.status_code == 200:
        return ContentFile(response.content)
    else:
        return None


for song_data in songs:
    song = Song(
        name=song_data['name'],
        artist=song_data['author'],
        duration=song_data['duration']
    )

    # Download and save the thumbnail
    thumbnail = download_file(song_data['thumbnail'])
    if thumbnail:
        temp_thumb = NamedTemporaryFile(delete=True)
        temp_thumb.write(thumbnail.read())
        temp_thumb.flush()
        song.thumbnail.save(f"{song_data['name']}_thumbnail.jpg", File(temp_thumb))

    # Download and save the song file
    song_file = download_file(song_data['file'])
    if song_file:
        temp_file = NamedTemporaryFile(delete=True)
        temp_file.write(song_file.read())
        temp_file.flush()
        song.file.save(f"{song_data['name']}.mp3", File(temp_file))

    song.save()
    print(f"Inserted {song.name} by {song.artist}")


Enter fullscreen mode Exit fullscreen mode

To run the script, use the following command.



python populate.py


Enter fullscreen mode Exit fullscreen mode

Now you have data in your database and we can proceed with the tutorial.

Next, we need to create a serializer. In the music directory, create a file called serializers.py and add the following code.



from rest_framework import serializers
from .models import Song

class SongSerializer(serializers.ModelSerializer):
    class Meta:
        model = Song
        fields = '__all__'


Enter fullscreen mode Exit fullscreen mode

A serializer in Django REST Framework is responsible for converting complex data types, such as querysets and model instances, into native Python datatypes that can be easily rendered into JSON, XML, or other content types. Here, SongSerializer converts the Song model into JSON format.

Then, create another file called viewsets.py where we will add the request logic handler for the songs.



from rest_framework import viewsets, permissions
from music.models import Song
from music.serializers import SongSerializer

class SongViewSet(viewsets.ModelViewSet):
    queryset = Song.objects.all()
    serializer_class = SongSerializer
    permission_classes = [permissions.AllowAny]


Enter fullscreen mode Exit fullscreen mode

In this code, we are declaring a viewset called SongViewSet and setting the default queryset to Song.objects.all(). This means that every listing or retrieving action will use this queryset to return the desired list of objects or the object itself. We are also setting the serializer class and the permission classes. For simplicity, we are allowing anyone to interact with the API without authentication and permissions, but in a real-world project, the API should be protected via authentication.

Now that we have written the API logic for the music application, let's register the viewsets in the URLs of the Django application by creating a router. We will then proceed to write some configurations in the settings.py file of the project.

At the root of the Django project, create a file called routers.py.



from rest_framework.routers import SimpleRouter
from music.viewsets import SongViewSet

router = SimpleRouter()
router.register('songs', SongViewSet)

urlpatterns = router.urls


Enter fullscreen mode Exit fullscreen mode

In this code, we are creating a router using the SimpleRouter API. We are then registering a new route called songs with the viewset concerned, SongViewSet.

In the backend/urls.py file, add the following code to register the newly created router for our REST API:



from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include(("routers", "core"), namespace="music-api")),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


Enter fullscreen mode Exit fullscreen mode

In the code above, we are registering a new path for the API and including the routes created in the routers.py. The last line in this code snippet is important so we can work with

static and media files in development mode.

Now that we have added the URLs required to access the API, let's write some important configurations in the settings.py file. These configurations will concern CORS, application configuration, and static and media file configuration.



...
# Application definition

INSTALLED_APPS = [
    ...
    "corsheaders",
    "rest_framework",
    "music",
]

MIDDLEWARE = [
    ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    ...
]

...

STATIC_URL = "static/"

MEDIA_ROOT = BASE_DIR / "media"

MEDIA_URL = "/media/"

...

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
]


Enter fullscreen mode Exit fullscreen mode

In the code above, we are registering new applications, such as corsheaders, rest_framework, and finally our defined app music. In the next configuration, we are adding the CorsMiddleware to the list of existing middleware. Then, we are adding configuration for static and media files by defining the STATIC_URL, MEDIA_ROOT, and finally MEDIA_URL. Finally, we are adding the CORS_ALLOWED_ORIGINS to set the only origins allowed to interact with the API. In this case, we are ensuring that applications running on localhost at port 3000 can freely access the API.

Great! Now run the following commands to create database migrations and apply them to the database.



python manage.py makemigrations
python manage.py migrate


Enter fullscreen mode Exit fullscreen mode

Nice! With the backend and the API built and ready to serve data, we can now move to building the frontend of the application.

Building the Frontend

In the precedent section of this article, we have built the API to serve details about songs. In this section, we are going to build the frontend of the application using Next.js.

With Next.js, we are using the AppRouter architecture which is pretty much straightforward. In the src directory, create a new directory called components. This directory will contain the components for this MVP application, such as the MusicPlayer component. In this newly created directory, create a file called music-player.jsx.

This file will contain the code for the MusicPlayer component.



import React from 'react';
import AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';

const MusicPlayer = ({ url }) => {
  return (
    <AudioPlayer
      autoPlay
      src={url}
      onPlay={e => console.log("Playing")}
    />
  );
};

export default MusicPlayer;


Enter fullscreen mode Exit fullscreen mode

There is not much to see here, we are just using the AudioPlayer component of react-h5-audio-player and then passing the url props that will be passed to the MusicPlayer component. We are also importing the default CSS styles of react-h5-audio-player.

Now, let's use this component in the page.js file in the src/app directory.



"use client"

import React, { useEffect, useState } from 'react';
import MusicPlayer from "@/components/music-player";
import Image from "next/image";

export default function Home() {
  const [songs, setSongs] = useState([]);
  const [currentSong, setCurrentSong] = useState(null);

  useEffect(() => {
    fetch('http://localhost:8000/api/songs/')
      .then((response) => response.json())
      .then((data) => setSongs(data));
  }, []);

  const playSong = (song) => {
    setCurrentSong(song.file);
  };

  return (
    <div className="container mx-auto px-4">
      <h1 className="text-4xl font-bold text-center my-8">Music Streaming</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {songs.map((song) => (
          <div key={song.id} className="bg-white p-4 rounded-lg shadow-md">
            <Image width={400} height={400} src={song.thumbnail} alt={song.name} className="w-full h-48 object-cover rounded-lg mb-4" />
            <h2 className="text-2xl font-semibold mb-2 text-black">{song.name}</h2>
            <p className="text-gray-600 mb-2">By {song.artist}</p>
            <button onClick={() => playSong(song)} className="text-blue-500 hover:underline">
              Listen Now
            </button>
          </div>
        ))}
      </div>
      {currentSong && (
        <div className="fixed bottom-0 left-0 right-0 bg-gray-800 p-4">
          <MusicPlayer url={currentSong} />
        </div>
      )}
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Great! Now, in the frontend application, you will be able to display songs and listen to those songs when clicking on the "Listen Now" button.

Listing of songs

Listening to a song

We now have a working MVP of the application, but there are some concerns regarding the scalability and confidentiality of the songs.

Cons of Our Architecture

The system architecture for this MVP might be simple, but it has a lot of flaws, notably from a security and confidentiality standpoint. Here are the cons:

  1. File Storage Distribution: File storage is directly on the server or next to the server. It must be distributed using a global CDN to ensure efficient file access.

  2. Caching Requirements: The system doesn't have a caching component implemented. As the system is more of a read than a write system, we need to implement a caching component to avoid useless requests to the database.

These cons can be addressed as these only require additional setup and not too much code configuration. In the next part of this topic, we will configure caching with Redis but also configure storage with AWS S3. I will ensure to give the similar services or processes if you are using another cloud provider.

Conclusion

In this article, we have created the first version of our application where a user can request for songs details and then retrieve a file directly and start playing it in the browser. We have used Django to build the API, and then Next.js for the frontend.

In the second part of this article, we will add caching and a better distribution of the files using Redis and AWS S3.

If you enjoyed this article and want to stay updated with more content, subscribe to my newsletter. I send out a weekly or bi-weekly digest of articles, tips, and exclusive content that you won't want to miss 🚀

Top comments (4)

Collapse
 
baljit1975 profile image
Baljit • Edited

Nice Tutorial but I am facing following error in backend:

Traceback (most recent call last):
File "C:\Program Files (x86)\Python36-32\lib\site-packages\django\core\files\temp.py", line 61, in del
self.close()
File "C:\Program Files (x86)\Python36-32\lib\site-packages\django\core\files\temp.py", line 49, in close
if not self.close_called:
AttributeError: 'TemporaryFile' object has no attribute 'close_called'
Traceback (most recent call last):
File "manage.py", line 117, in
temp_thumb = NamedTemporaryFile(delete=True)
TypeError: init() got an** unexpected keyword argument 'delete'**

Collapse
 
koladev profile image
Mangabo Kolawole

Can you please explain how you came to this error? That will help me help you

Collapse
 
thecodeinn profile image
Richard Emijere

The blog title does not reflect the content. Where is the use of Golang??

Collapse
 
koladev profile image
Mangabo Kolawole

The article is separated into many parts. The Golang part will be in Part 3.