Я работал над своим веб-сайтом-портфолио и подумал, что было бы здорово разместить песню, которую я слушаю, в своем профиле. Если вы хотите добиться того же, то это руководство для вас. В этом блоге мы увидим, как использовать веб-API Spotify для создания виджета или компонента для его создания.
Обзор
API Spotify требует от клиента входа в систему для доступа к такой информации, как воспроизводимая в данный момент песня и состояния воспроизведения. Но это приведет к тому, что пользователь, посетивший веб-сайт, войдет в систему, и он отобразит песню, которую он играет, что противоречит нашей цели. Чтобы получить песню, которую мы играем, мы вручную сгенерируем и сохраним токен после авторизации. Токен авторизации/токен обновления будет использоваться для генерации токенов доступа каждый раз, когда кто-то посещает веб-сайт. Итак, приступим к реализации.
Репозиторий GitHub — Ссылка
Реализовано в Портфолио — Ссылка
Шаг 1. Настройка API Spotify
- Перейдите на Spotify для разработчиков — https://developer.spotify.com/.
- Войдите в свою учетную запись Spotify и перейдите на панель управления. Нажмите «Создать приложение».
3. Добавьте сведения о своем приложении и нажмите «Сохранить». Установите URL-адрес веб-сайта (может быть URL-адрес вашего веб-сайта) и URI перенаправления на «https://localhost:3000».
4. Теперь перейдите на страницу настроек нового приложения и запишите идентификатор клиента и секрет клиента.
Шаг 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
- Создайте компонент 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 «В настоящее время воспроизводится».