用 PHP 重现 Spotify Wrapped:生成个人音乐收听统计数据的分步指南

用PHP重现Spotify年度回顾

我特别喜欢Spotify推出的年度报告,他们把它叫做“年度回顾”。这是个小应用程序,能告诉你这一年里你最喜欢的艺术家是谁,以及你听得最多的音乐类型有哪些。我对自己听了这些或那些音乐并不意外,不过因为我每周在Spotify上听歌大概30 - 40小时,我还真记不清那一年我最喜欢的艺术家是谁了。

今年的报告让我琢磨起来,不管什么时候,自己生成一份Spotify报告到底有多难。也许是想看看本周我听谁的歌最多,或者当下我喜欢哪种音乐类型。

我知道Spotify有个API,但我从没真正用过。所以我决定用PHP写个程序,来生成一份每周的回顾报告。在Drupal开发中,也会有类似利用外部API实现特定功能的需求,比如Drupal模块开发时可能会调用第三方音乐服务的API来实现音乐相关的展示。

一、连接到Spotify API

Spotify Web API是个RESTful JSON服务,它用OAuth作为认证机制。因为所有返回数据的API调用都得认证,所以你得先拿到一个OAuth令牌,才能用这个API。

为了做到这一点,我得在Spotify上创建一个应用程序,然后用它把我的个人Spotify账户和这个应用程序进行认证。创建应用程序挺简单的,你会得到一个客户端ID和客户端密钥,用来和Spotify建立连接。

Spotify允许你在“开发模式”下创建应用程序,这种模式使用配额小,最多只允许对25个用户进行认证。对我来说,这足够了。

接着,我开始找一个能让我连接到Spotify Web API的PHP库。我本来可以自己写这个库,但我觉得应该已经有一些库能简化我要写的代码。

巧了,有好几个PHP的Spotify库。试了几个之后,我选了jwilsson/spotify - web - api - php库。这个库好像涵盖了所有的Spotify API,维护得不错,还采用了现代PHP编码技术(像编码标准和单元测试)。

要连接到API,我只需要写一小段代码,提供我设置应用程序时拿到的客户端ID和客户端密钥就行。下面这段代码会创建SpotifyWebAPI\Session对象,然后把用户重定向到Spotify,让他们对应用程序进行认证。设置认证重定向的时候,我还会传入一组我想对该用户使用的权限范围。

  
    <?php

    require '../vendor/autoload.php';

    // 启动一个PHP会话。
    session_start();

    // 创建Spotify会话。
    $spotifySession = new SpotifyWebAPI\Session(
      '[客户端ID]',
      '[客户端密钥]',
      'https://localhost/callback.php'
    );

    // 重定向到Spotify。
    $options = [
      '权限范围' => [
        '读取用户邮箱',
        '读取用户最近播放记录',
      ],
    ];

    header('Location: ' . $spotifySession->getAuthorizeUrl($options));
    die();
  

我们一访问这个页面,就会看到类似这样的一个屏幕,确认我们是否想把个人Spotify账户连接到这个应用程序。你能在这儿看到权限范围,这样用户就能更清楚这个应用程序可以访问哪些内容。注意,用户得先登录到Spotify才能看到这个对话框。

用户一接受,就会被重定向到我们创建应用程序时指定的“回调”URL。目前这只是个本地地址,但OAuth在本地地址上也能正常工作。

回调URL指向另一个页面,这个页面会从回调URL里获取“代码”参数,给用户生成一个访问令牌,然后把它存到PHP会话中。完成这些后,我们会显示一个指向history.php页面的链接,在这个页面我们会分析用户的历史记录。

下面是callback.php页面的内容,它用来接收传入的Spotify认证信息。

  
    <?php

    require '../vendor/autoload.php';

    // 启动一个PHP会话。
    session_start();

    // 创建Spotify会话。
    $spotifySession = new SpotifyWebAPI\Session(
      '[客户端ID]',
      '[客户端密钥]',
      'https://localhost/callback.php'
    );

    if (isset($_GET['代码'])) {
      $spotifySession->requestAccessToken($_GET['代码']);

      $_SESSION['访问令牌'] = $spotifySession->getAccessToken();
      $_SESSION['刷新令牌'] = $spotifySession->getRefreshToken();

      echo '<a href="/history.php">历史记录</a>';
    }
  

