Я работал над своим веб-сайтом-портфолио и подумал, что было бы здорово разместить песню, которую я слушаю, в своем профиле. Если вы хотите добиться того же, то это руководство для вас. В этом блоге мы увидим, как использовать веб-API Spotify для создания виджета или компонента для его создания.

Обзор

API Spotify требует от клиента входа в систему для доступа к такой информации, как воспроизводимая в данный момент песня и состояния воспроизведения. Но это приведет к тому, что пользователь, посетивший веб-сайт, войдет в систему, и он отобразит песню, которую он играет, что противоречит нашей цели. Чтобы получить песню, которую мы играем, мы вручную сгенерируем и сохраним токен после авторизации. Токен авторизации/токен обновления будет использоваться для генерации токенов доступа каждый раз, когда кто-то посещает веб-сайт. Итак, приступим к реализации.

Репозиторий GitHub — Ссылка

Реализовано в Портфолио — Ссылка

Шаг 1. Настройка API Spotify

  1. Перейдите на Spotify для разработчиков — https://developer.spotify.com/.
  2. Войдите в свою учетную запись Spotify и перейдите на панель управления. Нажмите «Создать приложение».

3. Добавьте сведения о своем приложении и нажмите «Сохранить». Установите URL-адрес веб-сайта (может быть URL-адрес вашего веб-сайта) и URI перенаправления на «https://localhost:3000».

4. Теперь перейдите на страницу настроек нового приложения и запишите идентификатор клиента и секрет клиента.

Шаг 2. Создание токена обновления

Нам нужно сгенерировать токен обновления, который в дальнейшем будет использоваться для создания токенов доступа всякий раз, когда кто-то посещает веб-страницу.

  1. Создайте следующую ссылку с вашим идентификатором клиента и обновите токен.
  2. Области пользователей используются для ограничения доступа к информации; будет доступна только та информация, которой они пожелают поделиться. Чтобы использовать текущую конечную точку воспроизведения, нам нужно установить переменную области видимости в user-read-current-playing
https://accounts.spotify.com/en/authorize?client_id=<your_client_id>&response_type=code&redirect_uri=http%3A%2F%2Flocalhost:3000&scope=user-read-currently-playing

3. Нажмите «Авторизовать». Затем вы будете перенаправлены на URI перенаправления. В URL-адресе запишите значение атрибута кода. Мы будем использовать это для создания токена обновления.

http://localhost:3000/?code=<your_code>

4. Чтобы сгенерировать токен обновления, нам нужна строка в кодировке Base64, содержащая идентификатор клиента и ранее полученный секрет в следующем формате clientid:clientsecret. Сгенерировать строку онлайн можно здесь — https://www.base64encode.org/

5. Получив закодированную строку, выполните следующую команду Curl. Запустить его можно здесь — https://reqbin.com/curl

curl -H "Authorization: Basic <your base64 clientid:clientsecret>"
-d grant_type=authorization_code -d code=<your_code> -d redirect_uri=http%3A%2F%2Flocalhost:3000 https://accounts.spotify.com/api/token

В ответ вы получите файл JSON с токеном обновления. Примечание. У токенов обновления нет срока действия, их можно удалить только при отзыве доступа. Запишите токен обновления.

{
    "access_token": "BQD...woC",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "AQD...w-e4",
    "scope": "user-read-currently-playing"
}

Шаг 3. Создание компонента React

  1. Создайте компонент NowPlaying.js.
import React from 'react'

const NowPlaying = () => {
  return (
    <div>NowPlaying</div>
  )
}

export default NowPlaying

2. Настройка конечных точек токена

const NOW_PLAYING_ENDPOINT = 'https://api.spotify.com/v1/me/player/currently-playing';
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';

3. Создайте переменные для client_id, client_secret иrefresh_token, которые были записаны ранее.

const client_id = '<your_client_id>';
const client_secret = '<your_client_secret>';
const refresh_token = '<your_refresh_token>';

4. После этого нам нужно написать функцию для создания токена доступа, который генерируется каждый раз при посещении или обновлении веб-сайта. Нам также необходимо установить и использовать 'querystring' (используется для создания строки запроса URL-адреса).

