GSoC 2021: Complete Rust binding for the MusicBrainz API

Hi Everyone! I am Ritiek Malhotra (ritiek on IRC) and recently completed my undergraduate degree in Computer Science and Engineering. I participated in Google Summer of Code ’21 and worked on musicbrainz_rs – a library wrapper on the MusicBrainz Web API written for the Rust programming language.

Why MetaBrainz?

When the program was announced, I was not familiar with MetaBrainz. I’ve heard about MetaBrainz quite a few times before this but hadn’t used any of their products. I’d been looking for Rust projects through the GSoC organizations page, and that’s how I stumbled on MetaBrainz. I learned about the MusicBrainz project and the entire concept seemed very interesting to me (and it still does!). Initially, I was a little intimidated by all the Rust concepts that musicbrainz_rs made use of – macros, traits, generics and lifetimes. I knew how to write basic Rust but not so much about these intermediate concepts. Nonetheless, I tried to learn more about the library, crafted a few small contributions and we were able to work it out from there!

The library still had to cover up some missing features from the MusicBrainz Web API and that’s what my proposal mainly focused on. In short, I proposed to add support for the Cover Art Archive, search capabilities, and implement auto-retries on queries that get rate-limited.

If you’ve no idea what I’m talking about, last month I also wrote an introductory post to MusicBrainz, its Web API, and my work on musicbrainz_rs, which you can check out here.

All about the journey!

I’ve already begun working on implementing the cover art feature during the application period; I mainly wanted to know if this project was something I’d be able to take on. My mentor, Paul, had been very helpful during these initial stages when I’d been struggling to get a good grasp of the code base, and I was able to implement a fetch_coverart trait method to access release coverarts. I also wrote some test cases during this period in the hopes to get familiar with what I would be dealing with as in both the MusicBrainz Web API and our library. I think it worked out pretty well and I eventually worked out a proposal!

I got a little more involved with the project during the decisive period but didn’t make any noticeable contributions during this time. It was a happy moment when I checked out that my proposal had been selected. I had a slow start since I had my finals during the bonding period. I made attempts to get more familiar with the code base once I was done with them – I fixed a bug involving fuzzy search on artists and involved in a tiny bit of refactoring. A little later, I worked out a get_coverart method to access cover arts. This was more of an alternate design implementation to what I’ve previously worked on during the application period. I also implemented a way to make calls to fetch for cover arts of specific type and resolution through the builder pattern our library already made use of.

Once we were done with adding cover art capabilities, which the users can call through:

use musicbrainz_rs::entity::release::*;
use musicbrainz_rs::entity::CoverartResponse;
use musicbrainz_rs::prelude::*;
use musicbrainz_rs::FetchCoverart;

The fetch_coverart trait:

let in_utero_coverart = Release::fetch_coverart()
    .id("76df3287-6cda-33eb-8e9a-044b5e15ffdd")
    .execute()
    .expect("Unable to get cover art");

if let CoverartResponse::Json(coverart) = in_utero_coverart {
    assert!(!coverart.images[0].back);
    assert_eq!(
        coverart.images[0].image,
        "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842.jpg"
    );
} else {
    assert!(false);
}

The get_coverart method:

let in_utero = Release::fetch()
    .id("76df3287-6cda-33eb-8e9a-044b5e15ffdd")
    .execute()
    .expect("Unable to get release");

// Calling `get_coverart()` method on an already fetched Release entity.
let in_utero_coverart = in_utero
    .get_coverart()
    .execute()
    .expect("Unable to get coverart");

if let CoverartResponse::Json(coverart) = in_utero_coverart {
    assert!(!coverart.images[0].back);
    assert_eq!(
        coverart.images[0].image,
        "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842.jpg"
    );
} else {
    assert!(false);
}

The cover art builder pattern to make calls to access a cover art of specific type or resolution:

let in_utero_500px_front_coverart = Release::fetch_coverart()
    .id("76df3287-6cda-33eb-8e9a-044b5e15ffdd")
    .res_500()
    .back()
    .execute()
    .expect("Unable to get cover art");