我们可以直接重定向到历史记录页面,但有时候给用户显示一个确认页面,说“感谢连接”会更好。

从这一步开始,要连接到Spotify Web API,我们只需要运行下面这段代码。它会从PHP会话变量里提取“访问”令牌,然后和Spotify建立连接。

  
    $api = new SpotifyWebAPI\SpotifyWebAPI();
    $access = $_SESSION['访问令牌'];
    $api->setAccessToken($access);
  

现在,$api变量就是我们和Spotify的连接。

这就是OAuth的一般工作原理,Spotify API是个很好的入门例子。

二、生成Spotify播放历史

查看Spotify Web API可以知道,可以用/me/player/recently - played端点获取用户最近播放的曲目。因为Spotify的文档写得其实挺不错的,所以我就不详细介绍API响应的内容了。他们有个类似Swagger的API演示,你能通过它查看API请求返回的实际数据。

SpotifyWebAPI类中的“getMyRecentTracks”方法封装了这个端点,所以我们只要调用这个方法就能生成数据。

调用这个端点会生成一长串曲目列表,但API是分页的。所以,我们得循环请求,从当前光标位置之后获取更多数据(光标位置是API的一部分)。

获取数据之前,我们要设置几个数组,用来存储数据,方便后续分析和展示。

  
    // 为我们的数据设置存储容器。
    $artists = [];
    $genres = [];
    $tracks = [];
  

然后,我们可以循环调用getMyRecentTracks方法来获取所有的历史数据。

  
    // 从Spotify获取数据。
    $recent = null;
    while (true) {
      try {
        if ($recent === null) {
          // 获取最近播放的曲目。
          $recent = $api->getMyRecentTracks();
        } else {
          // 如果我们有一个$recent对象,那么在之前的光标位置查找结果。
          $options = ['之前的光标位置' => $recent->cursors->before];
          $recent = $api->getMyRecentTracks($options);
        }
      } catch (\SpotifyWebAPI\SpotifyWebAPIException $e) {
        // OAuth令牌出现问题,将用户重定向回index.php页面以重新生成令牌。
        header('Location: index.php');
        die();
      }

      // 遍历我们的结果并提取所需的数据。
      foreach ($recent->items as $id => $item) {
        // 获取艺术家数据。
        $apiArtists = getArtistWithCache($api, $item->track->artists[0]->id);

        // 进行音乐类型统计。
        foreach ($apiArtists->genres as $genre) {
          if (isset($genres[$genre])) {
            $genres[$genre]++;
          } else {
            $genres[$genre] = 0;
          }
        }

        // 进行艺术家播放时长统计,并累加收听曲目的时长。
        if (isset($artists[$item->track->artists[0]->name])) {
          $artists[$item->track->artists[0]->name] += $item->track->duration_ms;
        } else {
          $artists[$item->track->artists[0]->name] = $item->track->duration_ms;
        }

        // 存储曲目数据,以便后续展示。
        $tracks[] = [
          '时间' => $item->played_at,
          '艺术家' => $item->track->artists[0]->name,
          '专辑' => $item->track->album->name,
          '曲目' => $item->track->name,
          '图片' => $item->track->album->images[2],
        ];
      }

      if ($recent->cursors === null) {
        // 如果没有光标位置,那么跳出循环。
        break;
      }
      $before = $recent->cursors->before;
    }
  

这段代码运行后,我们的数组就会充满数据。

运行这段代码后我很快发现,用这个API端点,Spotify只允许你获取最近播放的50首曲目。没有其他API端点可以获取你的Spotify历史记录,所以基本上就只能这样了。我听说可以向Spotify请求更早的数据,但处理这些请求可能得花点时间。

这个循环里的getArtistWithCache函数是个自定义函数,它能防止在循环里反复请求同一个端点获取相同的数据。它用一个静态变量来填充艺术家数据的缓存,如果在本次请求中再次请求相同的艺术家,它会直接返回缓存中的数据。

  
    function getArtistWithCache($api, $id) {
      static $artistCache = [];

      if (isset($artistCache[$id])) {
        return $artistCache[$id];
      }

      $apiArtist = $api->getArtist($id);

      $artistCache[$id] = $apiArtist;

      return $artistCache[$id];
    }
  