npm i querystring --save-dev
//Function to generate an access token using the refresh token everytime the website is opened or refreshed
export const getAccessToken = async (client_id, client_secret, refresh_token) => {
    //Creates a base64 code of client_id:client_secret as required by the API
    const basic = Buffer.from(`${client_id}:${client_secret}`).toString('base64');

    //The response will contain the access token
    const response = await fetch(TOKEN_ENDPOINT, {
        method: 'POST',
        headers: {
        Authorization: `Basic ${basic}`,
        'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: querystring.stringify({
        grant_type: 'refresh_token',
        refresh_token,
        }),
    });

  return response.json();
};

5. Создайте функцию getNowPlaying для получения сведений о песне из конечной точки, которая сейчас воспроизводится.

//Uses the access token to fetch the currently playing song
export const getNowPlaying = async () => {
  try {
    //Generating an access token
    const { access_token } = await getAccessToken(client_id, client_secret, refresh_token);

    //Fetching the response
    const response = await fetch(NOW_PLAYING_ENDPOINT, {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    });

    //If response status > 400 means there was some error while fetching the required information
    if (response.status > 400) {
      throw new Error('Unable to Fetch Song');
    } else if(response.status === 204) { //The response was fetched but there was no content
      throw new Error('Currently Not Playing')
    }

    //Extracting the required data from the response into seperate variables
    const song = await response.json();
    const albumImageUrl = song.item.album.images[0].url;
    const artist = song.item.artists.map((artist) => artist.name).join(', ');
    const isPlaying = song.is_playing;
    const songUrl = song.item.external_urls.spotify;
    const title = song.item.name;
    const timePlayed = song.progress_ms;
    const timeTotal = song.item.duration_ms;
    const artistUrl = song.item.album.artists[0].external_urls.spotify;

    //Returning the song details
    return {
      albumImageUrl,
      artist,
      isPlaying,
      songUrl,
      title,
      timePlayed,
      timeTotal,
      artistUrl
    };
  } catch (error) {
    console.error('Error fetching currently playing song: ', error);
    return error.message.toString();
  }
};

6. Теперь давайте определим функцию NowPlaying, которая отвечает за рендеринг финального компонента.

const NowPlaying = () => {
  //Hold information about the currently playing song
  const [nowPlaying, setNowPlaying] = useState(null);

  useEffect(() => {
    const fetchNowPlaying = async () => {
    const data = await getNowPlaying();
    setNowPlaying(data)
    };

    //The spotify API does not support web sockets, so inorder to keep updating the currently playing song and time elapsed - we call the API every second
    setInterval(() => {
      fetchNowPlaying();
    }, 1000);
  }, []);
  if (nowPlaying != null && nowPlaying.title) {
    //Converting the playback duration from seconds to minutes and seconds
    secondsPlayed = Math.floor(nowPlaying.timePlayed/1000);
    minutesPlayed = Math.floor(secondsPlayed/60);
    secondsPlayed = secondsPlayed % 60;
  
    //Converting the song duration from seconds to minutes and seconds
    secondsTotal = Math.floor(nowPlaying.timeTotal/1000);
    minutesTotal = Math.floor(secondsTotal/60);
    secondsTotal = secondsTotal % 60;
  
    albumImageUrl = nowPlaying.albumImageUrl
    title = nowPlaying.title
    artist = nowPlaying.artist
  } 
  else if (nowPlaying === 'Currently Not Playing') { //If the response returns this error message then we print the following text in the widget
    playerState = 'OFFLINE' 
    title = 'User is'
    artist = 'currently Offline'
  } 
  else { //If the response wasn't able to fetch anything then we display this
    title = 'Failed to'
    artist = 'fetch song'
  }
  return (
    <div>
      <h1>Song Title: {title}</h1>
      <h2>Artist: {artists}</h2>
      <p>{minutesPlayed}:{secondsPlayed}/{minutesTotal}:{secondsTotal}</p>
     </div>
   )
 )

7. Мы успешно собрали всю необходимую информацию из API и отобразили ее на веб-странице. Используйте приведенные ниже JSX и CSS, если вы хотите воспроизвести созданный мной компонент или использовать приведенные выше данные для создания собственного виджета. Я использовал библиотеку React-icons и несколько изображений (их можно найти в репозитории GitHub).

npm i react-icons --save-dev

NowPlaying.js

import React, { useEffect, useState } from 'react';
import querystring from 'querystring';
import { Buffer } from 'buffer';
import {AiOutlinePauseCircle} from 'react-icons/ai';
import {BiErrorCircle} from 'react-icons/bi'
import {HiOutlineStatusOffline} from 'react-icons/hi'
import './styles.css'

