April 11, 2024
Creating Your Own Music Streaming Platform: A Comprehensive Guide
Introduction
In this detailed guide, we'll walk you through the process of building your own music streaming platform from scratch. Whether you're a beginner or an experienced developer, this tutorial will provide you with the essential steps to create a robust application similar to Spotify. Let's dive in!
Understanding the Project
Before we start coding, let's outline what our music streaming platform will offer:
- User Interface: Displaying album artwork, artist name, and song title.
- Functionality: Providing features like play/pause, skip forward/backward, playlist management, and adding new songs to the library.
Setting Up the Project
To begin, ensure you have the necessary tools installed:
- Backend: Django framework for handling server-side logic.
- Frontend Styling: Tailwind CSS for sleek and responsive designs.
- Audio Management: Mutagen library to handle audio metadata.
Clone the starter files from the repository provided, which includes the initial setup and configurations.
Building the Core Components
1. Defining Models
Start by defining database models for music and albums using Django's ORM. These models will represent songs and their associated attributes like title, artist, album, duration, audio file, and cover image.
from musics.helper import get_audio_length
from django.db import models
from .validators import validate_is_audio
class Music(models.Model):
title=models.CharField(max_length=500)
artiste=models.CharField(max_length=500)
album=models.ForeignKey('Album',on_delete=models.SET_NULL,null=True,blank=True)
time_length=models.DecimalField(max_digits=20, decimal_places=2,blank=True)
audio_file=models.FileField(upload_to='musics/',validators=[validate_is_audio])
cover_image=models.ImageField(upload_to='music_images/')
def save(self,*args, **kwargs):
if not self.time_length:
# logic for getting length of audio
audio_length=get_audio_length(self.audio_file)
self.time_length =f'{audio_length:.2f}'
return super().save(*args, **kwargs)
class META:
ordering="id"
class Album(models.Model):
name=models.CharField(max_length=400)
2. Creating Views and URLs
Set up views to handle user requests and URLs to map these views. For example, create a homepage view to render the main interface where users can interact with the music player and manage playlists.
# urls
from musics.views import addMusic, homePage, musicList
from django.urls import path
app_name='musics'
urlpatterns = [
path('',homePage,name='home_page'),
path('add/',addMusic,name='add_music'),
]
#views
from musics.models import Album, Music
from django.shortcuts import redirect, render
from django.http import JsonResponse
from .form import AddMusicForm
def homePage(request):
musics=list(Music.objects.all().values())
return render(request,'home.html',{
'musics':musics
})
def addMusic(request):
form=AddMusicForm()
if request.POST:
form=AddMusicForm(request.POST,request.FILES)
if form.is_valid():
instance=form.save(commit=False)
album=form.cleaned_data.get('album')
if album:
music_album=Album.objects.get_or_create(name=album)
print(music_album)
instance.album=music_album[0]
instance.save()
return redirect("music:home_page")
else:
instance.save()
return redirect("music:home_page")
else:
print("no",form.data)
return render(request,'addPage.html',{
'form':form
})
# home.html
{% extends 'base.html' %}
{% load music_tags %}
{% block Title %}
Spotify: The world of better music
{% endblock Title %}
{% block content %}
<div class=" flex items-center justify-center py-12 relative" style="background: #edf2f7; ">
<div class="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<div class="relative max-w-xl w-full h-36 bg-white rounded-lg shadow-lg overflow-hidde mb-32">
<div class="absolute inset-0 rounded-lg overflow-hidden bg-red-200">
<img src="https://images.unsplash.com/photo-1499415479124-43c32433a620?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=889&q=80" alt="">
<div class="absolute inset-0 backdrop backdrop-blur-10 bg-gradient-to-b from-transparent to-black">
</div>
</div>
<div class="absolute flex space-x-6 transform translate-x-6 translate-y-8">
<div class="w-36 h-36 rounded-lg shadow-lg overflow-hidden">
<img class="music_img w-full object-cover" src="https://images.unsplash.com/photo-1488036106564-87ecb155bb15?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=375&q=80" alt="">
</div>
<div class="text-white pt-12">
<h3 class="font-bold">Album</h3>
<div class="text-sm opacity-60 album">Super Interpret</div>
<div class="mt-8 text-gray-400">
<div class="flex items-center space-x-2 text-xs">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>
<span>Easy listening</span>
</div>
</div>
</div>
</div>
</div>
<div class="max-w-xl bg-white rounded-lg shadow-lg overflow-hidden">
<div class="relative">
<img
src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80"
class="object-cover">
<div class="absolute p-4 inset-0 flex flex-col justify-end bg-gradient-to-b from-transparent to-gray-900 backdrop backdrop-blur-5 text-white">
<h3 class="font-bold artist">Super Artist</h3>
<span class="opacity-70 song-title">Albumtitle</span>
</div>
</div>
<div>
<div class="relative h-1 bg-gray-200 progress_container">
<div class="absolute h-full w-2 progress bg-green-500 flex items-center justify-end">
<div class="rounded-full w-3 h-3 bg-white shadow"></div>
</div>
</div>
</div>
<div class="flex justify-between text-xs font-semibold text-gray-500 px-4 py-2">
<div class="currentDuration">
0:00
</div>
<div class="flex space-x-3 p-2">
<button class="focus:outline-none hover:bg-gray-200 prev">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="19 20 9 12 19 4 19 20"></polygon><line x1="5" y1="19" x2="5" y2="5"></line></svg>
</button>
<button class="rounded-full w-8 h-8 flex hover:bg-gray-200 items-center justify-center pl-0.5 ring-2 ring-gray-100 focus:outline-none playing">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
</button>
<button class="focus:outline-none hover:bg-gray-200 next">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg>
</button>
</div>
<div class="Duration">
0:00
</div>
</div>
<ul class="text-xs sm:text-base divide-y border-t cursor-default audio-tracks">
{% for music in musics %}
<li class="flex items-center space-x-3 hover:bg-gray-100">
<button class="p-3 hover:bg-green-500 group focus:outline-none play_single">
<svg class="w-4 h-4 group-hover:text-white play_svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
</button>
<div class="flex-1">
{{music.artiste}} - {{music.title}}
</div>
<div class="text-xs text-gray-400">
{{music.time_length | time_formater}}
</div>
<a href="media/{{music.audio_file}}" class="focus:outline-none pr-4 group">
<svg class="w-4 h-4 group-hover:text-green-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"/></svg>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<a href="/add" class="w-20 h-20 bg-green-600 absolute rounded-full right-48 bottom-12 flex justify-center items-center cursor-pointer block">
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon h-10 w-10 text-gray-100 font-black' viewBox='0 0 512 512'><title>Add</title><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M256 112v288M400 256H112'/></svg>
</a>
<audio class="audio-player" preload="metadata" autoplay="false" src="/media/musics/2017-04-14_-_Happy_Dreams_-_David_Fesliyan.mp3"></audio>
</div>
{{musics|json_script:'musics'}}
<script>
const player=document.querySelector('.audio-player')
const play=document.querySelector('.playing')
const prev=document.querySelector('.prev')
const next=document.querySelector('.next')
const currentTime=document.querySelector('.currentDuration')
const duration=document.querySelector('.Duration')
const progress=document.querySelector('.progress')
const progress_container=document.querySelector('.progress_container')
const audio_tracks=document.querySelector('.audio-tracks')
const song_title=document.querySelector('.song-title')
const artist=document.querySelector('.artist')
const album=document.querySelector('.album')
const music_img=document.querySelector('.music_img')
// initilize music indexing
let musicIndex=0
//get music document from backend
const musics=JSON.parse(document.getElementById('musics').textContent)
// functions
// format time for music
const formatTime=secs=>{
let min = Math.floor((secs % 3600) / 60);
let sec = Math.floor(secs % 60)
if (sec<10){
sec=`0${sec}`
}
return `${min}:${sec}`
}
// loading a set detail of music in UI
const setSRC=()=>{
player.src=`/media/${musics[musicIndex].audio_file}`
song_title.textContent=musics[musicIndex].title
artist.textContent=musics[musicIndex].artiste
music_img.setAttribute('src',`media/${musics[musicIndex].cover_image}`)
if (musics[musicIndex].album_id !== null){
album.textContent=musics[musicIndex].album_id
}else{
album.textContent='Single'
}
}
//determine player should play or not
const playOrPause=()=>{
if (player.paused){
player.play()
}
else{
player.pause()
}
}
// load first music
setSRC()
player.pause()
// eventlisteners
// when play btn is clicked
play.addEventListener("click",e=>{
playOrPause()
})
// update the progress bar
player.addEventListener('timeupdate',e=>{
let secs=player.currentTime
let total=player.duration
currentTime.textContent=formatTime(player.currentTime)
let progress_container_width=progress_container.offsetWidth
let progress_width=progress.offsetWidth
let audio_played=(secs/total)*100
let newWidth = progress_container_width*(audio_played/100)
progress.style.width=`${newWidth}px`
})
//when a progress bar is clicked to change music timer
progress_container.addEventListener('click',(e)=>{
const click_percentage=(e.offsetX/progress_container.offsetWidth)*100
// console.log(click_percentage)
const audio_played=(click_percentage/100)*player.duration
// console.log(audio_played)
let plaing
if (player.paused){
playing=false
}
else{
playing=true
}
player.currentTime=audio_played
if(playing==false){
player.pause()
}
})
// loads durations of music for current music to UI
player.addEventListener('loadedmetadata',()=>{
duration.textContent=formatTime(player.duration)
// console.log(duration.textContent)
})
// when an audio is chosen from the song tracks
audio_tracks.addEventListener('click',e=>{
if((e.target.nodeName=='BUTTON' && e.target.classList.contains('play_single'))||
(e.target.nodeName=='svg'&&e.target.classList.contains('play_svg'))){
let parent_cont
if (e.target.nodeName=='BUTTON'){
parent_cont=e.target.parentNode
}
else{
parent_cont=e.target.parentNode.parentNode
}
const newIndex=Array.from(audio_tracks.querySelectorAll('li')).indexOf(parent_cont)
if (newIndex==musicIndex){
if(player.paused){
player.play()
}else{
player.pause()
}
}else{
musicIndex=newIndex
setSRC()
player.play()
}
}
})
// when the prev btn is clicked
prev.addEventListener('click',()=>{
musicIndex=musicIndex-1
if(musicIndex<0){
musicIndex=musics.length-1
}
setSRC()
playOrPause()
})
// when the next btn is clicked
next.addEventListener('click',()=>{
musicIndex=musicIndex+1
if(musicIndex>musics.length-1){
musicIndex=0
}
setSRC()
playOrPause()
})
</script>
{% endblock content %}
#addPage.html
{% extends 'base.html' %}
{% block content %}
<h2 class="text-center text-green-700 text-2xl font-bold mt-8 mb-10">Add A new Song</h2>
<form action="" method="post" class="w-10/12 md:w-1/2 mx-auto mb-8" enctype="multipart/form-data">
{% csrf_token %}
<div class="my-4">
<label for="id_title" class="text-lg ">Album (Optional):</label>
<input type="text" name="album" maxlength="500" id="id_album" class="block w-full p-2 outline-none border-2 border-black rounded-lg">
</div>
<div class="my-4">
<label for="id_title" class="text-lg ">Title:</label>
<input type="text" name="title" maxlength="500" required="" id="id_title" class="block w-full p-2 outline-none border-2 border-black rounded-lg">
</div>
<div class="my-4">
<label for="id_title" class="text-lg ">Artist:</label>
<input type="text" name="artiste" maxlength="500" required="" id="id_artiste" class="block w-full p-2 outline-none border-2 border-black rounded-lg">
</div>
<div class=" overflow-hidden flex items-center justify-center flex-col my-4" >
<div class="details-audio text-black text-base">
</div>
<div class="flex w-full items-center justify-center bg-grey-lighter">
<label class="w-64 flex flex-col items-center px-4 py-6 bg-white text-blue rounded-lg shadow-lg tracking-wide uppercase border border-blue cursor-pointer hover:bg-blue-500 hover:text-white">
<svg class="w-8 h-8" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z" />
</svg>
<span class="mt-2 text-base leading-normal">Select a Music File</span>
<input type='file' accept="audio/mp3" name="audio_file" class="hidden " onchange="showname('audio')" id="id_audio_file" required />
</label>
</div>
</div>
<div class=" overflow-hidden flex items-center justify-center flex-col my-4" >
<div class="details-image text-black text-base">
</div>
<div class="flex w-full items-center justify-center bg-grey-lighter">
<label class="w-64 flex flex-col items-center px-4 py-6 bg-white text-blue rounded-lg shadow-lg tracking-wide uppercase border border-blue cursor-pointer hover:bg-blue-500 hover:text-white">
<svg class="w-8 h-8" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z" />
</svg>
<span class="mt-2 text-base leading-normal">Select a Music Cover Image</span>
<input type='file' accept="image/*" name="cover_image" class="hidden cover_image" onchange="showname('image')" id="id_cover_image" required />
</label>
</div>
</div>
<button class="bg-black p-4 text-white text-center ">
Submit
</button>
</form>
<script>
function showname (type) {
if(type=='audio'){
var name = document.getElementById('id_audio_file');
}else{
var name = document.getElementById('id_cover_image');
}
const file_name= name.files.item(0).name
document.querySelector(`.details-${type}`).textContent=`${file_name}`
};
</script>
{%endblock content%}
Enhancing Functionality
1. Dynamic Time Formatting
Format the display of song duration to show minutes and seconds for better user understanding.
from mutagen.mp3 import MP3
def get_audio_length(file):
audio = MP3(file)
return audio.info.length
const formatTime=secs=>{
let min = Math.floor((secs % 3600) / 60);
let sec = Math.floor(secs % 60)
if (sec<10){
sec=`0${sec}`
}
return `${min}:${sec}`
}
2. Integrating JavaScript for Dynamic Functionality
Use JavaScript to implement dynamic features such as audio playback controls, progress bar updates, and interaction with the user interface.
// update the progress bar
player.addEventListener('timeupdate',e=>{
let secs=player.currentTime
let total=player.duration
currentTime.textContent=formatTime(player.currentTime)
let progress_container_width=progress_container.offsetWidth
let progress_width=progress.offsetWidth
let audio_played=(secs/total)*100
let newWidth = progress_container_width*(audio_played/100)
progress.style.width=`${newWidth}px`
})
3. Loading Music Data into the UI
Send music data from the backend to the frontend using Django templates and JSON scripts, then load it into the user interface to display song details.
{{musics|json_script:'musics'}}
// initilize music indexing
let musicIndex=0
//get music document from backend
const musics=JSON.parse(document.getElementById('musics').textContent)
4. Implementing Audio Playback Controls
Add controls for play, pause, next, and previous actions, allowing users to navigate through the playlist and control playback seamlessly.
//determine player should play or not
const playOrPause=()=>{
if (player.paused){
player.play()
}
else{
player.pause()
}
}
// load first music
setSRC()
player.pause()
// when the prev btn is clicked
prev.addEventListener('click',()=>{
musicIndex=musicIndex-1
if(musicIndex<0){
musicIndex=musics.length-1
}
setSRC()
playOrPause()
})
// when the next btn is clicked
next.addEventListener('click',()=>{
musicIndex=musicIndex+1
if(musicIndex>musics.length-1){
musicIndex=0
}
setSRC()
playOrPause()
})
5. Adding New Songs
Implement a feature to upload new songs to the database, including validation to ensure supported audio formats.
import os
from django.core.exceptions import ValidationError
from mutagen.mp3 import MP3
def validate_is_audio(file):
try:
audio = MP3(file)
if not audio :
raise TypeError()
first_file_check=True
except Exception as e:
first_file_check=False
if not first_file_check:
raise ValidationError('Unsupported file type.')
valid_file_extensions = ['.mp3']
ext = os.path.splitext(file.name)[1]
if ext.lower() not in valid_file_extensions:
raise ValidationError('Unacceptable file extension.')
Conclusion
Congratulations! By following this comprehensive guide, you've successfully created your own music streaming platform using Django and JavaScript. You've learned how to handle music playback, manage playlists, and enhance user interaction. Feel free to explore further and customize your application with additional features and personal touches. You can find the complete YouTube video and source code here. Happy coding!
241 views