Радиоканал Макс 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 => {...}

Мы используем codevariable для доступа к коду, который мы заменим на 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 в ответ на .txtfile, используя 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

Окончательная версия моего существующего плейлиста выглядит следующим образом:

Я надеюсь, что это поможет и вам. Я был бы очень рад, если бы вы могли поделиться со мной местами, где я упускаю, ошибаюсь или могли бы быть более продуктивными следующим образом. Не забудьте поставить лайк и подписаться! Приятного прослушивания и приятного кодирования!