现在,我们只需要为收集到的不同数据数组生成统计信息。

为了生成艺术家统计信息,我们只要对数组进行排序并遍历,打印出艺术家的名字,再把毫秒时长转换成分钟。

  
    // 生成艺术家统计信息。
    arsort($artists);
    echo '<table>';
    foreach ($artists as $artist => $duration) {
      $duration = number_format($duration/1000/60, 2);
      echo '<tr>';
      echo '<td>' . $artist . '</td><td>' . $duration . ' 分钟</td>';
      echo '</tr>';
    }
    echo '</table>';
  

为了生成音乐类型统计信息,我们只要遍历数组里的项,打印出音乐类型以及分析中遇到该类型的次数。

  
    // 生成音乐类型统计信息。
    arsort($genres);
    echo '<table>';
    foreach ($genres as $genre => $number) {
      echo '<tr>';
      echo '<td>' . $genre . '</td><td>' . $number . '</td>';
      echo '</tr>';
    }
    echo '</table>';
  

你可以选择生成一个包含专辑封面缩略图的曲目列表,这样看起来会不错。

  
    echo '<ul>';
    foreach ($tracks as $track) {
      echo '<li>';
      echo '<img src="' . $track['图片']->url . '" height="' . $track['图片']->height . '">';
      echo $track['艺术家'] . ': ' . $track['曲目'] . ' <br>';
      echo '<span style="font-size: small">' . $track['时间'] . '</span>';
      echo '</li>';
    }
    echo '</ul>';
  

这会生成一个虽然简陋但能用的曲目列表。

三、我的统计数据

在我自己的账户上运行这段代码,得到了下面这些艺术家统计结果。

夜蝠乐队 67.99分钟
密涅瓦之骨乐队 61.97分钟
机械师乐队 39.97分钟
修道院乐队 31.87分钟
血腥咒文乐队 18.19分钟
死亡点乐队 11.16分钟

我写这篇文章的时候一直在听夜蝠乐队的歌,所以它的播放时长超过了一个小时。目前我特别喜欢西班牙乐队密涅瓦之骨乐队,从数据能看出来我今天听了他们好多歌。机械师乐队和血腥咒文乐队的歌在过去几年里一直是我循环播放的曲目。

从这些曲目生成的音乐类型如下。

拉丁摇滚 14
史诗黑金属 12
死亡核 10
旋律死亡核 10
新金属核 10
丹麦死亡金属 5
丹麦金属 5
洞穴死亡金属 2
宇宙死亡金属 2
死亡金属 2
死亡碾核 2
丹佛金属 2
现代死亡金属新浪潮 2
技术死亡金属 2

Spotify里的音乐类型特别杂乱,和它们代表的艺术家之间的联系好像也很牵强。我完全不知道哪首曲目产生了“洞穴死亡金属”这个类型,但听起来这是个我会喜欢的类型!

四、结论

我本来打算生成一些关于我听歌习惯的统计数据,可因为这个API端点最多只返回50首曲目,能分析的内容不多。我能生成的统计数据挺有意思,但因为这些曲目都是我今天听的,所以最后只是告诉我一些我已经知道的事儿。

由于可用的数据有限,我觉得没必要深入研究创建图表和特殊图形。所以,我只创建了几个表格,就到此为止了。

可以扩展这段代码来生成更长的报告,但这意味着每隔几个小时就得轮询一次Spotify API,以保持曲目列表的更新。我将来可能会试试这么做,但目前我对结果挺满意的。如果我觉得这个应用程序值得进一步完善,可能会有后续的第二部分。

你可以随意在自己的应用程序中使用上述代码。

我四处找了找,发现有个服务已经能实现这个功能。所以,如果你有兴趣用你的Spotify数据生成更长的报告,可以找找相关服务。在Drupal开发领域,未来可能会有像Drupal11这样的版本,也会不断需要进行Drupal升级,以便更好地支持各种功能开发,比如调用Spotify API生成报告这类功能。