//Setting up the Spotify API and Endpoints
const NOW_PLAYING_ENDPOINT = 'https://api.spotify.com/v1/me/player/currently-playing';
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';
const client_id = 'ab3...7c7';
const client_secret = '3eb...607';
const refresh_token = 'AQC...Few';


//Function to generate an access token using the refresh token everytime the website is opened or refreshed
export const getAccessToken = async (client_id, client_secret, refresh_token) => {
    //Creates a base64 code of client_id:client_secret as required by the API
    const basic = Buffer.from(`${client_id}:${client_secret}`).toString('base64');

    //The response will contain the access token
    const response = await fetch(TOKEN_ENDPOINT, {
        method: 'POST',
        headers: {
        Authorization: `Basic ${basic}`,
        'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: querystring.stringify({
        grant_type: 'refresh_token',
        refresh_token,
        }),
    });

  return response.json();
};

//Uses the access token to fetch the currently playing song
export const getNowPlaying = async () => {
  try {

    //Generating an access token
    const { access_token } = await getAccessToken(client_id, client_secret, refresh_token);

    //Fetching the response
    const response = await fetch(NOW_PLAYING_ENDPOINT, {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    });

    //If response status > 400 means there was some error while fetching the required information
    if (response.status > 400) {
      throw new Error('Unable to Fetch Song');
    } else if(response.status === 204) { //The response was fetched but there was no content
      throw new Error('Currently Not Playing')
    }

    //Extracting the required data from the response into seperate variables
    const song = await response.json();
    const albumImageUrl = song.item.album.images[0].url;
    const artist = song.item.artists.map((artist) => artist.name).join(', ');
    const isPlaying = song.is_playing;
    const songUrl = song.item.external_urls.spotify;
    const title = song.item.name;
    const timePlayed = song.progress_ms;
    const timeTotal = song.item.duration_ms;
    const artistUrl = song.item.album.artists[0].external_urls.spotify;

    //Returning the song details
    return {
      albumImageUrl,
      artist,
      isPlaying,
      songUrl,
      title,
      timePlayed,
      timeTotal,
      artistUrl
    };
  } catch (error) {
    console.error('Error fetching currently playing song: ', error);
    return error.message.toString();
  }
};

