使用ChatGPT根据您的听歌历史推荐歌曲。

罗汉库玛

随着人工智能的最新发展,创建能够提供个性化反馈和建议的系统比以往任何时候都更容易。此外,由于Spotify使其所有用户的听歌数据变得更加便捷,我们可以利用ChatGPT和Spotify的免费公共API服务创建一个非常简单的网站,根据您自己的听歌历史提供相当准确的推荐。

在这篇博客文章中,我们将经过完整的过程来创建一个功能性网站,该网站将执行以下操作:

  1. 使用Spotify验证用户。
  2. 访问并展示用户最常播放的曲目和艺术家。
  3. 使用即时工程原则从OpenAI的GPT-3.5-Turbo模型生成准确有效的推荐响应。
  4. 解码由GPT-3.5-Turbo输出的歌曲列表,并将它们发送回Spotify以获取歌曲信息。
  5. 展示推荐音乐清单给用户,并提供链接让他们在Spotify上进行播放。

我们将忽略前端的大部分内容,并设置基本框架应用程序,该应用程序可以使用CSS样式和更多的React组件进行编辑和完善。

如果您想看到该项目的完整发展版本,可在此查看我的网站。

虽然这听起来像一个复杂的项目,但实际上只需要写几行代码就可以完成。

首先,我们需要设置Spotify API。我们可以通过导航到下面的链接并设置一个新的应用程序来完成这个过程。现在,您输入的大部分信息并不重要。唯一重要的是“重定向URI”字段。

当用户在我们网站上点击登录按钮时,他们将被重定向到Spotify域下的网站。一旦用户通过Spotify网站允许我们网站的权限,他们将被重定向到我们的重定向URI,这应该是将他们带回我们的网站。目前,由于我们正在本地工作,我们可以将其设置为“http://localhost:3000”。

ChatGPT中文站

需要注意的是,在撰写此博客时,Spotify的API服务完全免费,这使其非常适合用于小型副业或教育目的。

一旦我们使用Spotify初始化应用程序,我们可以导航到设置。在那里,我们将看到两个重要项目:客户端ID和应用程序状态。

  1. 客户端 ID 是我们用作密钥的内容,可让我们连接到 Spotify 的认证服务。
  2. 应该默认设置为开发模式的应用状态表明该网站仅允许手动注册该应用的用户进行身份验证。默认情况下,这仅包括您用于创建应用的自己的个人帐户。如果想要添加一些朋友,您可以在用户管理选项卡中手动添加他们的电子邮件。但是,如果您希望您的网站可以公开访问,您需要提交扩展请求,这可能需要多达6周时间由Spotify处理。

为了开始,我们将初始化一个新的React项目。个人而言,我喜欢开始使用create-react-app模板,因为它是最容易使用的,但任何模板都可以。为此,请导航到终端和您希望项目所在的目录,然后输入以下内容。

npx create-react-app song-recommender

以下是简体中文翻译,保留 HTML 结构: 这将初始化一个名为“歌曲推荐者”的项目,并加载许多通用的依赖库,使我们能够立即开始网站的开发。首先,我们输入:

npm start
npm i axios

我们可以通过删除 App.css、App.test.js、index.css、logo.svg、setupTests.js 和 reportWebVitals.js 文件来清理项目。我们还可以修改 index.js 代码以使其退出安全模式,这可以防止我们的网站进行额外的 API 调用。

index.js 应该长这样:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);

最后,我们可以将App.js文件中的所有内容删除,使其看起来像这样:

import React from "react"
import axios from "axios"

const App = () => {
return (
<div> Hello World </div>
)
}

export default App

如果你的项目正常工作,你应该在“http://localhost:3000”上,在浏览器上看到“你好,世界”的字样。太棒了!我们终于可以开始了。

我们要做的第一件事是设置一个基本的登录界面,以帮助我们访问用户的数据。Spotify 的 API 有很多关于不同类型认证流程的文档,你可以选择使用任何一种。不同的流程具有不同的安全性和复杂度。为了这个项目,我们将使用 Implicit Grant Flow,这是最容易设置的其中一种。但是,将代码重构为使用 Authorization Code with PKCE 也不是太困难,这是最安全的方法。如果你想了解更多,请阅读在此阅读Spotify的官方文档。

隐式授权流程的基本思想是,一旦用户通过Spotify的门户登录,Spotify将通过URI将用户发送回我们的网站并附带附加信息(称为令牌)。我们随后可以解析这个新的URI,并保存该令牌以便使用我们需要的任何API调用。

我们将从创建一个简单的登录界面组件开始(它只是一个简单的按钮,上面写着“登录到 Spotify”)。我们将通过创建一个 Login.js 文件并键入以下代码来完成:

import React from 'react'

const LoginButton = () => {
return (
<a> Login to Spotify </a>
)
}

export default LoginButton

为了在我们的网站上显示登录按钮,我们需要修改App.js及其导入语句。我们还将导入useState和useEffect钩子,稍后我们将使用它们。最终,它应该看起来像这样:

import React, { useState, useEffect } from "react"
import LoginButton from "./Login"

const App = () => {
return (
<LoginButton />
)
}

export default App

接下来,我们会为我们的登录按钮添加功能。如果你想了解Implicit Grant Flow的工作方式,可以在Spotify的文档中查看更多信息。要为按钮添加功能,我们需要为按钮添加适当的href标签,以便它链接到正确的端点。我们可以通过定义以下常量来实现这一点。

const CLIENT_ID = "YOUR CLIENT ID FROM SPOTIFY API WEBSITE"
const REDIRECT_URI = "http://localhost:3000"
const AUTH_ENDPOINT = "https://accounts.spotify.com/authorize"
const RESPONSE_TYPE = "token"
const SCOPES = ['user-top-read']
const loginEndpoint = `${AUTH_ENDPOINT}?client_id=${CLIENT_ID}&scope=${SCOPES}&redirect_uri=${REDIRECT_URI}&response_type=${RESPONSE_TYPE}&show_dialog=true`

我们添加‘user-top-read’范围,因为我们需要用户的许可来查看他们的听歌历史(包括他们的顶级艺术家和曲目)。我们可以通过修改Login.js中的标签将其连接到我们的登录按钮。

<a href={loginEndpoint}> Login to Spotify </a>

现在,如果您在我们网站上单击按钮,应该会将您导航到Spotify的身份验证门户,然后将您重定向回网站。您还应该在浏览器上看到一个修改过的URI,其中包含一个特殊的访问令牌。

ChatGPT中文站

接下来,我们需要从URI中解码此令牌并使用它来进行API调用。我们可以通过将以下代码添加到App.js中来解码访问令牌:

// ...

const App = () => {

const [token, setToken] = useState('')

const getTokenFromURI = () => {
const oldToken = window.localStorage.getItem("token")
if (oldToken) { return oldToken }

const hash = window.location.hash
if (!hash) { return '' }

const newToken = hash.substring(1).split("&").find(elem => elem.startsWith("access_token")).split("=")[1]
return newToken
}

useEffect(() => {
setToken(getTokenFromURI())
window.location.hash = ""
window.localStorage.setItem("token", token)
}, [])

// ...

}

export default App

简而言之,我们正在查看URI,将常量哈希定义为“#”之后的URI子字符串。如果URI中有“#”,而我们尚未有先前的令牌,则解码哈希并更新我们的状态变量令牌。通过将此函数放置在没有依赖项的useEffect钩子中,我们确保这仅在打开页面时运行一次,而不是在每个帧上运行。如果您想了解更多关于useState和useEffect钩子的信息,可以在此查看React的官方文档。

由于我们有一个用于存储用户 Spotify 令牌的状态变量,因此我们有效地有了一种告知用户已经成功验证的方式。利用这一点,我们可以根据用户是否登录创建两个不同的视图。为了实现这一点,我们修改 App.js 代码,包括以下内容:

// ...
import Navbar from "./Navbar"

const App = () => {

// ...

if (token === '') {
return (
// Can replace with more complex login screen component
<LoginButton />
)
}

return (
<div>
<Navbar />
<MusicList />
</div>
)
}

export default App

我们还需要相应地制作 Navbar.js 组件。为了这篇博客文章的缘故,我们将制作一个非常简单的 Navbar 组件,屏幕上只有最少的信息。如果您想了解更多关于 CSS 样式和如何实现良好的前端 web 开发实践的内容,您可以在 freeCodeCamp 的 React 应用样式化文章中阅读更多内容。

如果您想查看该项目的完整版本,可以导航到这里。

在构建我们的导航栏组件之前,我们需要在App.js文件中声明以下状态变量,并将它们作为属性传递给其他组件:

// Imports

const App = () => {

// ...

const [trackList, setTrackList] = useState([])
const [artistList, setArtistList] = useState([])
const [recList, setRecList] = useState([])
const [searchType, setSearchType] = useState('tracks')
const [searchLength, setSearchLength] = useState('short_term')

// ...

return (
<div>
<Navbar
setSearchType = {setSearchType}
setSearchLength = {setSearchLength}
setToken = {setToken}
/>
</div>
)

}

export default App

我们的基础Navbar.js组件将包括以下功能:

  1. 下拉菜单可以选择音轨、艺术家和推荐,这将是网站的三个主要功能。
  2. 下拉选择短期,中期和长期之一,这将确定Spotify将查看的时间范围。
  3. 登出按钮

我们可以使用以下代码初始化这3个特性并设置它们的功能,使用之前声明的状态变量。

import React from 'react'

const Navbar = (props) => {
return (
<div>
<select onChange={e => props.setSearchType(e.target.value)}>
<option value="tracks"> Tracks </option>
<option value="artists"> Artists </option>
<option value="recommendations"> Recommendations </option>
</select>

<select onChange={e => props.setSearchLength(e.target.value)}>
<option value="short_term"> Short Term </option>
<option value="medium_term"> Medium Term </option>
<option value="long_term"> Long Term </option>
</select>

<button> Log Out </button>

</div>
)
}

export default Navbar

在我们能够在屏幕上显示任何用户数据之前,我们需要能够处理 Spotify 和 OpenAI 的 API 调用。为了做到这一点,我们将建立一个 Node.js Express 服务器,以便我们可以安全地执行这些调用。

如果您以前从未使用过Node.js或不了解设置Web服务器的含义,则Fireship提供一个非常易于跟随和信息丰富的视频,解释如何逐步设置您的第一个服务器,您可以在这里查看。

在我们的项目中,src文件夹之外,我们将创建一个新的文件夹用于我们的服务器。在终端内,我们将导航到此文件夹并运行以下命令来初始化我们的项目并安装我们将使用的所有依赖项:

npm init -y
npm i axios cors dotenv express openai

在这一点上,您需要从OpenAI获取自己的个人API密钥。您可以在此处执行此操作。您需要在OpenAI帐户上启用计费,并将收取您进行的API调用费用。但是,使用的模型是gpt-3.5-turbo,每次API调用的价格只是几分钱,因此您不必担心被收取大量费用。

一旦您拥有此 API 密钥,您可以将其复制并粘贴到服务器文件夹中的 .env 文件中。这使得您的 server.js 文件可以访问信息,但是如果决定部署您的网站,则不会公开显示信息。

你的 .env 文件应该如下所示:

OPENAI_API_KEY = "YOUR-OPENAI-API-KEY-HERE"

我们的server.js文件如下所示:

require("dotenv").config();
const cors = require('cors');
const { Configuration, OpenAIApi } = require("openai");
const axios = require("axios");
const express = require("express");
const app = express();
app.use(cors());

app.use(express.json());

const port = process.env.PORT || 4000;


const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY });
const openai = new OpenAIApi(configuration);


// ASK-OPEN-AI -- MAKE REQUEST TO CHATGPT API
// PARAMETERS:
// - prompt (required): Prompt which will be sent to GPT3.5-Turbo
app.get('/ask-open-ai', async (req, res) => {
const prompt = req.query.prompt;

try {
if (prompt == null) {
throw new Error("No prompt provided.");
}

const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
role: "assistant",
content: prompt
},
],
});

const completion = response.data.choices[0].message.content;

return res.status(200).json({
success: true,
message: completion,
});

} catch (error) {
return res.status(400).json({
success: false,
message: error.message
});
}
});

// GET-TOP-SPOTIFY -- GET USER MOST LISTENED TO DATA
// PARAMETERS:
// - search_type (required): Search type used in API call (must be 'tracks' or 'artists)
// - access_token (required): User Spotify access token to allow us to look at their account data
// - time_range (required): Length of search used in Spotify API call(must be 'short_term', 'medium_term', or 'long_term')
// - offset (optional): Index from which to start getting top 50 data (we will use offset 49 to get user's 51-99) (Default Value: 0)
app.get('/get-top-spotify', async (req, res) => {
const search_type = req.query.search_type;
const access_token = req.query.access_token;
const time_range = req.query.time_range;
const offset = req.query.offset;

try {
if (access_token == null) {
throw new Error("No access token provided.");
}

if (search_type !== 'tracks' && search_type !== 'artists') {
throw new Error("Invalid search query provided.");
}

if (time_range !== 'short_term' && time_range !== 'medium_term' && time_range !== 'long_term') {
throw new Error("Invalid time range provided.");
}

if (offset == null) {
offset = 0;
}

const response = await axios.get(`https://api.spotify.com/v1/me/top/${search_type}?`, {
headers: {
Authorization: `Bearer ${access_token}`
},
params: {
limit: 50,
offset: offset,
time_range: time_range
}
});

completion = response.data.items
return res.status(200).json({
success: true,
message: completion,
});

} catch (error) {
console.log(error.message)
return res.status(400).json({
success: false,
message: error.message
})
}
})

// SEARCH-SPOTIFY -- GET MOST RELEVANT TRACK OBJECT FROM SPOTIFY BASED ON SEARCH QUERY
// PARAMETERS:
// - search_query (required): Query used to find track (Similar to searching on Spotify App)
// - access_token (required): User Spotify access token to allow us to make Spotify searches
app.get('/search-spotify', async (req, res) => {
const search_query = req.query.search_query;
const access_token = req.query.access_token;

try {
if (access_token == null) {
throw new Error("No access token provided.");
}

if (search_query == null) {
throw new Error("No search query provided.");
}

const response = await axios.get(`https://api.spotify.com/v1/search?q=${search_query}&type=track&limit=1`, {
headers: { Authorization: `Bearer ${access_token}` },
});

completion = response.data.tracks.items[0]
return res.status(200).json({
success: true,
message: completion,
});

} catch (error) {
return res.status(400).json({
success: false,
message: error.message
})
}
})

app.listen(port, () => console.log(`Server is running on port ${port}`));

总结一下,我们已经声明了一个 web 服务器,我们可以通过 http://localhost:4000 在本地访问。不过不深入介绍 Express 服务器的工作原理,我们已经创建了以下 3 个端点:

  1. ask-open-ai - 允许我们传递一个搜索请求并从GPT-3.5-Turbo接收响应。
  2. 获取顶级Spotify — 允许我们访问用户的短期、中期和长期的前99首歌曲或艺术家。
  3. 搜索-Spotify-允许我们传入搜索查询,并基于我们的搜索返回最相关的歌曲。

我们的 web 服务器已经在其 .env 文件中存储了必要的身份验证信息,这使我们能够安全地进行这些调用。虽然我们是唯一使用这些端点的人,但是处理所有边缘情况(例如,如果有人在没有输入搜索查询或未提供访问令牌的情况下向我们的服务器发出 API 调用)是一个好的实践。在这些情况下,我们输出带有错误消息的结果并报告错误,以便更容易调试。

如果您以前没有使用Express.js服务器,我建议您浏览以上代码并尝试理解每行代码及其目的。为了这篇博客文章,我们将继续向前。

要启动服务器,请在导航到服务器文件夹后在终端中运行以下命令。

node server.js

如果一切正常,您应该会看到消息“服务器正在端口4000上运行”。

我们可以通过在浏览器中进行http请求来测试我们的端点是否正常工作。将以下链接复制并粘贴到浏览器中,并验证您是否从GPT3.5收到了适当的响应。

http://localhost:4000/ask-open-ai?prompt='What is the most listened to track on Spotify of all time'

响应应该像这样:

ChatGPT中文站

我们无法轻易地测试Spotify的终端,因为我们需要来自Spotify的访问令牌才能使用它们,而在我们实际应用程序的上下文中执行此操作会更容易,因此我们将继续前进。

现在我们的服务器已经完全设置好了,我们可以回到前端设置代码来进行这些API调用,并从我们收到的数据中存储重要信息。

在App.js中,我们添加了以下异步函数调用:

const GetUserInfo = async (searchType, offset) => {
const response = await axios.get(`${PORT}/get-top-spotify`, {
params: {
search_type: searchType,
access_token: token,
time_range: searchLength,
offset: offset
}
})

return response.data.message;
}

这个函数接收一个searchType(可以是‘tracks’或者‘artists’)和一个偏移量值,返回一个带有相应信息的50个音轨或艺术家对象的列表。

然后,我们需要以下函数来解码从此函数返回的信息。

const UnwrapSpotifyData = (items, itemType) => {
try {
if (itemType === 'artists') {
return items.map(artist => (
{ name: artist.name, picture: artist.images[0].url, link: artist.external_urls.spotify }
))
}

return items.map(track => (
{ name: track.name, artist: track.artists[0].name, picture: track.album.images[0].url, explicit: track.explicit, duration: track.duration_ms, link: track.external_urls.spotify }
))
} catch (error) {
console.log(error.message)
}
}

这将获取API调用返回的每个对象,并将名称、图片URL、链接和其他功能存储在JavaScript对象中,我们可以在MusicList.js组件中访问该对象。

最后,每当用户更改其搜索长度时,我们希望为艺术家和曲目调用这些函数,并希望这些函数更改我们已经声明的状态变量。我们可以使用以下useEffect钩子来实现这一点。

useEffect(() => {
const fetchData = async () => {
try {
const first50TracksResponse = await GetUserInfo('tracks', 0)
const last50TracksResponse = await GetUserInfo('tracks', 49)
const first50ArtistsResponse = await GetUserInfo('artists', 0)
const last50ArtistsResponse = await GetUserInfo('artists', 49)

const first50Tracks = UnwrapSpotifyData(first50TracksResponse, 'tracks')
const last50Tracks = UnwrapSpotifyData(last50TracksResponse, 'tracks').slice(1)
const first50Artists = UnwrapSpotifyData(first50ArtistsResponse, 'artists')
const last50Artists = UnwrapSpotifyData(last50ArtistsResponse, 'artists').slice(1)

setTrackList([...first50Tracks, ...last50Tracks]);
setArtistList([...first50Artists, ...last50Artists]);

} catch (error) {
console.error('Error fetching data:', error);
}
}

fetchData();
}, [searchLength]);

(注意:我们从last50Artists/Tracks数组的每个第一个元素中切片,以便在列表中不重复两次列出第50位排名的音乐/艺人。)

这将调用我们刚创建的两个函数4次,并将JS对象存储在我们的trackList和artistList状态变量中。因为我们将searchLength放入依赖项数组中,每当用户通过下拉菜单更改搜索长度时,这些函数都将被调用。

最后一步是创建我们的MusicList.js组件,以使网站正常运行。 在App.js中,我们将向它传递以下prop:

const App = () => {

// ...

return (
<div>
<Navbar
setSearchType = {setSearchType}
setSearchLength = {setSearchLength}
setToken = {setToken}
/>

<MusicList
listInfo = {searchType == 'tracks' ? trackList : artistList}
/>
</div>
)

}

export default App

这个三元运算符根据我们的searchType状态变量给出了适当的列表。我们的基本MusicList.js文件只会显示艺术家/曲目的名称,艺术家的名称(如果项目是曲目),以及每个项目的排名。 然而,有了我们从API中收集到的信息,我们可以潜在地显示明确歌曲的明确符号,专辑封面,歌曲在Spotify上的链接,以及各种其他Spotify指标,如歌曲流行度。Spotify API提供了许多可能性。

我们简化后的MusicList.js文件如下:

import React from 'react'

const MusicList = (props) => {
return (
props.listInfo.map((item, index) =>
<div> {index + 1}. {item.name} {item.artist ? ` --- ${item.artist}` : ''}</div>
)
)
}

export default MusicList

有了这个,我们已经完全完成了网站的Spotify部分。我们现在可以成功地访问用户的所有3个搜索长度的热门歌曲和艺术家。现在,我们可以开始将这些信息与GPT3.5-Turbo连接起来,开始做出推荐。

虽然OpenAI声称不会存储通过API调用收集的任何数据,但重要的是在未经允许的情况下不向其他第三方来源提供用户信息。我们将通过在用户打开推荐选项卡时添加一个按钮来实现。默认情况下,我们不会将任何用户数据发送到OpenAI。用户每次想从GPT3.5-Turbo获得更多建议时必须手动按下按钮(即:给予许可)。

我们首先创建一个新的RecList.js组件,初始化为此:

import React from 'react'
import axios from 'axios'

const RecList = (props) => {
return (
<div>RecList</div>
)
}

export default RecList

我们还将进行必要的导入和条件语句,以在我们的searchType状态变量设置为“推荐”时显示此组件。我们还将传递必要的属性。

// ...

import RecList from './RecList'

const App = () => {

// ...

if (searchType == 'recommendations') {
return (
<RecList
recList = {recList}
setRecList = {setRecList}
trackList = {trackList}
/>
)
}

// ...

}

export default App

2023-10-20 16:52:14 AI中文站翻译自原文