How to Build a Music Streaming App with React using Auth0 and Cloudinary

Idris Olubisiđź’ˇ - Oct 17 '21 - - Dev Community

Since its debut, music streaming has developed dramatically, and it is currently one of the most acceptable methods to listen to music. Many streaming sites let you hear for free, which has reduced the need for piracy while ensuring that artists are compensated fairly. It's also very accessible, with numerous possibilities.

We'll learn how to use Auth0 and Cloudinary to develop a music streaming app in this tutorial.

Sandbox

This project was completed in a Codesandbox. To get started quickly, fork the Codesandbox or run the project.

GitHub Repository:

music-app-with-auth0-and-cloudinary

Created with CodeSandbox




What is Auth0?

Auth0 is an extensible authentication and authorisation system that is simple to set up. It also provides a complete identity and access management system that works right out of the box, with the ability to customise, expand, and develop new features as needed.

What is Cloudinary?

Cloudinary provides a secure and comprehensive API for uploading media files fast and efficiently from the server-side, the browser, or a mobile application. We can upload media assets using Cloudinary's REST API or client libraries (SDKs). These SDKs wrap the upload API and make it easier to integrate with websites and mobile apps.

Creating a new React project and Installing dependencies

To create a new project, we use the npx create-react-app command to scaffold a new project in a directory of our choice.

To install the dependencies we will be using the command below:

cd <project name> 

npm install @auth0/auth0-react @supabase/supabase-js bootstrap moment react-audio-player react-bootstrap react-helmet
Enter fullscreen mode Exit fullscreen mode

Once the app is created, and the dependencies are installed, we will see a message with instructions for navigating to our site and running it locally. We do this with the command.

    npm start
Enter fullscreen mode Exit fullscreen mode

React.js will start a hot-reloading development environment accessible by default at http://localhost:3000

Setting Up Auth0 Account

Kindly visit Auth0 to sign up if you haven’t or login to dashboard, click the Applications dropdown then application and finally click the Create Application button as shown below:

cloudinary demo

We can now create our application as shown below:

cloudinary demo

As shown below, we have successfully created our application, but we need to set URLs to point back to our application.

cloudinary demo

Scroll down to the Application URIs section and set the following

Allowed Callback URLs = https://de7pd.csb.app
Allowed Logout URLs = https://de7pd.csb.app
Allowed Web Origins= https://de7pd.csb.app

Replace https://de7pd.csb.app with our application URL or http://localhost:3000 that we set up earlier. We will be making use of Domain and Client ID in our application later in this tutorial.

Setting up our Application UI and Auth0 integration.

Let’s import and set up our application to use the bootstrap dependencies we installed. Navigate to public/index.html update the file by linking the bootstrap CSS and js with the snippet below:

  • Adding CSS reference
<!DOCTYPE html>
    <html lang="en">
      <head>
        //...

        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
          integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU"
          crossorigin="anonymous"
        />
        <title>Music Streaming App</title>
      </head>
      <body>
        //...
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode
  • Adding JS reference
    <!DOCTYPE html>
    <html lang="en">
      <head>
      //...
      </head>
      <body>
        <noscript>
          You need to enable JavaScript to run this app.
        </noscript>
        <div id="root"></div>
        <script
          src="https://unpkg.com/react/umd/react.production.min.js"
          crossorigin
        ></script>
        <script
          src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"
          crossorigin
        ></script>
        <script
          src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js"
          crossorigin
        ></script>
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

We require the user interface to stream music on the home page. We will create this by updating the app.js file to a component. Since it is the React tradition to work with a component structure, we will create a folder called components in the src folder and create header.js, music.js, and musicList.js components.

