-
-
Save fxthomas/fd85e906e41f4e6e06f38e92a497005b to your computer and use it in GitHub Desktop.
#!/usr/bin/python | |
# coding=utf-8 | |
"""Python Script for bootstrapping a MusicBrainz release using a VGMDB album. | |
This script uses the unofficial VGMDB.info JSON API to prefill the MusicBrainz | |
"Add Release" form with data from VGMDB. | |
It is only meant as a first step to make adding a new MB release easier; please | |
check for missing/erroneous data and make sure the imported release follows the | |
MusicBrainz guidelines! | |
Because VGMDB has a lot of Japanese content, we import track/release titles from | |
this language before trying other languages, and try to guess values for some | |
fields (e.g. "Soundtrack" album types). | |
Documentation about the field format is found at: | |
https://musicbrainz.org/doc/Development/Release_Editor_Seeding | |
""" | |
import re | |
import sys | |
import json | |
import html | |
import argparse | |
import webbrowser | |
from urllib.request import urlopen | |
from tempfile import NamedTemporaryFile | |
from datetime import datetime | |
def strptimes(s, fmts): | |
for fmt in fmts: | |
try: | |
return datetime.strptime(s, fmt) | |
except ValueError: | |
continue | |
return None | |
def vgmdb_get_album_url(album_id, format_="json"): | |
"""Return the VGMDB API URL for the given album ID""" | |
return "https://vgmdb.info/album/%d?format=%s" % (album_id, format_) | |
def vgmdb_get_album_data(album_url): | |
"""Retrieve data for a VGMDB album""" | |
return json.load(urlopen(album_url)) | |
def write_musicbrainz_html_form(fd, album_data): | |
"""Write a local MusicBrainz import form containing album data""" | |
fd.write("""<!doctype html>""") | |
fd.write("""<meta charset="UTF-8">""") | |
fd.write("""<title>Add VGMDB album As Release...</title>""") | |
fd.write("""<form action="https://musicbrainz.org/release/add" method="post">""") | |
album_title = album_data['names'].get('ja') | |
album_title = album_title or next(iter(album_data["names"].values())) | |
fd.write(f"""<input type="hidden" name="name" value="{html.escape(album_title)}">""") | |
fd.write(f"""<input type="hidden" name="status" value="official">""") | |
if "soundtrack" in album_data["classification"].lower(): | |
fd.write(f"""<input type="hidden" name="type" value="album">""") | |
fd.write(f"""<input type="hidden" name="type" value="soundtrack">""") | |
if "ja" in album_data['names'].keys(): | |
fd.write(f"""<input type="hidden" name="language" value="jpn">""") | |
fd.write(f"""<input type="hidden" name="script" value="Jpan">""") | |
all_artists = [] | |
composers = [] | |
arrangers = [] | |
performers = [] | |
lyricists = [] | |
for composer_data in album_data["composers"]: | |
composer_name = composer_data["names"].get("ja") | |
composer_name = composer_name or next(iter(composer_data["names"].values())) | |
if composer_name not in all_artists: | |
all_artists.append(composer_name) | |
if composer_name not in composers: | |
composers.append(composer_name) | |
for arranger_data in album_data["arrangers"]: | |
arranger_name = arranger_data["names"].get("ja") | |
arranger_name = arranger_name or next(iter(arranger_data["names"].values())) | |
if arranger_name not in all_artists: | |
all_artists.append(arranger_name) | |
if arranger_name not in arrangers: | |
arrangers.append(arranger_name) | |
for performer_data in album_data["performers"]: | |
performer_name = performer_data["names"].get("ja") | |
performer_name = performer_name or next(iter(performer_data["names"].values())) | |
if performer_name not in all_artists: | |
all_artists.append(performer_name) | |
if performer_name not in performers: | |
performers.append(performer_name) | |
for lyricist_data in album_data["lyricists"]: | |
lyricist_name = lyricist_data["names"].get("ja") | |
lyricist_name = lyricist_name or next(iter(lyricist_data["names"].values())) | |
if lyricist_name not in all_artists: | |
all_artists.append(lyricist_name) | |
if lyricist_name not in lyricists: | |
lyricists.append(lyricist_name) | |
artists = ["Various Artists"] if len(performers) >= 3 else all_artists | |
track_artists = performers or arrangers or composers or lyricists or all_artists | |
join_phrase = ", " | |
for artist_ix, artist_name in enumerate(artists): | |
fd.write(f"""<input type="hidden" name="artist_credit.names.{artist_ix}.artist.name" value="{html.escape(artist_name)}">""") | |
if artist_ix < len(artists)-1: | |
fd.write(f"""<input type="hidden" name="artist_credit.names.{artist_ix}.join_phrase" value="{html.escape(join_phrase)}">""") | |
release_date = strptimes(album_data['release_date'], ["%Y-%m-%d", "%Y"]) | |
if release_date: | |
fd.write(f"""<input type="hidden" name="events.0.date.year" value="{release_date.year}">""") | |
fd.write(f"""<input type="hidden" name="events.0.date.month" value="{release_date.month}">""") | |
fd.write(f"""<input type="hidden" name="events.0.date.day" value="{release_date.day}">""") | |
if "ja" in album_data['names'].keys(): | |
fd.write(f"""<input type="hidden" name="events.0.country" value="JP">""") | |
catalog_nr = album_data['catalog'] | |
fd.write(f"""<input type="hidden" name="labels.0.catalog_number" value="{html.escape(catalog_nr)}">""") | |
vgmdb_link = album_data['vgmdb_link'] | |
fd.write(f"""<input type="hidden" name="urls.0.url" value="{html.escape(vgmdb_link)}">""") | |
fd.write(f"""<input type="hidden" name="urls.0.link_type" value="86">""") # VGMDB | |
fd.write(f"""<input type="hidden" name="edit_note" value="Imported from {html.escape(vgmdb_link)}">""") | |
for disc_ix, disc_data in enumerate(album_data["discs"]): | |
if album_data["media_format"] == "CD": | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.format" value="CD">""") | |
for track_ix, track_data in enumerate(disc_data["tracks"]): | |
track_title = track_data["names"].get("Japanese") | |
track_title = track_title or next(iter(track_data["names"].values())) | |
if track_data["track_length"] and track_data["track_length"].lower() != "unknown": | |
track_length = datetime.strptime(track_data["track_length"], "%M:%S") | |
track_length = 1000 * (track_length.minute*60 + track_length.second) | |
else: | |
track_length = 0 | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.name" value="{html.escape(track_title)}">""") | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.length" value="{track_length}">""") | |
for artist_ix, artist_name in enumerate(track_artists): | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.artist_credit.names.{artist_ix}.mbid" value="">""") | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.artist_credit.names.{artist_ix}.name" value="{html.escape(artist_name)}">""") | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.artist_credit.names.{artist_ix}.artist.name" value="{html.escape(artist_name)}">""") | |
fd.write(f"""<input type="hidden" name="mediums.{disc_ix}.track.{track_ix}.artist_credit.names.{artist_ix}.join_phrase" value=", ">""") | |
fd.write("""<input type="submit" value="Add Cluster As Release...">""") | |
fd.write("""</form>""") | |
fd.write("""<script>document.forms[0].submit()</script>""") | |
# Parse arguments | |
parser = argparse.ArgumentParser(description=__doc__) | |
parser.add_argument("album_id_url", help="VGMDB album id or URL", type=str) | |
parser.add_argument("--show-api-page", "-s", help="Show API page instead of the MB form", action="store_true") | |
args = parser.parse_args() | |
# Parse album ID | |
album_id = None | |
m = re.match(r"(https?://)?vgmdb.net/album/(?P<album_id>\d+).*", args.album_id_url) | |
if m: | |
album_id = int(m.group("album_id")) | |
elif args.album_id_url.isdigit(): | |
album_id = int(args.album_id_url) | |
else: | |
print("Invalid album ID or URL") | |
sys.exit(1) | |
# Retrieve album URL | |
if args.show_api_page: | |
album_url = vgmdb_get_album_url(album_id, format_="html") | |
print("Opening %s" % album_url) | |
webbrowser.open(album_url) | |
sys.exit(0) | |
# Retrieve album data, write and open MusicBrainz form | |
album_url = vgmdb_get_album_url(album_id) | |
album_data = vgmdb_get_album_data(album_url) | |
with NamedTemporaryFile(suffix=".html", encoding="utf-8", mode="w+", delete=False) as fd: | |
write_musicbrainz_html_form(fd, album_data) | |
print("Opening %s" % fd.name) | |
webbrowser.open(fd.name) |
@Tenome Thanks, never got this kind of album before. Updated so it works with year-only dates!
(That's a totally obscure release by the way, I was curious but did not find it anywhere online!)
@fxthomas Might need to be updated again? I tried this URL and it gave me an internal server error, but that might just be a problem on VGMDB's end. The other VGMDB userscript also doesn't seem to work anymore, so it could be that VGMDB updated (again). Here's the album I tried: https://vgmdb.net/album/105445
Traceback (most recent call last):
\Scripts\vgmdb2mb.py", line 183, in
album_data = vgmdb_get_album_data(album_url)
\Scripts\vgmdb2mb.py", line 48, in vgmdb_get_album_data
return json.load(urlopen(album_url))
\lib\urllib\request.py", line 222, in urlopen
return opener.open(url, data, timeout)
\lib\urllib\request.py", line 531, in open
response = meth(req, response)
\lib\urllib\request.py", line 640, in http_response
response = self.parent.error(
\lib\urllib\request.py", line 569, in error
return self._call_chain(*args)
\lib\urllib\request.py", line 502, in _call_chain
result = func(*args)
\lib\urllib\request.py", line 649, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 500: Internal Server Error
The API endpoint this script uses is hosted at http://vgmdb.info which is separate from the VGMDB website. It's sometimes offline, but usually gets back up after a while.
The script breaks if the VGMDB page only has the year, just FYI, since it expects the full date format.
https://vgmdb.net/album/20652
Thanks for the script though, it's been very useful. The other MB VGMDB script doesn't work half the time.