//Main function to process the data and render the widget
const NowPlaying = () => {

  //Hold information about the currently playing song
  const [nowPlaying, setNowPlaying] = useState(null);

  useEffect(() => {
    const fetchNowPlaying = async () => {
      const data = await getNowPlaying();
      setNowPlaying(data)
    };

    //The spotify API does not support web sockets, so inorder to keep updating the currently playing song and time elapsed - we call the API every second
    setInterval(() => {
      fetchNowPlaying();
    }, 1000);

  }, []);

  //Setting default values for the listener's current state and the duration of the song played
  let playerState = ''
  let secondsPlayed = 0, minutesPlayed = 0, secondsTotal = 0, minutesTotal = 0;
  let albumImageUrl = './images/albumCover.png'
  let title = ''
  let artist = ''

  if (nowPlaying != null && nowPlaying.title) {

    //Used while displaing a sounbar/pause icon on the widget
    nowPlaying.isPlaying ? playerState = 'PLAY' : playerState = 'PAUSE'

    //Converting the playback duration from seconds to minutes and seconds
    secondsPlayed = Math.floor(nowPlaying.timePlayed/1000);
    minutesPlayed = Math.floor(secondsPlayed/60);
    secondsPlayed = secondsPlayed % 60;

    //Converting the song duration from seconds to minutes and seconds
    secondsTotal = Math.floor(nowPlaying.timeTotal/1000);
    minutesTotal = Math.floor(secondsTotal/60);
    secondsTotal = secondsTotal % 60;

    albumImageUrl = nowPlaying.albumImageUrl
    title = nowPlaying.title
    artist = nowPlaying.artist
  } else if (nowPlaying === 'Currently Not Playing') { //If the response returns this error message then we print the following text in the widget
    playerState = 'OFFLINE' 
    title = 'User is'
    artist = 'currently Offline'
  } else { //If the response wasn't able to fetch anything then we display this
    title = 'Failed to'
    artist = 'fetch song'
  }

  //Used to set 0 as padding when the it is a single digit number
  const pad = (n) =>{
    return (n < 10) ? ("0" + n) : n;
  }

  return (
    //Depending on the value of playerState, the href, album image and icons are updated
    <a style={{textDecoration: 'none', color: 'black'}} href={playerState === 'PLAY' || playerState === 'PAUSE' ? nowPlaying.songUrl : ''}>
    <div className='nowPlayingCard'>
      {/* Albumn image and href displayed based on playerState */}
      <div className='nowPlayingImage'>
        {playerState === 'PLAY' || playerState === 'PAUSE' ? <a href={nowPlaying.songUrl}><img src={albumImageUrl} alt="Album" /></a> : <img src={albumImageUrl} alt="Album" />}
      </div>
      <div id='nowPlayingDetails'>
        {/* Song Title displayed based on playerState */}
        <div className={`nowPlayingTitle ${title.length > 15 ? 'marquee-content' : ' '}`}>
          {playerState === 'PLAY' || playerState === 'PAUSE' ? <a href={nowPlaying.songUrl}>{title}</a> : title}
        </div>
        {/* Artist displayed based on playerState */}
        <div className='nowPlayingArtist'>
        {playerState === 'PLAY' || playerState === 'PAUSE' ? <a href={nowPlaying.artistUrl}>{artist}</a> : artist}
        </div>
        {/* Song Timer displayed based on playerState */}
        <div className='nowPlayingTime'>{pad(minutesPlayed)}:{pad(secondsPlayed)} / {pad(minutesTotal)}:{pad(secondsTotal)}</div>
      </div>
      {/* Icon displayed based on playerState */}
      <div className='nowPlayingState'>
      {playerState === 'PLAY' ? <img alt='soundbar' src='./images/soundbar.gif' title='Now Listening'/> : playerState === 'PAUSE' ? <AiOutlinePauseCircle size={40} /> : playerState === 'OFFLINE' ? <HiOutlineStatusOffline size={40}/> : <BiErrorCircle size={40}/> }</div>
    </div>
    </a>
  );
};

export default NowPlaying;

styles.css

/*I have used a local ttf file to load the font, you can use any font you wish*/
@font-face {
    font-family: "louisGeorge";
    src: url("../public/fonts/LouisGeorgeCafe.ttf");
}

html {
    background-color: #D9D9D9;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: 'louisGeorge';
}

.nowPlayingCard {
    flex-shrink: 0;
    border-radius: 22px;
    border: 2px solid #000;
    box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.3);
    align-items: center;
    width: 300px;
    height: 80px;
    display: flex;
    justify-content: space-between;
    transition: all 0.5s ease;
}

.nowPlayingCard:hover {
    box-shadow: 10px 10px 0 rgba(0, 0, 0, 0.3);
    transform: translateX(-10px) translateY(-10px);
    background-color: rgba(0, 0, 0, 0.1);
    transition: all 0.5s ease;
}
  
.nowPlayingCard a {
    color: black;
    text-decoration: none;
}

.nowPlayingCard a:hover {
    color: black;
    text-decoration: underline;
}

.nowPlayingImage img {
    border-radius: 8px;
    border: 1px solid black;
    box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
    transition: all 0.5s ease;
    width: 60px;
    height: 60px;
    flex-shrink: 0;
    margin: 10px;
}

.nowPlayingImage img:hover {
    box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.3);
    transform: translateX(-3px) translateY(-3px);
    transition: all 0.5s ease;
}

#nowPlayingDetails {
    justify-content: center;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    width: 54%;
    height: 100%;
    display: flex;
    flex-direction: column;
    width: 54%;
    height: 100%;
}

.nowPlayingTitle, .playlistName {
    flex-shrink: 0;
    color: #000;
    white-space: nowrap;
    text-align: left;
    font-size: 20px;
    width: 100%;
}

.nowPlayingArtist, .playlistHeader {
    text-align: left;
    flex-shrink: 0;
    overflow: hidden;
    white-space: nowrap;
    width: 100%;
}

.nowPlayingTime {
    text-align: left;
}

.nowPlayingState {
    text-align: center;
    width: 20%;
    padding: 10px;
}

.nowPlayingState img{
    filter: invert(100%);
    width: 100%;
}

Вот и все, что мы создали виджет Spotify «В настоящее время воспроизводится».