Musicbrainz Integration

MusicBrainz Integration

When I was younger, I decided to start keeping track of my music collection. I wanted to be able to browse my collection digitally, see which albums I got when, and look at statistics like average album length or artists with the most albums. I made a ClarisWorks document along with a spreadsheet to handle it. This became really tedious to manually enter in all of the metadata and track information in multiple places and I found that I wasn’t keeping it up after a few years.

Eventually I decided to make my own application to display and browse my collection. My intention was to be able to query some other service to get artist, album, and track metadata. That’s when I discovered MusicBrainz.org. It is a music encyclopedia-wiki that makes the music data available to the public. I built a tool that integrated with it and now I no longer have issues keeping up with entering the data because it’s so easy to retrieve it from MusicBrainz. Since MusicBrainz is a wiki, if they don’t have the album that I’m entering, then I just add it on MusicBrainz first.

My application has gone through a couple of iterations since then but I have a basic php class that I use for retrieving information from the MusicBrainz api.

Note: I’ve modified this version to be more of a standalone static class. Also, this is only currently designed parse the data for ‘release’ entity types. It wouldn’t be hard to add other entity types but I haven’t needed to to that.

<?php

use Symfony\Component\HttpClient\HttpClient;

class MusicBrainzApi {

  const URL = 'https://musicbrainz.org/ws/2/';
  const COVER_URL = 'https://coverartarchive.org/release/';
  // You should use your own custom user agent.
  const USER_AGENT = 'my-app/1.0 (https://example.com)';

  /**
   * Query the MusicBrainz database.
   *
   * @param array $options
   *  A series of options to limit and control the query. (See below)
   *
   * @return array|string
   *  The parsed json array or raw json string depending on the options.
   */
  public static function query(array $options) {
    $options += [
      'entity' => 'release',
      'query' => [],
      'mbid' => NULL,
      'fmt' => 'json',
      'inc' => NULL,
      'limit' => NULL,
      'offset' => NULL,
      'release' => NULL,
      'parse' => FALSE,
    ];
    $request_options = [
      'query' => [
        'fmt' => $options['fmt'],
      ],
      'headers' => [
        'User-Agent' => self::USER_AGENT,
      ],
    ];

    $url = self::URL . $options['entity'];

    if (!empty($options['query'])) {
      array_walk($options['query'], function(&$value, $key) {
        $value = $key . ':' . $value;
      });
      $request_options['query']['query'] = implode(' AND ', $options['query']);
    }
    if (!empty($options['mbid'])) {
      $url .= '/' . $options['mbid'];
    }
    foreach (['inc', 'limit', 'offset', 'release'] as $key) {
      if (!empty($options[$key])) {
        $request_options['query'][$key] = $options[$key];
      }
    }

    $client = HttpClient::create();
    $request = $client->request('GET', $url, $request_options);

    if ($request->getStatusCode() != 200) {
      throw new \Exception('Request to musicbrainz failed with status ' . $request->getStatusCode());
    }
    $json = json_decode($request->getBody());
    return $options['parse'] ? self::parse($options['entity'], $json) : $json;
  }

  /**
   * Parse the returned json string.
   *
   * @param string $entity
   *  The type of entity requested from music brainz.
   * @param string $json
   *  The raw json string returned.
   *
   * @return array
   *  A parsed version of the returned json.
   */
  public static function parse($entity, $json) {
    switch ($entity) {
      case 'release':
        return self::parseRelease($json);
    }
    return $json;
  }

  public static function parseRelease($release) {
    $parsed = [
      'id' => $release->id,
      'title' => $release->title,
      'link' => 'https://www.musicbrainz.org/release/' . $release->id,
    ];
    foreach (['country', 'date', 'track-count', 'score'] as $key) {
      if (!empty($release->{$key})) {
        $parsed[$key] = $release->{$key};
      }
    }
    if (!empty($release->{'artist-credit'})) {
      $parsed['artist'] = reset($release->{'artist-credit'})->artist->name;
      $parsed['artist_id'] = reset($release->{'artist-credit'})->artist->id;
    }
    if (!empty($release->{'label-info'})) {
      $parsed['label'] = reset($release->{'label-info'})->label->name;
    }
    if (!empty($release->{'media'})) {
      $media = reset($release->{'media'});
      if (!empty($media->format)) {
        $parsed['format'] = $media->format;
      }
      if (!empty($media->tracks)) {
        $parsed['tracks'] = [];
        foreach ($media->tracks as $track) {
          $parsed['tracks'][$track->id] = [
            'id' => $track->id,
            'title' => $track->title,
            'track_number' => $track->number,
            'length' => $track->length,
          ];
        }
        uasort($parsed['tracks'], function($a, $b) {
          return $a['track_number'] <=> $b['track_number'];
        });
      }
    }
    try {
      if ($cover_data = self::findCover($release->id)) {
        $parsed['cover'] = $cover_data;
      }
    }
    catch (\Exception $e) {
      // It wasn't found. NBD.
    }
    return $parsed;
  }

  /**
   * Find the album cover if it exists.
   *
   * @param string $album_id
   *  MusicBrainz release id.
   *
   * @return string
   *  Encoded image data of the album cover.
   */
  public static function findCover($album_id) {
    $cover_data = NULL;
    $url = self::COVER_URL . $album_id;
    $client = HttpClient::create();
    $request = $client->request('GET', $url);
    if ($request->getStatusCode() != 200) {
      throw new \Exception('Request to coverartarchive.org failed with status ' . $request->getStatusCode());
    }
    $data = json_decode($request->getBody());
    if (!empty($data->images)) {
      $cover_data = reset($data->images);
    }
    return $cover_data;
  }
}