In the components/music.js file, let us update it with the snippet below:

    import ReactAudioPlayer from "react-audio-player";
    import moment from "moment";

    export default function Music({ musicList, index }) {
      return (
        <div className="col-md-4">
          <div className="card p-3 mb-2" key={index}>
            <div className="d-flex justify-content-between">
              <div className="d-flex flex-row align-items-center">
                <div className="icon">
                  {" "}
                  <i className="bx bxl-mailchimp"></i>{" "}
                </div>
                <div className="ms-2 c-details">
                  <h6 className="mb-0">{musicList.name}</h6>{" "}
                  <span>{moment(musicList.created_at).format("MMMM Do YYYY")}</span>
                </div>
              </div>
              <div className="badge">
                {" "}
                <span role="img" aria-label="">
                  Hot 🔥
                </span>{" "}
              </div>
            </div>
            <div className="mt-2">
              <h4 className="heading">{musicList.title}</h4>
              <div className="mt-2">
                <ReactAudioPlayer src={`${musicList.url}`} controls />
              </div>
            </div>
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we created a component for a single music card with musicList and index props. We also imported ReactAudioPlayer and moment for the audio player and upload date formatting, respectively.

Inside the musicList.js component, we will update it with the snippet below by importing the music component and iterating through the sample musicList array.

    import Music from "./music";

    export default function App() {
      const musicList = [
        {
          name: "olanetsoft",
          title: "Bang Bang",
          url: "https://res.cloudinary.com/demo/video/upload/dog.mp3",
          created_at:"2021-10-04T23:30:01.000Z",
        }
      ]
      return (
        <div className="row">
          {musicList.map((m, key) => (
            <Music musicList={m} index={key} />
          ))}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Let us update the header.js component we created earlier with the snippet below:

    import { Button } from "react-bootstrap";

    export default function Header() {

      return (
        <div className="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
          <h5 className="my-0 mr-md-auto font-weight-normal">
            Music Streaming App with Auth0 and Cloudinary
          </h5>
          <nav className="my-2 my-md-0 mr-md-3">
            <a className="p-2 text-success" href="/">
              Home
            </a>
            <a className="p-2 text-danger" href="/">
              Trending
            </a>
            <a className="p-2 text-info" href="/">
              Top Songs
            </a>
          </nav>
            <Button
              id="btnUpload"
              className="btn margin"
              variant="primary"
            >
              Upload Song
            </Button>
        </div>
      );
    } 
Enter fullscreen mode Exit fullscreen mode

We can now update our src/app.js file as shown below:


    import MusicList from "../components/musicList";
    import "./styles.css";

    export default function App() {
      return (
        <div className="container mt-5 mb-3">
          <Header />
          <MusicList />
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The current user interface doesn’t look aesthetically pleasing, we’ll add some styling with CSS. We will update src/styles.css file with the following content in this GitHub Gist.

Our application should now look like this on http://localhost:3000/:

cloudinary demo

We're currently working with sample data, which is not ideal. We should be able to upload and stream songs that others have uploaded.

We'll use Auth0 to track who's uploading new songs, and then we'll use Cloudinary to do the actual uploading before saving it to the database.

Setting up Auth0 in our Application

Let’s create .env file in root of our project and populate it with Domain and Client ID from our Auth0 dashboard with the snippet below:

    AUTH0_DOMAIN=dev-9hbpo12k.us.auth0.com
    AUTH0_CLIENT_ID=tdYpNQ8Qqjymi0dOC7wZdGGWlYCN6FR3
Enter fullscreen mode Exit fullscreen mode

Inside src/index.js let us import Auth0Provider and setup our application with the snippet below:

    import { StrictMode } from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    import { Auth0Provider } from "@auth0/auth0-react";

    const domain = process.env.AUTH0_DOMAIN;
    const clientId = process.env.AUTH0_CLIENT_ID;

    const rootElement = document.getElementById("root");

    ReactDOM.render(
      <StrictMode>
        <Auth0Provider
          domain={domain}
          clientId={clientId}
          redirectUri={window.location.origin}
        >
          <App />
        </Auth0Provider>
        ,
      </StrictMode>,
      rootElement
    );
Enter fullscreen mode Exit fullscreen mode

We can now create login-button.js , logout-button.js and loading.js component inside components folder respectively, using the snippet below:

Inside components/login-button.js

    import { useAuth0 } from "@auth0/auth0-react";
    import { Button } from "react-bootstrap";

    export default function Login() {
      const { loginWithRedirect } = useAuth0();
      return (
        <Button
          id="btnLogin"
          className="btn margin"
          onClick={() => loginWithRedirect()}
          variant="primary"
        >
          Upload Music
        </Button>
      );
    }
Enter fullscreen mode Exit fullscreen mode

components/logout-button.js

    import { useAuth0 } from "@auth0/auth0-react";
    import { Button } from "react-bootstrap";

    export default function Logout() {
      const { logout } = useAuth0();
      return (
        <Button
          id="btnLogin"
          className="btn margin"
          onClick={() => logout()}
          variant="danger"
        >
          Logout
        </Button>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Then inside components/loading.js

    import "../src/styles.css";
    export default function Loading() {
      return <div className="spinner"></div>;
    }
Enter fullscreen mode Exit fullscreen mode

We may proceed to import the login and logout component inside header.js file created earlier as shown below:

    import { useState, useEffect } from "react";
    import { Button } from "react-bootstrap";

    import { useAuth0 } from "@auth0/auth0-react";
    import Login from "../components/login-button";
    import Logout from "../components/logout-button";

    export default function Header() {
      const { isAuthenticated } = useAuth0();

      return (
        <div className="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
             {/*  */}
          {isAuthenticated ? (
            <>
              <div>
                <Button
                  id="btnUpload"
                  className="btn margin"
                  variant="primary"
                >
                  Upload Song
                </Button>
                &nbsp;&nbsp;
                <Logout />
              </div>
            </>
          ) : (
            <Login />
          )}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Updating src/app.js

    //...
    import Loading from "../components/loading";

    export default function App() {
      const { isLoading } = useAuth0();
      if (isLoading) {
        return <Loading />;
      }
      return (
        //....
      );
    }
Enter fullscreen mode Exit fullscreen mode

Let us test our application, and we should have something similar to what we have below after clicking the Upload Song button.

cloudinary demo

cloudinary demo

In the screenshot above, we've successfully logged in, and you'll notice that the UI in the header has changed to include a logout button.

Configuring Cloudinary and DB to Upload Songs

We will be using Cloudinary’s upload widget because of its ability lets us upload media assets from multiple sources including Dropbox, Facebook, Instagram,.

Create a free cloudinary account to obtain your cloud name and upload_preset.

Upload presets allow us to centrally define a set of asset upload choices rather than providing them in each upload call. A Cloudinary cloud name is a unique identifier associated with our Cloudinary account.

First, from a content delivery network (CDN) we will include the Cloudinary widget’s JavaScript file in our index.js located in src/app.js. We include this file using the react-helmet’s <Helmet> component, which lets us add data to the Head portion of our HTML document in React.

    //..

    import "./styles.css";

    import { Helmet } from "react-helmet";

    export default function App() {
      //...
      return (
        <div className="container mt-5 mb-3">
          <Helmet>
            <meta charSet="utf-8" />
            <script
              src="https://widget.Cloudinary.com/v2.0/global/all.js"
              type="text/javascript"
            ></script>
        //...
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The widget requires our Cloudinary cloud_name and uploadPreset. The createWidget() function creates a new upload widget and on the successful upload of either a video or audio, we assign the public_id of the asset to the relevant state variable.

To get our cloudname and uploadPreset we follow the steps below:

The cloud name is obtained from our Cloudinary dashboard as shown below.

cloudinary demo

An upload preset can be found in the “Upload” tab of our Cloudinary settings page, which we access by clicking on the gear icon in the top right corner of the dashboard page.

cloudinary demo

We then click on the Upload tab on the settings page:

cloudinary demo

We scroll down to the bottom of the page to the upload presets section, where we see our upload preset or the option to create one if we don't have any.

Let us update our components/header.js with the snippet below:

Let us open our app in the browser and click the Upload Song button; we should see something like this:

cloudinary demo

We can further customise the widget with more information in this documentation.

We have successfully configured and setup cloudinary in our application, but we will also integrate a supabase database to save all of the songs that users have uploaded.

let us create client.js to integrate supabase with the sippet below:

    import { createClient } from "@supabase/supabase-js";

    const URL = "https://kpriwlucrliyacagwjvk.supabase.co";
    const ANNON_PUBLIC_SECRET = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzMzM2NzU2OSwiZXhwIjoxOTQ4OTQzNgY5fQ.uBBXtyxbwKixUgql4tiYUsqOgSPyB4mLSc2kybqPCPI";

    export const supabase = createClient(
      URL,
      ANNON_PUBLIC_SECRET
    );
Enter fullscreen mode Exit fullscreen mode

To get the url and annon public key, create a supabase account, start a new project, go to settings then Api tab.

We'll create a new table named songs with columns for url, name, and title by going to the table editor tab on the sidebar. Let’s ensure the column type is text for all the columns created.

After successfully creating our table, let's update the components/header.js file with the snippet below:

In the preceding code line,

  • We created state variables that are updated when the upload is completed.
  • We created a function called createSong that connects to the songs table in Supabase, and then we input our data.
  • We then verify the variables to ensure they aren't undefined before using the createPost method to save them in the database.

Let's update the musicList component to retrieve all uploaded songs with the snippet shown below:

    import { useState, useEffect } from "react";
    import { supabase } from "../client";
    import Music from "./music";

    export default function App() {
      const [musicList, setMusicList] = useState([]);

      useEffect(() => {
        fetchSongs();
      }, []);

      async function fetchSongs() {
        const { data } = await supabase.from("songs").select();

        setMusicList(data);
      }

      return (
        <div className="row">
          {musicList.map((m, key) => (
            <Music musicList={m} index={key} />
          ))}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Voila 🥳 We are all set; we can now successfully upload songs, stream songs, etc.

cloudinary demo

Conclusion

This article explains how to use Auth0 and Cloudinary to build a music streaming app utilising Cloudinary's widget capability.

Resources

Content created for the Hackmamba Jamstack Content Hackathon using Auth0 and Cloudinary

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .