软硬件交互

如何调取音乐和天气API让特斯拉原型读取真实数据

本文将揭示如何将天气信息或音乐这样的动态真实数据无缝集成到原型中。

Kay van den Aker, Prototype designer

August 21, 2023

如何调取音乐和天气API让特斯拉原型读取真实数据

介绍

在本文中,我们将展示外部数据源与原型进行互联的能力,这一能力使原型具备真实而沉浸的测试体验。通过企业版订阅中的 ProtoPie Connect 以及全新的 parseJson 函数即可实现这样的功能。
这个教程中,我们将向用户界面集成整合天气 API 以及Spotify API 所产生的真实数据,通过有限的操作引领无限的可能!
什么是 API? API 即应用程序编程接口(Application Programming Interface),从最基本的用途来看,API 使你可以连接到另一个应用程序上并从中获取一些信息。
查看下方的演示视频,可以了解到本教程案例的最终形态。
你对原型设计中能用上这个最新的 API 功能是否感兴趣? 快来点击注册 ProtoPie 以便尽早了解这一全新的 API 功能吧!

所需资源

在本教程中,我们会用到以下资源。你可以按教程指引逐步构建,也可以直接下载最终文件后从中了解其工作原理。
注意: 需要 Python 3.9来运行相应的程序代码(比3.9更新的版本会引发无法解决的错误,因此如果你使用了较新版本,请回退到3.9版本)
import python-socketio
import fileinput
import sys
import requests, datetime
import json

# 更改下列地址为你的地址
address = 'http://localhost:9981'

io = socketio.Client()

months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']

def get_access_token():
    response = requests.post('https://accounts.spotify.com/api/token', {
        'grant_type': 'client_credentials',
        'client_id': '{paste your client id here}',
        'client_secret': '{paste your client secret here}',
    })
    return response.json()['access_token']

def get_top_tracks(country_code):
    access_token = get_access_token()
    endpoint = f'https://api.spotify.com/v1/browse/categories/toplists/playlists?country={country_code}'

    playlists = requests.get(endpoint, headers={'Authorization': f'Bearer {access_token}'}).json()['playlists']['items']

    top_50_playlist = next((p for p in playlists if p['name'].startswith('Top 50')), None)

    playlistName = ""

    if top_50_playlist is None:
        top_50_href = playlists[0]['tracks']['href']
        playlistName = playlists[0]['name']
    else:
        top_50_href = top_50_playlist['tracks']['href']
        playlistName = top_50_playlist['name']


    tracks = requests.get(top_50_href, headers={'Authorization': f'Bearer {access_token}'}).json()['items']

    musicData = {
        'playlistName': replace_whitespace(playlistName),
        'topSongs': []
    }

    for i in range(8):
        title = replace_whitespace(tracks[i]['track']['name'])
        artist = replace_whitespace(tracks[i]['track']['artists'][0]['name'])
        musicData['topSongs'].append({"title": title, "artist": artist})

    print("Music data:")
    print(json.dumps(musicData, indent=1, ensure_ascii=False))
    return(json.dumps(musicData, ensure_ascii=False))

@io.on('connect')
def on_connect():
    print('[SOCKETIO] Connected to server')
    io.emit('ppBridgeApp', { 'name': 'python' })

@io.on('ppMessage')
def on_message(data):
    messageId = data['messageId']
    messageValue = data['value'] if 'value' in data else None

    if messageId == "getWeatherAndMusicData":
        location = messageValue
        locationUrl = "http://api.openweathermap.org/geo/1.0/direct?q=" + location + "&limit=5&appid={Paste-your-API-key-here}"
        locationData = requests.get(locationUrl).json()
        print(locationData)

        country = str(locationData[0]['country'])
        latitude = str(locationData[0]['lat'])
        longitude = str(locationData[0]['lon'])

        print(location + ", " + country + " has latitude " + latitude + " and longitude " + longitude)

        weatherUrl = "https://api.openweathermap.org/data/2.5/weather?lat=" + latitude + "&lon=" + longitude + "&appid={Paste-your-API-key-here}"
        weatherData = requests.get(weatherUrl).json()

        timeUTC = datetime.datetime.utcnow().timestamp()
        timeLocal = datetime.datetime.fromtimestamp(timeUTC + weatherData['timezone'])
        timeData = {"localtime": {"year": timeLocal.strftime("%Y"), "month": months[int(timeLocal.strftime("%m"))], "day": timeLocal.strftime("%d"), "hour": timeLocal.strftime("%H"), "minute": timeLocal.strftime("%M") }}

        weatherData.update(timeData)
        print("Weather data:")
        print(json.dumps(weatherData, indent=1))

        io.emit('ppMessage', {'messageId':'weatherData', 'value': str(weatherData).replace("'", "\"")})
        musicData  = get_top_tracks(country)
        io.emit('ppMessage', {'messageId':'musicData', 'value': musicData})

def replace_whitespace(obj):
    return obj.replace('\u00a0', ' ').replace('\t', ' ').strip()

io.connect(address)

制作过程讲解

1. 启动本地服务端

为了将 Python 程序连接到 ProtoPie 上,首先我们需要设置Socket.io
在 ProtoPie Connect 启动后,它会在左下角显示的地址上启动一个服务端。此时Socket.io 将允许 Python 程序连接到本地服务端,从而实现消息收发!
1
ProtoPie Connect

2. 创建 Python Bridge App 🐍

2.1. Bridge App 示例解析
在这个页面 (https://github.com/ProtoPie/protopie-connect-bridge-apps/blob/master/python-bridge/client.py)可以查看 Python 应用程序连接到 ProtoPie 的示例模板,以及使用 ProtoPie 构建的各种 Bridge App,这些资料可以帮你尽快上手!
如果觉得这种方式还是太复杂,也可以将 Python Bridge App 编译为 ProtoPie Connect 的插件,这种方式我们在用户指南中有具体讲解 (https://www.protopie.io/learn/docs/zh/connect/custom-plugins)。
基本设置
首先,在页面顶部,用 imports 语句以导入的方式获取相关的程序包,以保障程序正确运行。
然后,有 address 的这一句是声明了程序要试图连接的 IP 地址。如果 Python 应用程序和 ProtpPie Connect 运行在同一台计算机上,就不需要对这个默认的 IP 地址进行更改了。
最后,io 开头的这一句是声明表示 socket 接口的变量,我们通过这一变量来实现与 ProtoPie Connect 进行交互。
import socketio
import fileinput
import sys

address = 'http://localhost:9981'
io = socketio.Client()
连接函数设置
基本设置完成后,可以看到 def 开头的语句定义了一个 on_connect 函数,当 Python 程序连接到服务端时,这个函数就会被调用。
被调用时,这个函数会用 emit 语句向服务端发送一条消息,以确认连接成功。
@io.on('connect')
def on_connect():
    print('[SOCKETIO] Connected to server')
    io.emit('ppBridgeApp', { 'name': 'python' })
接收消息
on_connect 函数之后,可以看到一个名为 on_message 的函数。如果你希望程序在接收到特定消息后执行指定的操作,那么这些操作的程序代码就应该放到这个函数中。
@io.on('ppMessage')
def on_message(data):
    messageId = data['messageId']
    value = data['value'] if 'value' in data else None
    print('[SOCKETIO] Receive a Message from connect', data)
启动连接
一切就绪后,程序将实质启动连接并试图连接到上面的代码所提供的 IP 地址上。
io.connect(address)
发送消息
默认的 Bridge App 还提供了在终端内输入消息 ID 和消息值后将这些信息一并发送到 ProtoPie Connect 服务端的功能。
while 1:
  messageId = input('Please input a message id: ')
  value = input('Please input a value: ')

  print('\tSend ', messageId, ':', value, ' data to Connect');
  io.emit('ppMessage', {'messageId':messageId, 'value':value})
动手试试!
确保已经装好了 Python ,然后右键点击刚保存的 client.py 文件并选择“在文件夹中打开新终端(New Terminal at Folder)”,可以看到一个新的终端窗口被打开了。
3
在终端内执行 pip3 install -r requirements.txt 命令来安装相关的程序包,然后执行 python3 client.py 命令来启动这个程序。
终端内会显示提示,让你输入一个消息 id 以及相应的消息值,输入完成后按下回车键确认。
如果一切顺利,应该可以在 ProtoPie Connect 中看到该消息及相应的消息值!
4

2.2. 接下来,获取天气数据!

现在,为了获取天气数据,我们需要发送一条名为“getWeatherAndMusicData”的消息,并将位置作为消息值。Python 程序会返回该位置的天气和音乐数据。
获取经纬度数据
要想通过我们的程序来获得特定位置的天气数据,首先要通过地理编码 API 来获取城市的经纬度。
请查阅地理编码 API 网站上的文档,了解如何获取到 API 密钥。这个所谓的密钥是一串唯一的编码,用于访问该网站的 API。拿到属于你的密钥后,将它填入代码中显示 {API key}的地方。
在我们的程序中,首先会检查从 ProtoPie 接收到的消息是否为“getWeatherAndMusicData”。
如果消息名正确,即意味着 ProtoPie 正在尝试获取数据,那么我们会将消息值(messageValue)赋给一个名为 location 的变量。
然后,我们创建一个名为 locationUrl 的变量,这个变量表示的是用于获取地理编码的网址,我们把从 ProtoPie 收到的消息值粘贴到这个网址的中间,以这种方式灵活获取不同位置的地理数据。
接下来,使用 requests.get(locationUrl).json() 语句来获取指定位置的地理数据。
@io.on('ppMessage')
def on_message(data):
    messageId = data['messageId']
    messageValue = data['value'] if 'value' in data else None

    if messageId == "getWeatherAndMusicData":
        location = messageValue
        locationUrl = "http://api.openweathermap.org/geo/1.0/direct?q=" + location + "&limit=5&appid={API key}"
        locationData = requests.get(locationUrl).json()

				print(locationData)
这步操作将会返回一长串包含大量数据的消息,而我们只需要其中的经纬度和国家信息。
如果你对这一长串消息的内容的可读性感到疑惑,只需将这串消息粘贴到这个 JSON 在线格式化 (https://jsonformatter.curiousconcept.com/)工具中,即可查看到明晰的数据内容。
我们可以用下列语句来获取到这条消息内我们所需要的值。
country = str(locationData[0]['country'])
latitude = str(locationData[0]['lat'])
longitude = str(locationData[0]['lon'])

print(location + ", " + country + " has latitude " + latitude + " and longitude " + longitude)
print 函数将会输出 Amsterdam, NL has latitude 52.3727598 and longitude 4.8936041 这样的内容,这正是我们在下一步获取天气数据时所需要的内容!
5
包含数据的长消息
获取天气数据
现在,我们用同样的方式来调用天气 API , 把刚才取得的经纬度数据组成一个网址,然后使用 requests.get(weatherUrl).json() 函数来获取指定位置的天气数据。
这个 print 语句将会输出我们刚从天气 API 获得的大量信息,譬如天气描述、温度、湿度等。
其中的 json.dumps(weatherData, indent=1) 语句用于按照规范的缩进形式来输出内容,以提升可读性😉。
6
Python bridge
我们还希望显示我们所指定位置的时间,所以我们使用下列代码来从 weatherData 中获取时区信息,并将所形成的timeData加入到 weatherData 中。
timeUTC = datetime.datetime.utcnow().timestamp()
timeLocal = datetime.datetime.fromtimestamp(timeUTC + weatherData['timezone'])
timeData = {"localtime": {"year": timeLocal.strftime("%Y"), "month": months[int(timeLocal.strftime("%m"))], "day": timeLocal.strftime("%d"), "hour": timeLocal.strftime("%H"), "minute": timeLocal.strftime("%M") }}

weatherData.update(timeData)

print(json.dumps(weatherData, indent=1))
下图所示的运行结果就是将要被添加到 weatherData 中的时间信息。
7
时间数据
现在,我们只需要简单地将这些信息发送回 socket 服务端,ProtoPie 就可以访问这些数据了。
所传送的消息 ID 是“weatherData”,消息值就是上面我们获取到的数据。我们使用 str(weatherData).replace("'", "\\\\"") 这条语句来将 json 数据转换回字符串,并将所有的 ' 替换为 \\\\" 以符合 ProtoPie 所适用的语法规则。
io.emit('ppMessage', {'messageId':'weatherData', 'value': str(weatherData).replace("'", "\"")})

2.3. 将数据发送给 ProtoPie

首先我们可以把 “getWeatherAndMusicData” 消息发送到 ProtoPie Studio,并在消息中以消息值的方式带上要获取相关数据的目标城市名。
8
Next, create a 接收触发动作并将接收到的数据赋给一个变量,确保该变量的类型被设为 Text(文本)。
9
我们可以将该变量与 parseJson 函数结合使用,从而从中获取所需的任何信息,譬如替换文本图层中的内容。
如希望了解更多关于如何使用 parseJson 函数来访问 Json 对象内部数据的知识,可以参看这个 Pie 文件 .
10
如果在 ProtoPie Connect 中打开并启动这个 Pie,可以看到如下图所示的结果。
11
ProtoPie Connect 中所呈现的 Pie
如你所见,Pie 将值为 “Amsterdam” 的 “getWeatherAndMusicData” 消息发送给 ProtoPie Connect,然后 Python Bridge App 返回了以 json 对象为值的 “weatherData” 消息。Pie 收到这一消息并从中提取了主要的天气描述内容。太棒了!

2.4. 获取音乐数据

在 Python 程序中发出 weatherData 消息之后,我们添加如下代码。
musicData  = get_top_tracks(country)
io.emit('ppMessage', {'messageId':'musicData', 'value': musicData})
这条语句会以 “country” 为参数调用名为 “get_top_tracks” 的函数,然后将从该函数获取到的值作为消息值放入 “musicData” 消息中,再把消息发送到 Socket 服务端。
获取访问令牌
get_top_tracks 函数的内容始于调用另一个名为 get_access_token 的函数。
def get_top_tracks(country_code):
    access_token = get_access_token()
Spotify 的 API 相比于其他常见的 API 而言要复杂一些,我们需要先用客户端(client)ID客户端密钥(client secret)去请求获取一个临时密钥,这个临时密钥我们称为访问令牌(access token)。创建一个 Spotify 开发者帐户就可以获得这些登录凭证。
图中所示的 get_access_token 函数会使用所提供的登录凭证从 Spotify 上获取一个访问令牌
如要了解更多关于 Spotify API 的知识,可以点击此处 .
def get_access_token():
    response = requests.post('https://accounts.spotify.com/api/token', {
        'grant_type': 'client_credentials',
        'client_id': '{paste your client_id here}',
        'client_secret': '{paste your client_secret here}',
    })
    return response.json()['access_token']
获取未整理的音乐数据
回到 get_top_tracks 函数,我们可以用这个访问令牌来获取我们所需要的音乐数据。
注意,我们得把从 Weather API 上拿到的国家代码(country code)添加到 Spotify 用于获取音乐数据的网址中,才能获取到相应国家的音乐数据!
def get_top_tracks(country_code):
    access_token = get_access_token()
    endpoint = f'https://api.spotify.com/v1/browse/categories/toplists/playlists?country={country_code}'

    musicData = requests.get(endpoint, headers={'Authorization': f'Bearer {access_token}'}).json()

		print(json.dumps(musicData, indent=1))
这步操作会再给你返回一堆庞大的数据内容。
12
播放列表数据内容

访问播放列表

通常我们会对一个国家的热门歌曲格外关注。
那么我们首先添加 ['playlists']['items'] 到请求中,以获取所有的播放列表。
然后,遍历播放列表中的所有对象,检索是否存在一个名字以 “Top 50” 为开头的对象。如果存在,则将这个对象赋给一个名为 “top_50_playlist” 的变量。而如果不存在,则将这个目标变量赋值为 None
如果变量 top_50_playlist 的值为 None,即表示找不到该国家的播放列表,则采用数据内容中的第一个播放列表来取而代之。反之,如果该变量的值不为 None,则变量里记录了 top_50_playlist 这个播放列表中的音乐链接以及播放列表名称。
top_50_href 和 playlistName 这两个变量将会被用于请求播放列表中各曲目数据所形成的数组。
playlists = requests.get(endpoint, headers={'Authorization': f'Bearer {access_token}'}).json()['playlists']['items']
top_50_playlist = next((p for p in playlists if p['name'].startswith('Top 50')), None)

playlistName = ""

if top_50_playlist is None:
	  top_50_href = playlists[0]['tracks']['href']
	  playlistName = playlists[0]['name']
else:
	  top_50_href = top_50_playlist['tracks']['href']
    playlistName = top_50_playlist['name']

	  tracks = requests.get(top_50_href, headers={'Authorization': f'Bearer {access_token}'}).json()['items']

创建 musicData 对象

接下来,我们创建一个名为 musicData 的对象,并将变量 playlistName 赋值给对象中的 “playlistName” 属性,同时创建一个名为 “topSongs” 的空数组
musicData = {
    'playlistName': replace_whitespace(playlistName),
    'topSongs': []
}
在这个过程中,我们调用了一个名为 replace_whitespace 的函数来处理 playlistName 变量,这是因为其变量值中可能包含有 ProtoPie 无法正确显示的特殊字符。这个函数会用空格来替换这些特殊字符,再返回替换后的结果。
def replace_whitespace(obj):
    return obj.replace('\u00a0', ' ').replace('\t', ' ').strip()

获取热门歌曲

现在,前8首歌曲的歌名和歌手信息会被添加到 musicData 中还空着的 topSongs 数组之内。
然后,把这8首歌的信息用 print 函数显示出来。重申一下,务必确保所使用的是 ASCII 字符集,我们当然不希望在 Pie 中显示出乱码!
for i in range(8):
    title = replace_whitespace(tracks[i]['track']['name'])
    artist = replace_whitespace(tracks[i]['track']['artists'][0]['name'])
    musicData['topSongs'].append({"title": title, "artist": artist})

print("Music data:")
print(json.dumps(musicData, indent=1, ensure_ascii=False))
return(json.dumps(musicData, ensure_ascii=False))
print 函数执行后所显示的内容如下图所示,相应的 musicData 对象包含了 playlistName, 即播放列表名称,以及一个名为 topSongs 的数组,这个数组里存放的是指定的国家中 Top 50 播放列表里的前8首歌。
13
音乐数据

2.5. 在原型中使用音乐数据

在 Pie 中,我们可以再次将这些音乐数据赋值给一个变量,然后结合 parseJson 函数来使用这一变量。
14
将接收触发动作赋给一个变量

3. 将所有内容添加到用户界面上

15
包含天气数据的特斯拉原型
在原型中,我们以各种方式使用 weatherData 和 musicData。以下是在 ProtoPie 的表达式填写框中使用这两个对象的一些示例:
  • 右上角日期时间显示所用的表达式: parseJson(weatherData, "localtime.day") + " " + parseJson(weatherData, "localtime.month") + " " + parseJson(weatherData, "localtime.hour") + ":" + parseJson(weatherData, "localtime.minute")
  • 顶部中央地理位置显示所用的表达式: parseJson(weatherData, "name") + ", " + parseJson(weatherData, "sys.country")
  • 天气数据显示所用的表达式: format((parseJson(weatherData, "main.temp") - 273), "##") + "°C – " + upperCase(left(parseJson(weatherData, "weather.0.description"), 1)) + right(parseJson(weatherData, "weather.0.description"), length(parseJson(weatherData, "weather.0.description")) - 1) .为了让用户界面看起来更美观,我们将获取到的主要温度值减去273,以得到摄氏温度值(原本是开氏温度值),同时将天气描述的首字母改为大写。🤷🏼‍♂️
16
天气数据
  • 天气图标是采用了这句由 Weather API 提供的天气图标代码实现的:parseJson(weatherData, "weather.0.icon")。 我们将天气图标的代码赋给一个变量,然后检测其变化,再根据变化从这里 (https://openweathermap.org/weather-conditions)所给出的各种图标中显示相应的图标。同时我们还根据天气情况,用这段代码以淡入淡出的显示方式来控制各种天气下的场景覆盖呈现。
17
ProtoPie Connect 中的天气数据
  • 还有一个名为 “mode” 的变量,我们采用**right(parseJson(weatherData, "weather.0.icon"), 1)**这个表达式来对其赋值。这个表达式起到的作用是获取天气图标代码中的第一个字母,即白天时的 “d” 和夜晚时的 “n”,通过这种方式我们就可以用变量来控制夜间模式的切换以及相应地调整文字颜色。
18
Mode 变量
  • 对于播放列表名称的表达式,我们使用 parseJson(musicData, "playlistName")
  • 对于每首歌曲,我们将歌名的文本对象改为**parseJson(musicData, "topSongs.0.title")** ,同时歌手改为**parseJson(musicData, "topSongs.0.artist")** ,其中的**.0.** 就顺排地改为**.1.** , .2. 等等。
  • 我们还设定了在检测到 musicData 变量内容变化时,对唱片集的封面赋一个随机的颜色,以便模拟歌曲上传时的视觉效果。color(random(100,250), random(100,250), random(100,250))
19
将颜色赋给唱片集封面

完成!🥧 真实数据已经成功添加到你的原型中!

恭喜,一路过关斩将!我们为你感到无比自豪!😉我们希望这次的学习过程能有助于你在未来的道路上不落窠臼地思考,并使你的Pies栩栩如生、引人入胜。

以 API 集成来升级你的原型创造力

免费入门 ProtoPie 并探索运用 “API 集成”这一强大功能创作高保真、沉浸式用户体验原型,从而激发无限创作可能。
祝你原型创作愉快!