if let CoverartResponse::Url(coverart_url) = in_utero_500px_front_coverart {
    println!("{}", coverart_url);
} else {
    assert!(false);
}

(These can also be executed through the fetch_release_coverart.rs example)

I then worked on implementing auto-retries. I want to emphasize on how important this was to us. We’ve previously had trouble running our test-suite due to the lack of auto-retries. Our test-suite is constantly making queries to MusicBrainz servers and the queries would start to fail after a while due to rate-limitations imposed by MusicBrainz. So we had ugly hacks in-place to get our test-suite to pass. We had introduced a one second sleep in our test-suite after every call to the MusicBrainz Web API to not trigger their rate-limitations, and this had been greatly increasing our test-suite run times.

I’m glad this is no longer the case after introducing auto-retries! The library now auto-retries queries failed due to rate-limiting by the MusicBrainz servers. In case of a failed query due to rate-limiting, we’re returned with the time duration in the response header until the next query would be accepted by the MusicBrainz servers. The library now automatically sleeps the current thread for this received duration and retries the query by default. The default is set to 2 retries per failed query and this number can also be changed if required through:

musicbrainz_rs::config::set_default_retries(3);

At this point, we were a little uncertain on the design part of adding search capabilities on all the entities supported by MusicBrainz, so I worked on adding relationship includes and other small nit-picks. You can request for relationship includes in your queries through:

let ninja_tune = Label::fetch()
    .id("dc940013-b8a8-4362-a465-291026c04b42")
    .with_recording_relations()
    .execute()
    .unwrap();

let relations = ninja_tune.relations.unwrap();

assert!(relations
    .iter()
    .any(|rel| rel.relation_type == "phonographic copyright"));

I also added support for relationship level includes which can also be requested for in a similar fashion:

let polly = Recording::fetch()
    .id("af40d6b8-58e8-4ca5-9db8-d4fca0b899e2")
    .with_work_relations()
    .with_work_level_relations()
    .execute()
    .unwrap();

let relations = polly.relations.unwrap();

assert!(relations.iter().any(|rel| rel.target_type == "work"));

And long story in short, we built separate entity structs for search purposes for most search entities supported in MusicBrainz.

There’s some inconsistency in the API response for the Place entity which I reported here. We should probably wait before implementing search on the Place entity and see maybe see if this can be resolved from the MusicBrainz Server side itself otherwise we’ll have to workaround this in our library as we currently parse the coordinates as f64 which fails when attempting to use the same coordinate struct to also parse the search response. On the other hand Tag entity requires http digest authentication which isn’t implemented in musicbrainz_rs at the moment. Tag search will need to be implemented once we have authentication up.

As an example, you can now search the implemented entities through:

use musicbrainz_rs::entity::area::AreaType::*;
use musicbrainz_rs::entity::area::*;
use musicbrainz_rs::Search;

let query = AreaSearchQuery::query_builder()
    .area("London")
    .and()
    .tag("place")
    .build();

let result = Area::search(query).execute().unwrap();

assert!(result
    .entities
    .iter()
    .any(|area| area.area_type.as_ref().unwrap() == &City));

These are the main things we worked upon. I also fixed some mismatches with the MusicBrainz API in our library, improved docs, and a did little bit of refactoring. Paul also started working on a showcase app using musicbrainz_rs. It’s still far from being complete but it attempts to show the neat things that can now be done using the library.

All of my pull requests made during the GSoC period can be found here.

Final thoughts

There are still quite a few things that could further be done in musicbrainz_rs as detailed in the issues section. Overall, I had a great time working on musicbrainz_rs these few months and would like to thank MetaBrainz Foundation and to the team who made review sessions so much fun, and especially my mentor, Paul, for providing me with this opportunity, letting me take the wheel for a while, and dealing with my cute questions all along the way! I’m starting to feel a tiny bit more confident in my ability to program in Rust.

I’d love to contribute to musicbrainz_rs if I get the chance in future, but for now I’ve got to focus on other things. My college is over and I’m yet to find out what awaits for the future next!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.