Skip to content

Instantly share code, notes, and snippets.

@ritiek
Last active October 8, 2022 16:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ritiek/81d9b2cedeb04bdb69b05e195ab6e6c0 to your computer and use it in GitHub Desktop.
Save ritiek/81d9b2cedeb04bdb69b05e195ab6e6c0 to your computer and use it in GitHub Desktop.
GSoC '21 Final Work Submission

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 learnt 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 CoverArt 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, the 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 Coverart 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 period. 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 and fixed a bug involving fuzzy search on artists and tiny bit of refactoring. A little later, I worked out a get_coverart method to access coverarts. 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 coverarts of specific type and resolution through the builder pattern our library already made use of.

Once we were done with adding coverart 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 coverart builder pattern to make calls to access a coverart 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 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 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 mis-matches with the web-api in our library, improved docs, and a 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 PRs made during the GSoC period can be found here.

Final thoughts

There are still quite a few things that could 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment