Collaborative music playing full stack web app integrated with the third party Spotify API using Python(Django/Django Rest Framework) and Javascript(React)
Host creates a room for people to listen to Spotify music. Host controls the guests permissions such as how many votes are needed to skip the song and whether guests can pause/play songs. Guests can join the room using the room code. Spotify Premium required.
Django & React - Full Stack Web App with Python & Javascript from Tech With Tim.
Notes:
Tutorial 1 - Full Stack Web App with Python & JS:
- ~
pip install django djangorestframework
- ~
django-admin startproject music-controller
- cd to music_controller directory and ~
django-admin startapp api
- Add 'api.apps.ApiConfig' and 'rest_framework' to INSTALLED_APPS in settings.py of music_controller project to add add to the project. ApiConfig is from the apps.py in api app
- Create urls.py file in api app to store URLs local to this app. Create urls.py file in music_controller project
- Include api.urls to the url_patterns in urls.py in the music_controller project.
- Add views to the url_patterns in urls.py in the api app
- In music_controller project directory, ~
python manage.py makemigrations
to update the database and store current changes made to the app - ~
python manage.py migrate
- ~
python manage.py runserver
Tutorial 2 - Django REST Framework:
- Create Room model in models.py in api app
- In music_controller project directory, ~
python manage.py makemigrations
to update the database and store current changes made to the app and ~python manage.py migrate
- Create serializers.py in api app. This will take our Python related code and translate it into a JSON response. Add RoomSerializer class to serializers.py
generics.CreateAPIView
api view class RoomView.generics
allows us to create a class that inherits from an api view.- ~
python manage.py runserver
and add data generics.ListAPIView
api view class RoomView will list out the data
Tutorial 3 - React Integration Using Webpack & Babel:
- cd to music_controller project and ~
django-admin startapp frontend
- In frontend app, create templates, src and static folders. static holds static files, anything our browser would cache. In static folder create frontend, css and images folders. In src folder create components folder.
- Add 'frontend.apps.FrontendConfig' to INSTALLED_APPS in setting.py of music_controller/music_controller
- cd to music_controller\frontend and perform installations
- ~
npm init -y
- Then install webpack ~
npm i webpack webpack-cli --save-dev
. Webpack is a free and open-source module bundler for JavaScript. It is made primarily for JavaScript, but it can transform front-end assets such as HTML, CSS, and images if the corresponding loaders are included. Webpack takes modules with dependencies and generates static assets representing those modules. - Then install Babel ~
npm i @babel/core babel-loader @babel/preset-env @babel/preset-react --save-dev
, Babel takes our code and transpiles it into code that is friendly with all browsers such as ES6 or ES7 Javascript code - Then install react ~
npm i react react-dom
- Then install material UI ~
npm install @material-ui/core
- To use async and await in our Javascript code install ~
npm install @babel/plugin-proposal-class-properties
- To reroute our pages install ~
npm install react-router-dom
- To use icons from Material UI install ~
npm install @material-ui/icons
- ~
- Create babel.config.json file in music_controller/frontend. Set up Babel loader and uses environment presets thats targetting node version 10 and @babel/preset-react because we are using react. Plugins so we can use async and await
- Create webpack.config.js. Webpack will bundle all our Javascript into one file and serve that bundle to the browser
- Update package.json scripts with webpack running in development and watch mode. and build script with webpack in production mode
- We want Django to render a page that react will take control of
- Inside templates, create folder frontend. Inside frontend create index.html
- Inside view.py of music_controller\frontend create index function to render the index template and let react take care of it
- Create a urls.py for music_controller\frontend
- Inside urls.py for music_controller\music_controller add url for frontend
- Create new component App.js inside components folder
- Inside src folder, create index.js and import App from components/App
- Check that server is running in music_controller ~
python manage.py runserver
- cd to music_controller\frontend and run ~
npm run dev
- The main.js inside static\frontend now contains the bundled up Javascript
Tutorial 4 - React Router and Building Components:
- Create an index.css in static\css folder
- cd to music_controller\frontend and run ~
npm run dev
. cd to music_controller\frontend and run ~python manage.py runserver
- Create new components HomePage.js RoomJoinPage.js CreateRoomPage.js
- As of late 2021, react dom v6 switch is replaced by "Routes". To use TechwithTim code, do ~
npm uninstall react-router-dom
and thennpm install react-router-dom@5.2.0
- Update urls.py of music_controller/frontend with urls for react components
Tutorial 5 - Handling POST Requests (Django REST):
- in views.py of api app import following
from rest_framework.views import APIview
for generic apiviewfrom rest_framework.response import Response
to send custom responses from our viewfrom rest_framework import status
to give us access to HTTP status codes, which we will need to use when use send our custom responses
- Create a new serializer class CreateRoomSerializer in serializers.py of api app. We will send a POST request to the endpoint. This serializer will make sure the payload of the POST request corresponds with the correct fields that we need to create a new room.
- Create new view class CreateRoomView in views.py of api app. use session_key.
- Whenever we connect to a website we establish a session. A session is s temporary connection between 2 computers/devices. ex) don't have to sign into FB later because using the same session, everything authenticated already. Sessions have unique identities, in this case stored in system RAM.
- Add CreateRoomView to urls in urls.py of api app.
- Issue with editing the Room with POST. Result in Bad Request: /api/create-room and "POST /api/create-room HTTP/1.1" 400 12241
- Solved with ~
manage.py migrate --run-syncdb
Tutorial 6 - Material UI Components:
- Update CreateRoomPage.js with MaterialUI and connect with backend.
Tutorial 7 - Calling APi Endpoints From React
- Create Room.js, responsible for handling the room page
- Update HomePage.js with route for room page using roomCode
this.props.match.params
, match is the prop that stores all the information about how we get to this component from this react router- Update urls.py in music_controller\frontend with url for room. Should be able to access http://127.0.0.1:8000/room/ROOMCODE
- Create a view for the Room. Add view class GetRoom to view.py in the api app Update urls.py in api app with GetRoom view
request.GET.get(self.lookup_url_kwarg)
will get the code from the parameers in the url that matches the name 'lookup_url_kwarg', in this case 'code'- Check to see if works with http://127.0.0.1:8000/api/get-room?code=ROOMCODE
- Update Room.js with getRoomDetails() to fetch response from api
- Update fetch in CreateRoomPage.js for the createRoom
Tutorial 8 - Creating the Room Join Page
- Add center class to index.css. Modify the render div in App.js with className="center"
- Update RoomJoinPage.js for handling textfieldchange and roombutton pressed
- Create apiview class JoinRoom to check room exists
- Add urls to urls.py in api app
- Update RoomJoinPage.js to handle the api/join-room request
Tutorial 9 - ComponentDidMount and Django Sessions
- Update HomePage with
renderHomePage()
. More practice with Grid, Typography and ButtonGroup.disableElevation
to remove shadows.variant="contained"
to align horizontally - Update HomePage path with
renderHomePage()
function - Check if user is already in a room and if they are, we can redirect them to that room
- Using React lifecycle component methods. Every component in React has a lifecycle which you can monitor and manipulate during 3 phasess: Mounting, Updating and Unmounting
- Create new apiview
class UserInRoom
in views.py of api app. Update urls.py with url for UserInRoom - Update HomePage with
async componentDidMount
. On first render, it will show us the homepage, oncecomponentDidMount
has finished running it will check if we have a room and redirect if so - Update HomePage router in HomePage.js to join session or go to homepage with no room
Tutorial 10 - Django Sessions and Leaving Rooms
- Update Room.js with MaterialUI
- Create
leaveButtonPressed()
in Room.js to connect with backend and access endpoint. Bindthis.leaveButtonPressed
in the constructor - Create new apiview LeaveRoom in views.py of the api app. If host of the room leaves, then removes room and everyone leaves. Query on all the room objects to see if user was the host using
Room.objects.filter(host=host_id)
- Update urls.py with
LeaveRoom
view - Create a
clearRoomCode
function in HomePage.js to set the state of the roomCode to null. - Update '/room/:roomCode' route in HomePage.js with render.
props
are given by the route. Return a room with the...props
and leaveRoomCallback. '...' is the spread operator, will take all the properties passed in as an object and spread them out as prop1 is prop1 value and prop2 is prop2 value etc. Callback is a way that child component actually modify the parent component. This passes a method to the Room component so the Room component can call that method and modify the Room component - Update
leaveButtonPressed()
in Room.js to call LeaveRoom endpoint with fetch - Update
getRoomDetails
in Room.js to account for if we do not get response.ok. If we don't get response.ok, use methodthis.props.leaveRoomCallBack()
, which was passed to us from the HomePage, clear the state on the HomePage. Then usethis.props.history.push("/")
to redirect back to the home page because the room doesn't exist
Tutorial 11 - Updating Django Models
- Create a new Serializer
UpdateRoomSerializer
to handle the new Room settings update. Cannot pass a unique code to the serializer and therefore need to usecode = serializers.CharField(validators=[])
and pass thatcode
to the UpdateRoomSerializer - Create a new view
class UpdateRoom
in views.py of the api app. Will be using patch to update instead of get or post. Import theUpdateRoomSerializer
and use that as the serializer_class. Check if room exists and if user is host before sending response - Update urls.py in api app for the
UpdateRoom
view - Update this.state in Room.js with
showSettings
to determine whetehr to show settings or not. Update Room.js withupdateShowSettings(value)
function. Bindthis.updateShowSettings
- Create renderSettingsButton() in Room.js make a method that will only show settings if the user is the host. Add ternary operator above leave room to show Settings button if user is the host
- Create
renderSettings()
to access room settings. ImportCreateRoomPage
for room settings updates - Bind renderSettingsButton and renderSettings to this
- Add if statement to render show settings or not
Tutorial 12 - React Dafult Props and Callbacks
- Modify CreateRoomPage so that we can pass props to the CreateRoomPage as well as have default props for a new room. Do so by removing hard-coded default values and providing a
defaultProps
- Create
renderCreateButtons()
andrenderUpdateButtons()
. One method that renders the buttons to create a new room and one method that renders the button to update the room. - Update
render()
in CreateRoomPage.js with ternary operator to determine title: "Update Room" or "Create a Room" - Create
handleUpdateButtonPressed()
in CreateRoomPage.js to handle the UpdateRoom on the backend. AdderrorMsg
andsuccessMsg
to the this.state. Bindthis.handleUpdateButtonPressed
. UpdateonClick
withthis.handleUpdateButtonPressed
for therenderUpdateButtons()
- Import Collapse from MaterialUI. Collapse will allow us to show something or collapse it on the screen. Add Collapse to
render()
function.in
is a Boolean value, tells us whether or not to show Collapse or not - Update default value in Guest Control of Playback State in CreateRoomPage.js with
this.props.guestCanPause.toString()
- After updating Room in settings, we want the Room page to auto update as well. Update Room.js for the updating CreateRoomPage using
updateCallback
and calling thethis.getRoomDetails
. UpdatehandleUpdateButtonPressed()
withthis.props.updateCallback();
to update Room.js when going back to that Room page. Bindthis.getRoomDetails
in Room.js - ~
npm install @material-ui/lab
.import Alert from "@material-ui/lab/Alert";
in Create RoomPage.js. Update error or success messages with Alerts.severity
: "success" makes it green and error makes it "red".onClose()
to clear the successMsg or errorMsg
Tutorial 13 - Spotify API Tutorial (Authentication & Tokens)
- We authenticate our application with Spotify. Then the user authenticates our application - your application has access to my information it can control the music so on and so forth. Then using that authentication(tokens), we can send requests to the Spotify API and will in turn control the user's music
- Application requests authorization to access data -> Spotify displays scope of what Application wants to access and prompts user to login -> User logs in and authorizes access
- Application requests access and refresh tokens -> Spotify returns access and refresh tokens, access tokens used to access info and refresh token used to ask for another token because access tokens expire after a period of time
- Application uses access tokens in requests to Web API -> Spotify WEB API returns requested data
- Application - User access token in requests to Web API -> Spotify returns new access token
- Go to Spotify Developer, log in, and create an app
- Create new app in music_controller/music_controller using ~
python manage.py startapp spotify
. Add'spotify.apps.SpotifyConfig'
to INSTALLED_APPS of settings.py in music_controller project - Create urls.py and credentials.py in spotify app. Add credentials from Spotify to the credentials.py
- To authenticate or request access from Spotify - in views.py of spotify app, create apiview
class AuthURL(APIView)
to generate a url we can use to authenticate our Spotify application. Update urls.py of spotify app for AuthURL. Update urls.py for music-controller project for spotify app - Need to set up a redirect URI in credentials.py and in Spotify developer online. After sending request to Authurl, we need a callback or some url that the information requested(returned code and state) gets returned to. After getting Authorization access, we then need to send another request and get the access and refresh token.
- To create new model that can store tokens, create new model
class SpotifyToken
in models.py of Spotify app. ~python manage.py makemigrations
and ~python manage.py migrate
- Create util.py in spotify app to save our tokens by saving into a brand new model or updating a model. Create
get_user_tokens
in util.py to get user tokens. Createupdate_or_create_user_tokens
in util.py to update/create user tokens - In views.py of the spotify app, create function
spotify_callback
. Associate session key/id with their access/refresh tokens and store in our database. Useupdate_or_create_user_tokens
to store tokens in database and then use redirect back to our original application. Update urls.py of spotify app with redirect to frontend forspotify_callback
. To allow this, need to addapp_name = 'frontend'
in urls.py of frontend app because Django needs to know this urls.py file belongs to the frontend app. Need to give the '' path a formal name so that when we call the redirect we know which path we should actually go to - Need to check if current user is authenticated. Just need to check if the current session id representing the user is in the database and if the token is expired or not. Create
is_spotify_authenticated()
andrefresh_spotify_token()
in utils.py of spotify app - Need to set up a view that can tell us whether or not we are authenticated. The util is returning python code but we need it to return json so our front end can understand. Create new apiview
class IsAuthenticated()
in views.py of spotify app to call util function and return json response. Set up IsAuthenticated in the urls.py of spotify app - As soon as we get into a room, if we are the host we need to immediately authenticate our Spotify, in order to control the music. If not the host, they need to authenticate with Spotify -> Show Spotify login prompt, give authorization, take tokens and store them in the database
- Add
spotifyAuthenticated
to this.state in Room.js. Create new methodauthenticateSpotify
to send request to backend to check if current user is authenticated, but only if the situation is a host. Need to wait untilgetRoomDetails()
has run before callingauthenticateSpotify
method and therefore need to modifygetRoomDetails()
with if statement checking ifthis.state.isHost
thenthis.authenticateSpotify()
. Bindthis.authenticateSpotify
. This will redirect us to spotify authorization page, then after user authorizes us, it will redirect us spotify callback. The spotify callback will save the token and redirect us to the front end and then the front end will redirect us back to the room page
Tutorial 14 - Using the Spotify API
- Want to get the information of the currently playing song like the duration, if it's playing or not. Need to send a request to the Spotify API to get the current information about the host of the room's playback information
- In util.py create function
execute_spotify_api_request()
to send requests to Spotify - To get information about the current song create a new api view
class CurrentSong(APIView)
in views.py of spotify app. Add path for CurrentSong view in urls.py of spotify app. Test by making a new room, playing Spotify and using http://127.0.0.1:8000/spotify/current-song - Update this.state with
song
as a dictionary with all the song information in Room.js. When song ever changes,this.state.song
will be updated accordingly - Create new method
getCurrentSong()
in Room.js to fetch current song data from spotify app. CallgetCurrentSong()
after authenticating spotify and getting room details withthis.getRoomDetails();
in the constructor - Need to be constantly checking for updates, like if song is playing or paused. Spotify does not have support for public web sockets, and therefore we need to use polling method. The polling method is basically continually updating every single second. We set up an interval so that every second we update this.state in Room.js. We do this by creating method
componentDidMount()
to setthis.interval
andcomponentWillUnmount()
to clearthis.interval
in Room.js. Also need to bindthis.getCurrentSong
. Make sure song is playing when checking http://127.0.0.1:8000/room/AOCYOS - Create new component MusicPlayer.js to provide a nice music player component in Room.js. In Room.js import MusicPlayer and use
to pass song information into MusicPlayer
Tutorial 15 - Pausing & Playing Music with Spotify API
- Create methods
play_song(session_id)
andpause_song(session_id)
in utils.py of spotify app. Update urls.py with paths for PauseSong and PlaySong - Create apiviews
def PauseSong()
anddef PlaySong()
in views.py of spotify app - Modify MusicPlayer.js to use api views
PauseSong
andPlaySong
- Create methods
pauseSong
andplaySong
in MusicPlayer.js to fetch url paths. Modify icon buttons to usepauseSong
andplaySong
Tutorial 16 - Skipping Songs and Handling Votes
- Create function
skip_song(session_id)
in util.py of spotify app. Create a path forskip_song
in urls.py of spotify app - Create new apiview
SkipSong
in views.py of spotify app - Create new method skipSong in MusicPlayer.js
- To handle votes we will need to store who has voted, check how many votes there are for a room, check if the vote was for which song and current/previous song and when the vote was cast
- Update Room model in models.py of api app with current_song field
- Create new model Vote in models.py of spotify app. ForeignKey, we need to pass an instance of another object(in this case Room object) to this Vote model. This will store a reference to the Room object in our Vote. That way whenever we look at a vote we can determine which Room Object that was in, as well as access information about that Room object directly. If Room gets deleted,
models.CASCADE
will cascade down and delete anything that was referencing this room - ~
python manage.py makemigrations
and ~python manage.py migrate
- Add method
update_room_song()
to viewCurrentSong(APIView)
in views.py in spotify app.from .models import Vote
. Addself.update_room_song(room, song_id)
before returning Response in viewCurrentSong(APIView)
- Update apiview
SkipSong
in views.py of spotify app with voting for skipsong. UpdateCurrentSong
method,song['votes']
withvotes = len(Vote.objects.filter(room=room, song_id=song_id))
andsong['votes_required']
with'votes_required': room.votes_to_skip
- Update MusicPlayer.js with
{this.props.votes} / {this.props.votes_required}
votes needed
Tutorial 17 - Functional Components (useState, useEffect)
- Create new component Info.js as a functional component
- Import Info component to HomePage.js. Add button for info and Route path to info in HomePage.js. Update urls.py in frontend with new info path
UpdateBackground
- Update index.js with gradient specifications
- Update index.css with gradient
- Update index.html with gradient div