Extracting Minecraft Music with Python

I create a Python script to extract music files from Minecraft's assets.

My son loves Minecraft. (Witness the evidence from his YouTube channel.) He even loves the music of Minecraft, particularly the music during the credits in the 1.13 version; and will sometimes put on the credits to listen. We wanted to be able to play this music at his upcoming birthday party, without having to bring a computer and have Minecraft running.

minecraft-raft-clash.png
Figure 1: This is the two of us playing the Minecraft Minigame "Raft Clash"

My wife found a tutorial for extracting the music, but we couldn't get the example code to run on our Macs. However, I could understand what it was trying to do sufficiently to write my own version. I decided to write it in Python, because parsing JSON with grep offends my sensibilites1. I'm not a Python expert though, so please don't use this as an example of how you should write Python!

Through this I learnt about how Minecraft stores its assets. The core idea (for our purpose, anyway) is that each Minecraft version has an index file containing a list of all its assets, and their SHA1 hash and their size. Here is an example:

{
  "objects": {
    "icons/icon_16x16.png": {
      "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a",
      "size": 3665
    },
    "minecraft/sounds/music/game/water/axolotl.ogg": {
      "hash": "ee92e4ed79b3c4c47eabe71b36375b5d3f05b017",
      "size": 10423856
    }
  }
}

These hashes attached to the asset names corresponds to file names in the objects directory, which is the asset we want. The names are the SHA1 hash of their contents. This is pretty neat, and allows Minecraft to store assets for multiple versions efficiently. If an asset changes from one version to the next, only the checksum in that version's index file needs to change, and the corresponding object added to the objects directory.

We wanted music from the most recent Minecraft version. To avoid having to edit the script (or make it take arguments) whenever we install an update, I decided to make the script automatically find the most recent version. It does that by listing all the indices files (line 11), sort the list, and take the most recent (line 12).

To play it safe, we wanted to be able to run the script multiple times without things breaking or overwriting previously extracted music. To do this I put the music files in a versioned directory under ~/Music/Minecraft. I removed the file extension (i.e. the .json bit) from the index filename, and used that as the version number (see line 15).

We were only interested in music, and like the existing script only extracted all the assets with "music" in their name. We also wanted to keep the context provided by the full asset name. (It's not immediately obvious to me that "axolotl" has to do with water.) This means I had to create the full directory path to the extracted file, however, which I do at line 34. We have to check if the directory already exists first, otherwise the os.makedirs function throws an exception2.

The find (line 22) function3 iterates over all the files and directories under the directory in its first argument, and returns the full path if it finds a file whose name matches the second argument. In our case we pass it the assets directory and the object hash, see line 29.

The full code is below.

 1: #!/usr/bin/env python
 2: import os
 3: import json
 4: from shutil import copyfile
 5: 
 6: # This is where vanilla Minecraft stores its assets
 7: assets_path = os.path.expanduser(
 8:    "~/Library/Application Support/minecraft/assets")
 9: 
10: indices_dir = os.path.join(assets_path, "indexes")
11: indices = os.listdir(indices_dir)
12: index_file = sorted(indices)[-1]
13: index_path = os.path.join(indices_dir, index_file)
14: 
15: version, _ = os.path.splitext(index_file)
16: output_dir = os.path.expanduser(
17:     "~/Music/Minecraft/%s/" % version)
18: 
19: with open(index_path, "r") as read_file:
20:     objects = json.load(read_file)["objects"]
21: 
22: def find(name, path):
23:     for root, dirs, files in os.walk(path):
24:         if name in files:
25:             os.path.join(root, name)
26: 
27: for k in objects:
28:     if "music" in k:
29:         asset_path = find(objects[k]["hash"], assets_path)
30:         outfile = os.path.join(output_dir, k)
31:         print("Extracting %s..." % outfile)
32: 
33:         if not os.path.exists(os.path.dirname(outfile)):
34:             os.makedirs(os.path.dirname(outfile))
35:         copyfile(asset_path, outfile)

There you have it! You can use this on macOS to extract the music from Vanilla Minecraft4. One improvement I considered, but ultimately decided was not needed (yet) was the ability to specify which version to extract the music from, and the directory to extract the music to.

1
That, and my wife's computer doesn't have jq installed.
2
I don't know if the Pythonic way would be to just try it, and catch the exception.
3
I found this on Stack Overflow, I think.
4
I know from first-hand experience that it does not work if you use the MultiMC launcher.

Author: Stig Brautaset

Validate