Радиоканал Макс FM, который я люблю слушать, доступен только в Анкаре, где я живу, и на сайте. Мне стало известно, что у этого радиоканала нет официального плейлиста на Spotify, и я хотел его создать (неофициально). Технологии, которые я использовал для этого проекта:
- JavaScript
- Спотифай API
- Сайт Макс FM
- NodeJS
- Кукольник, dotenv, expressJS, Axios, fs
Общие наброски программы таковы:
1- Узнайте имя и исполнителя песни, играющей мгновенно, из веб-плеера радио.
2- Чтобы выполнить процесс авторизации, чтобы иметь возможность использовать API Spotify.
3- Взаимодействие с API Spotify для добавления песни, полученной на первом этапе, в список воспроизведения.
4- Удаление самой старой добавленной песни из плейлиста, чтобы в списке всегда было 100 песен.
Давайте перейдем к этапам разработки проекта, который постоянно обновляет финальную версию плейлиста и сохраняет его постоянным на уровне 100 песен.
Прежде чем перейти к запросам API Spotify, я хочу начать с парсинга веб-страниц по радиоканалу. Для этого процесса мне нужен бот, который будет получать доступ к проигрывателю Max Fm через равные промежутки времени и узнавать информацию о мгновенно воспроизводимой песне. Что мне нужно, так это Puppeteer, который позволяет тестировать QA и автоматизировать Chromium. После HTTP-запроса GET, который я сделал веб-плееру радио с помощью Puppeteer, я выполнил операции синтаксического анализа в ответе и определил название песни и имя исполнителя и получил его в качестве вывода функции.
Дальше более сложная часть. API Спотифай! Используя API Spotify, мы можем выполнять множество операций с кодированием. Чтобы воспользоваться ими, нам сначала нужно добавить новое приложение, войдя в нашу существующую учетную запись через Панель управления Spotify, чтобы Spotify мог общаться с нами через обратный вызов, который мы интегрировали в это приложение. . Кроме того, на нашей панели управления есть информация, такая как CLIENT_ID, CLIENT_SECRET и ENDPOINT_URI, которая поможет нам во всем этом процессе. CLIENT_ID и CLIENT_SECRET предоставляются нам Spotify, но мы должны сами установить ENDPOINT_URI. Поскольку написанная мной программа будет работать только в моей собственной тестовой среде, я использую здесь значение localhost. Для этого я установил URI в виде http://localhost:8888/callback для работы на моем локальном хосте и сохраняю всю эту информацию на панели инструментов. Я храню эту информацию в своем файле .env в каталоге моего проекта. Чтобы получить доступ к файлу .env через мой основной файл и сохранить информацию в виде переменной, я импортирую библиотеку dotenv в свою программу и извлекаю информацию таким образом. Функциональная библиотека, когда вам нужно прочитать переменные окружения из внешнего файла. Последнее дополнение будет к плейлисту, над которым я буду работать. Я также сохраняю значение идентификатора моего плейлиста в этом файле .env.
Прежде всего, нам нужна авторизация, чтобы сделать запрос к Spotify, и мы продолжим, выбрав один из 4 различных потоков, которые предлагает нам Spotify. Поток, который я использую для этого проекта, называется Поток кода авторизации. Поток кода авторизации — это поток, который позволяет клиенту получить доступ к ресурсам пользователя, обеспечивает дополнительный уровень безопасности с помощью секретного ключа на стороне сервера и предоставляет возобновляемый токен доступа. (ЮВТ)
Нам нужно приложение для использования потока кода авторизации. Поскольку это приложение будет работать только на локальном хосте, я создаю промежуточное ПО с ExpressJs и таким образом управляю трафиком между Spotify и нужным приложением.
Функции, которые я отредактировал как async-await, ждут друг друга по очереди, когда запускается основная функция.
Мы заполняем файл .env информацией, которая нам понадобится позже, и добавляем наш файл js во входную часть как глобальную переменную с dotenv. Не забывайте, что вы должны ввести свои переменные среды в местах, обозначенных «XXX» в файле .env!
require('dotenv').config(); const PLAYLIST_ID = process.env.PLAYLIST_ID; const CLIENT_ID = process.env.CLIENT_ID; const CLIENT_SECRET = process.env.CLIENT_SECRET; const REDIRECT_URI = process.env.REDIRECT_URI;
Чтобы протестировать Express, я делаю запрос по своему корневому пути и получаю доступ к своему результату по адресу http://localhost:8888/. (Совет: вы можете сделать запрос в командной строке с помощью curl http://localhost:8888
или получить прямой доступ с помощью своего любимого веб-браузера.)
app.get('/', (req, res) => { const data = { name: 'michael', isActive: true, }; res.json(data); });
Увидев ответ на мои запросы на моем локальном хосте, я запущу процесс авторизации Spotify и отправлю HTTP-запрос GET на конечную точку /authorize
. На этом этапе важны обязательные и необязательные параметры, которые мы должны запрашивать при редактировании потока кода авторизации. Мы настраиваем наш запрос с client_id
, redirect_uri
, response_type
, scope
и state
используя queryString для более организованного и единого использования.
app.get('/login', (req, res) => { const state = generateRandomString(16); res.cookie(stateKey, state); const scope = 'user-read-private user-read-email playlist-modify-private playlist-modify-public'; const queryParams = querystring.stringify({ client_id: CLIENT_ID, response_type: 'code', redirect_uri: REDIRECT_URI, state: state, scope: scope, }); res.redirect(`https://accounts.spotify.com/authorize?${queryParams}`); });
После запроса нас перенаправляют на экран входа в Spotify, и после входа в систему и подтверждения мы сталкиваемся с ошибкой, говорящей о том, что мы получили /callback error
. Поскольку мы еще не создали обработчик маршрута /callback, получение этой ошибки является нормальным явлением. В ответе в адресной строке показывается код, который мы можем обменять на нужный нам access_token. После этого мы начинаем создавать обработчик маршрута /callback.
app.get('/callback', (req, res) => { const code = req.query.code || null; axios({ method: 'post', url: 'https://accounts.spotify.com/api/token', data: querystring.stringify({ grant_type: 'authorization_code', code: code, redirect_uri: REDIRECT_URI }), headers: { 'content-type': 'application/x-www-form-urlencoded', Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`, }, }) .then(response => {...}
Мы используем code
variable для доступа к коду, который мы заменим на access_token с помощью параметра запроса. Мы создаем запрос HTTP POST, который мы будем делать с помощью Axios. Чтобы воспользоваться преимуществами Axios, нам нужно загрузить его как зависимость и добавить в наш файл javascript. После запроса мы можем получить доступ к информации access_token
, refresh_token
, token_type
и expires_in
в HTTP-ответе, которую мы можем проверить через http://localhost:8888/callback. Здесь вы должны быть осторожны, извлекая информацию access_token и refresh_token из HTTP-ответа и сохраняя ее как переменную, потому что она понадобится нам в каждой операции.
Я предпочитаю записывать информацию access_token и refresh_token в ответ на .txt
file, используя fs, модуль в NodeJS, который позволяет мне работать с файловой системой. Так как access_token длится 1 час, я завершил процессы чтения и записи файлов, отправив HTTP-запрос на обновление access_token с информацией о моем refresh_token каждый 1 час. Асинхронная функция, которую я создал для refresh_token, почти такая же, как обработчик маршрута /callback.
const refresh = async () => { let buffer = fs.readFileSync("refresh.txt"); let ref_file_token = buffer.toString(); refresh_token = ref_file_token; axios({ method: 'post', url: 'https://accounts.spotify.com/api/token', data: querystring.stringify({ }), headers: { grant_type: 'refresh_token', refresh_token: refresh_token 'content-type': 'application/x-www-form-urlencoded', Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`, }, }).then(response => {...}
Пришло время сформировать нашу основную функцию с помощью информации, полученной с помощью веб-скрапинга, и нашего access_token, который необходим нам для отправки запроса в Spotify! К сожалению, невозможно напрямую добавить этот трек в список воспроизведения в нашей учетной записи Spotify с информацией о названии песни и имени исполнителя, которую мы получили с помощью веб-скрейпинга. Spotify нужен уникальный идентификатор (uris) этой песни, чтобы добавить песню в список воспроизведения. На этом этапе наши действия таковы:
1- Войдите в нашу учетную запись Spotify,
2- Используя информацию access_token, полученную после авторизации, чтобы получить идентификатор песни для Spotify с помощью метода HTTP GET,
3- Чтобы снова сделать запрос к Spotify с помощью метода HTTP POST, чтобы добавить песню в список воспроизведения с уникальным значением идентификатора.
Пока наш основной рабочий процесс такой, я опционально отредактировал функцию задержки внутри моей основной функции (чтобы сделать запрос в определенное время (10 минут и т.д.)) и добавлять в плейлист с каждой новой песней, в то же время удаление самой старой песни из списка в качестве даты добавления в плейлист. Таким образом, я отредактировал запрос на выборку с помощью метода HTTP DELETE, чтобы список оставался актуальным и постоянным на уровне 100 дорожек.
Блок кода ниже относится к функции задержки, которую я создал.
const delay = ms => new Promise(res => setTimeout(res, ms)) ... await delay(...)
С помощью моей функции задержки я предотвращаю возможные конфликты моих асинхронных функций, настраивая время ожидания внутри программы.
Выше я упоминал, что храню информацию access_token и refresh_token, возвращенную после авторизации, в файлах tokens.txt
и refresh.txt
. Когда я запускаю свою основную функцию, прежде чем войти в цикл while и сделать запрос к API Spotify, мне нужно прочитать информацию из tokent.txt и refresh.txt с помощью fs. Затем я присвоил эти значения моей переменной auth_token.
const fs = require("fs"); ... const buffer = fs.readFileSync("tokens.txt"); const file_token = buffer.toString(); auth_token = file_token;
Еще одна вещь, которую я хочу сделать перед входом в цикл while, - это получить уникальные идентификаторы песен, которые есть в моем существующем плейлисте. Я храню эти идентификаторы в массиве. Чтобы получить доступ к массиву, который я определил как локальную переменную, я передаю идентификаторы в другой пустой массив, который я определил вне функции. Теперь, когда мы выполнили шаги, предшествующие циклу while, мы можем приступить к написанию блоков кода, которые будут выполняться в цикле while.
В приведенном ниже блоке кода показан вызов функции очистки после времени, которое я добавил после входа в цикл while (600000 мс = 10 минут), и синхронизация вывода функции с выходной переменной. (Я не хочу делиться деталями своей функции очистки, знаете ли, я разработал эту часть для Max FM. Вы можете разработать функцию очистки, какую захотите.)
while (true) { await delay(600000); outputs = await scrape() ... }
После определения 10 минут в миллисекундах и добавления моей функции задержки во входные данные с помощью await, я снова вызываю свою функцию очистки с помощью await и назначаю вывод функции моей переменной с именем outputs
, которую я установил как пустую до этого. Важная часть на этом этапе заключается в том, что нам нужно создать нашу основную функцию как асинхронную функцию, чтобы использовать ожидания. В той части, где я присваиваю ответ переменной outputs
, я создаю условие if-else, а также создаю пустую переменную с именем latestSong
перед циклом while. Если ответ, возвращенный функцией очистки в предыдущем цикле, совпадает с ответом, возвращенным в следующем цикле, наш текущий цикл заканчивается здесь. Если наш ответ изменился (означает, что песня изменена), мы входим в блок else и, получив ответ от функции очистки, делаем запрос уникального идентификатора к API Spotify с помощью принести. На данный момент наш блок кода выглядит примерно так:
let spotiUrl = `https://api.spotify.com/v1/search?q=${outputs}&type=track&limit=1`; let method = "GET"; try{ fetch(spotiUrl, { method, headers: { "Authorization": `Bearer ${auth_token}`, Accept: 'application/json', 'Content-Type': 'application/json' } }).then(response =>...
На этом этапе есть важная часть, о которой я должен упомянуть. Запрос HTTP GET, который я сделал выше, также является первым запросом, который я сделал в цикле while. Это та часть, где я проверю действительность моего access_token, который стал непригодным для использования через 1 час, и если он истечет, я получу новый! Вот почему я проверяю ошибки перед почтовым запросом, и если срок действия моего токена истекает, я вызываю функцию refresh()
, которую я создал вне основной функции, и именно так я обновляю свой access_token.
... if(err == {"error":{"status":401,"message":"The access token expired"}} || "TypeError: Cannot read properties of undefined (reading 'items')") { await refresh(); await delay(5000); ... }
Я добавляю приведенный выше блок кода в блок catch моего HTTP-запроса GET с уникальным идентификатором, который я написал в блоке try-catch, и продолжаю использовать свою функцию задержки для поддержания порядка.
С уникальным значением идентификатора песни, которое мы получили от Spotify, пришло время отправить HTTP-запрос POST в Spotify, чтобы добавить песню в список воспроизведения с этим идентификатором. На данный момент нам нужен PLAYLIST_ID, который я сохранил в своем файле .env. Кроме того, для запроса HTTP POST у нас также есть раздел тела с заголовком в запросе на выборку. В теле есть наш ответ, то есть наш уникальный идентификатор на наш запрос HTTP GET, и значение позиции, которое позволяет нам добавить песню в начало списка.
let spotiUrl = `https://api.spotify.com/v1/playlists/${PLAYLIST_ID}/tracks`; let method = "POST"; fetch(spotiUrl, { method, headers: { "Authorization": `Bearer ${auth_token}`, Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({"uris": [`spotify:track:${outputsSpotiId}`],"position": 0}) }).then(response => ...
Последняя часть, с которой я свяжусь со Spotify, — это запрос на удаление самой старой песни в списке воспроизведения, что я сделаю с помощью метода HTTP DELETE. На этом этапе я могу использовать информацию о песне из плейлиста, которую я взял и добавил в свой массив перед входом в цикл while. Я делаю свой HTTP-запрос DELETE с этой переменной, приравнивая уникальный идентификатор самой старой датированной песни, который я получил путем извлечения из входящей информации, к переменной с именем removeSongId в каждом раунде. Также при выполнении моего HTTP POST-запроса при добавлении в начало списка с параметром «позиция: 0»; Используя параметр position : 99
в моем HTTP-запросе DELETE, я гарантирую, что достигну последнего элемента списка воспроизведения вместе с уникальным идентификатором.
let spotiUrl = `https://api.spotify.com/v1/playlists/${PLAYLIST_ID}/tracks`; let method = "DELETE"; fetch(spotiUrl, { method, headers: { "Authorization": `Bearer ${auth_token}`, Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({"uris": [`spotify:track:${removeSongId}`],"position": 99}) }).then(response => ...
Вот и все!
Плейлист, который я создал: |Live| Макс. Fm 95,8
Окончательная версия моего существующего плейлиста выглядит следующим образом:
Я надеюсь, что это поможет и вам. Я был бы очень рад, если бы вы могли поделиться со мной местами, где я упускаю, ошибаюсь или могли бы быть более продуктивными следующим образом. Не забудьте поставить лайк и подписаться! Приятного прослушивания и приятного кодирования!