beatsaver.com がプログラムからアクセスできる API を公開してくれているので、これは触りにいかないといけないぞということで全力でデータを落としてみました
この記事は Beat Saber Advent Calendar 2023 の4日目です。(センシティブ?なのかBANされちった。でも技術は悪くない。悪いのはいつも技術を利用する人間)
最近 twitch 配信のツールなどを調べてたら、おっ!?っというページに辿り着いてしまったのが始まりでした
https://api.beatsaver.com/docs/index.html?url=./swagger.json
これ beatsaver.com のサイトを構築するためのデータを全部提供してね?
ドキュメント(自動生成の)がちゃんとまとまってる!
API を何回叩いてもとくに制限がかからないので叩きたい放題だ!
エンジニアにとって魅力的な、あるいみ魔力が詰まってるので、やってしまうのは不可抗力なのです
よし、音楽ファイルをごっそり抜こう!
もくじ
- 全体の流れとか
- とある期間の譜面をランキング順でとってくる
- マッパーさんの作成した譜面をとってくる
- せっかく譜面情報をとったので表示してみる
- mp3ファイルをブッコぬく
- おまけのようなメインのような翻訳AI
全体の流れとか
- 譜面情報をとってくる(zipファイルへのURLが書かれたjson)
- zipファイルをダウンロード
- zipファイル内のeggファイルをoggに名前を変えてmp3に変換
- たいりょうにダウンロード
まずは、たくさんの譜面の情報をゲットするところから始まります。その中にzipファイルへのURLが書かれているのでダウンロードしていきます。
zipファイルの中から音楽ファイルだけ取り出してmp3に変換します。
あとはこれをひたすら繰り返すのみです!
この記事に出てくるコードや出力結果は全てGithubにあげてあります。
でも抜き出したmp3はアップしてません。著作権たいせつ
とある期間の譜面をランキング順でとってくる
それでは譜面データをとっていきましょう。
今回は 2022年12月1日 から 2023年11月30 日までのランキング上位の譜面を10,000曲とってきます。
全体のコードはこんな。
import json
import math
import requests
import time
def main():
FROM_DATA = "2022-12-01"
TO_DATA = "2023-11-30"
BARS_PER_PAGE = 20
FETCH_BARS = 10000
PAGES = math.ceil(FETCH_BARS / BARS_PER_PAGE)
bsrs = []
# 1. 譜面データを取得する
for i in range(PAGES):
page = i
url = f"https://api.beatsaver.com/search/text/{page}?from={FROM_DATA}&to={TO_DATA}&sortOrder=Rating"
print(f"Fetching page: {page + 1} / {PAGES}")
response = requests.get(url)
if response.status_code == 200:
response_json = response.json()
for bsr in response_json["docs"]:
bsrs.append(bsr)
else:
print(f"Error fetching data for {url}")
time.sleep(5)
# 2. id, タイトルなどをファイルに保存
with open("bsrs_ranking_summary.txt", "w", encoding="utf-8") as list_file:
for i, bsr in enumerate(bsrs):
print(
f"{i + 1}: {bsr['id']} [+{bsr['stats']['upvotes']} {bsr['stats']['score']}]: {bsr['name']}"
)
list_file.write(
f"{i + 1}: {bsr['id']} [+{bsr['stats']['upvotes']} {bsr['stats']['score']}]: {bsr['name']}\n"
)
# 3. 全ての情報をファイルに保存
with open("bsrs_ranking.json", "w", encoding="utf-8") as json_file:
json.dump(bsrs, json_file, ensure_ascii=False, indent=4)
if __name__ == "__main__":
main()
処理はおおまかに3段階に別れています。
- 譜面データを取得する
- id, タイトルなどをファイルに保存(bsrs_ranking_summary.txt)
- 全ての情報をファイルに保存(bsrs_ranking.json)
初めてのコードなので、1に出てくるAPI呼び出しについて説明します。
今回使用する API は譜面の検索です。 APIのURLは https://api.beatsaver.com/search/text/{page}
になります。
beatsaver.com のドキュメントだとこの部分になります。
from を 2022-12-01
to を 2023-11-30
sortOrder を Rating
に設定することで、この期間の譜面をランキング順でとってこれます。
1回のAPI呼び出しで20譜面を取得できるので、毎回pageを+1しながら500回呼び出します。
FROM_DATA = "2022-12-01"
TO_DATA = "2023-11-30"
BARS_PER_PAGE = 20
FETCH_BARS = 10000
PAGES = int(FETCH_BARS / BARS_PER_PAGE)
bsrs = []
# 譜面データを取得する
for i in range(PAGES):
page = i
url = f"https://api.beatsaver.com/search/text/{page}?from={FROM_DATA}&to={TO_DATA}&sortOrder=Rating"
bars
に譜面データがどんどん入っていくので、全てが終わったあとに bsrs_ranking_summary.txt
と bsrs_ranking.json
に保存します。
bsrs_ranking_summary.txt
は全体が把握しやすいようにbsr番号
といいね数
とスコア
とタイトル
だけ入れてあります。bsrs_ranking.json
には取得した全てのデータが入っています。
1: 1e6ff [+14569 0.9899]: {Modchart} acloudyskye - Somewhere Out There
2: 212cf [+2221 0.9883]: Someone Else's Hat - David Maxim Micic (modchart)
3: 23f6b [+1992 0.9877]: acloudyskye & N33T - Nothing Else (Modchart)
4: 1dcf4 [+2573 0.9862]: DYES IWASAKI - Bad Hatter ft. Lily Mizusaki
5: 210e3 [+5939 0.9856]: [Modchart] Sarah Cothran - As The World Caves In
6: 1ca5d [+1121 0.9839]: Calvin Harris - Sweet Nothing (feat. Florence Welch)
7: 241c5 [+855 0.9834]: GIRI GIRI (TV Size) [Kaguya-sama: Love Is War Season 3 Opening] - Masayuki Suzuki ft. Suu
8: 271dc [+1053 0.9831]: [Electro Swing Pack Vol. 2] Swingrowers - Butterfly (Wolfgang Lohr Remix)
9: 27a13 [+1704 0.9827]: Jimmy Eat World - The Middle [Arcs][V3 Lights]
10: 2c785 [+1546 0.9824]: [Arc/Chain] Let's Groove - Earth, Wind & Fire
...省略...
[
{
"id": "35c11",
"name": "[Arc/Chain] September - Earth, Wind & Fire",
"description": "My second Earth, Wind & Fire map! The last one I did (Let's Groove) is one of my favorite maps I've worked on. I've been thinking about doing this song for a while now so I thought it would be a great idea to upload it on September 21st. \r\n\r\nDon't forget to turn off static lighting to see the V3 light show on the Lizzo environment. Enjoy!\r\n\r\n",
"uploader": {
"id": 4232648,
"name": "ramenator05",
"hash": "60ecf439686b9f0006a67a41",
"avatar": "https://cdn.beatsaver.com/avatar/4232648.jpg",
"type": "SIMPLE",
"admin": false,
"curator": false,
"verifiedMapper": true,
"playlistUrl": "https://api.beatsaver.com/users/id/4232648/playlist"
},
"metadata": {
"bpm": 120.0,
"duration": 217,
"songName": "September",
"songSubName": "",
"songAuthorName": "Earth Wind & Fire",
"levelAuthorName": "ramenator05"
},
...省略...
それぞれの中身のすべてはこのリンクにあるけど、気をつけてください。それぞれ1万行と100万行あります。データに飲み込まれないように。
akiraak/beatsaver-com-bukkonuki/blob/main/bsrs_ranking_summary.txt
akiraak/beatsaver-com-bukkonuki/blob/main/bsrs_ranking.json
マッパーさんの作成した譜面をとってくる
オキニのマッパーさんっているよね。そんなときはマッパーさんが作成した譜面だけとってこれます。こんかいは misterlihao さんが作成した譜面をとってきます。
全体のコードはこんな。
import json
import requests
import time
# 1. マッパーさんの譜面を取得する
def user_bsr_ids(user_id: str):
url = f"https://api.beatsaver.com/users/id/{user_id}/playlist"
bsr_ids = []
response = requests.get(url)
if response.status_code == 200:
response_json = response.json()
for song in response_json["songs"]:
bsr_ids.append(song["key"])
else:
print(f"Error fetching data for {url}")
return bsr_ids
def main():
BSRS_PER_ONCE = 50
USER_ID = "4284977" # misterlihao
bsr_ids = user_bsr_ids(USER_ID)
bsrs = []
# 2. 譜面データを取得する
for i in range(0, len(bsr_ids), BSRS_PER_ONCE):
fetch_ber_ids = bsr_ids[i : i + BSRS_PER_ONCE]
ids_param = ",".join(fetch_ber_ids)
url = f"https://api.beatsaver.com/maps/ids/{ids_param}"
response = requests.get(url)
if response.status_code == 200:
response_json = response.json()
print(
f"Fetching {len(response_json)} bsrs: {i + 1} - {i + len(response_json)} / {len(bsr_ids)}"
)
for id, bsr in response_json.items():
bsrs.append(bsr)
else:
print(f"Error fetching data for {url}")
time.sleep(5)
# 3. id, タイトルなどをファイルに保存
with open("bsrs_mapper_misterlihao_summary.txt", "w", encoding="utf-8") as list_file:
for i, bsr in enumerate(bsrs):
print(
f"{i + 1}: {bsr['id']} [+{bsr['stats']['upvotes']} {bsr['stats']['score']}]: {bsr['name']}"
)
list_file.write(
f"{i + 1}: {bsr['id']} [+{bsr['stats']['upvotes']} {bsr['stats']['score']}]: {bsr['name']}\n"
)
# 4. 全ての情報をファイルに保存
with open("bsrs_mapper_misterlihao.json", "w", encoding="utf-8") as json_file:
json.dump(bsrs, json_file, ensure_ascii=False, indent=4)
if __name__ == "__main__":
main()
ランキングと処理が少し変わって全部で4段階になってます。
- マッパーさんの譜面IDを取得する
- 譜面データを取得する
- id, タイトルなどをファイルに保存(bsrs_mapper_misterlihao_summary.txt)
- 全ての情報をファイルに保存(bsrs_mapper_misterlihao.json)
まずは 1 で、マッパーさんの作成した譜面はマッパーさんのプレイリストに入ってます。それをとってきます。
API は https://api.beatsaver.com/users/id/{user_id}/playlist
です。
でもドキュメントには書いてない。なんでやねん
user_id
には misterlihao ではなく数値のIDを入れる必要があります。
ここでわかります。
2 は譜面IDを複数渡すとその譜面データが返ってくる。でも一度に最大50個までしか指定できないのです。
misterlihao さんは247個の譜面(昨日まで246だったのにまた増えた)なので、50x5回呼んであげると全ての譜面データがとれます。
3 と 4 はランキングと同じなので省略。
1: 350a4 [+133 0.9582]: 忘れるね ver.日南めい - 雲下ミルクティー
2: 37557 [+33 0.9178]: 1212。 - エイハブ
3: 3320f [+67 0.9465]: Good night feat. 鏡音リン(Rin) - めろくる(Mellowcle)
4: 34a16 [+110 0.9525]: ヴァニタスと常夜燈 feat. 裏命 - 道端の石(Michi Bata)
5: 36058 [+95 0.9385]: リーラ feat.可不 - sawayaka
6: 33b99 [+165 0.9476]: スターダスト - 雨宿り(Amayadori)
7: 370d5 [+63 0.9444]: シュガーハイヴ covered by 梓川 - 雄之助(Yunosuke)
8: 32914 [+198 0.9588]: 大黒天 feat.よーい - RuLu
9: 32b9d [+103 0.9336]: ホンメイ feat. Nqsi - Neo
10: 35772 [+173 0.9597]: 転生したら可愛かった feat.かぴ - HoneyWorks
...省略...
保存したファイルの全ての中身はここ
akiraak/beatsaver-com-bukkonuki/blob/main/bsrs_mapper_misterlihao_summary.txt
akiraak/beatsaver-com-bukkonuki/blob/main/bsrs_mapper_misterlihao.json
しかし misterlihao さんのは好きな曲(?)が多い。
Bluerose
猫ならばいける
NENENENENENENENE Daibakusou
Homenobi
My name is elite
ちな、ビーセイビュワーは直リンクのフルスクリーンで開けます。
https://allpoland.github.io/ArcViewer/?id=bsr_id
Q - https://allpoland.github.io/ArcViewer/?id=2552f
開けるけど、ここはJASRACと契約してないだろうし、ヤバいかも?
せっかく譜面情報をとったので表示してみる
データがそろった。データが手元にあったら表示してみたいのはエンジニアの本能なのでしかたがありません。
表示するだけのコードです。
import argparse
import json
import os
def get_bsr(bar_id: str):
bsrs_json_files = ["bsrs_ranking.json", "bsrs_mapper_misterlihao.json"]
for bsrs_json_file in bsrs_json_files:
if not os.path.exists(bsrs_json_file):
print(f"! File not found: {bsrs_json_file}")
continue
with open(bsrs_json_file, "r", encoding="utf-8") as json_file:
bsrs = json.load(json_file)
for bsr in bsrs:
if bsr["id"] == bar_id:
return bsr
return None
def show_detail(bar_id: str):
bsr = get_bsr(bar_id)
if not bsr:
print(f"bsr not found: {bar_id}")
return
print(f"ID: {bsr['id']}")
print(f"Name: {bsr['name']}")
print(f"Description: {bsr['description']}")
print(f"Mapper: {bsr['uploader']['name']}")
print(f"BPM: {bsr['metadata']['bpm']}")
minutes, seconds = divmod(bsr['metadata']['duration'], 60)
print(f"Duration: {minutes:02d}:{seconds:02d}")
print(f"👍Upvotes: {bsr['stats']['upvotes']}")
print(f"👎Downvotes: {bsr['stats']['downvotes']}")
print(f"Score: {bsr['stats']['score']}")
print(f"tags: {bsr['tags'] if 'tags' in bsr else ''}")
print(f"downloadURL: {bsr['versions'][0]['downloadURL']}")
print(f"coverURL: {bsr['versions'][0]['coverURL']}")
print(f"previewURL: {bsr['versions'][0]['previewURL']}")
print(f"bsaber.com: https://bsaber.com/songs/{bar_id}/")
print(f"beatsaver.com: https://beatsaver.com/maps/{bar_id}")
print(f"Viewer: https://allpoland.github.io/ArcViewer/?id={bar_id}")
if 'curatedAt' in bsr:
print(f"curatedAt: {bsr['curatedAt']}")
print(f"createdAt: {bsr['createdAt']}")
print(f"updatedAt: {bsr['updatedAt']}")
print(f"lastPublishedAt: {bsr['lastPublishedAt']}")
print("")
print("--- Maps ---")
for bsr_map in bsr['versions'][0]['diffs']:
print(f"{bsr_map['difficulty']}:")
print(f" Offset: {bsr_map['offset']}")
print(f" NJS: {bsr_map['njs']}")
print(f" Notes: {bsr_map['notes']}")
print(f" Bombs: {bsr_map['bombs']}")
print(f" Obstacles: {bsr_map['obstacles']}")
print(f" NPS: {bsr_map['nps']}")
print(f" Length: {bsr_map['length']}")
print(f" Characteristic: {bsr_map['characteristic']}")
print(f" Events: {bsr_map['events']}")
print(f" Chroma: {bsr_map['chroma']}")
print(f" Me: {bsr_map['me']}")
print(f" Ne: {bsr_map['ne']}")
print(f" Cinema: {bsr_map['cinema']}")
print(f" Seconds: {bsr_map['seconds']}")
print(f" MaxScore: {bsr_map['maxScore']}")
def main():
parser = argparse.ArgumentParser(description="譜面の詳細を表示する")
parser.add_argument("--bsr", type=str, help="bsr ID")
args = parser.parse_args()
show_detail(bar_id=args.bsr)
if __name__ == "__main__":
main()
実行時に --bsr
を指定すると詳細を表示してくれます。
$ python show_bsr_detail.py --bsr 1b06c
ID: 1b06c
Name: My name is elite ☆ - sakura miko マイネームイズエリート☆ さくらみこ
Description: My name is elite ☆ origin by sakura miko
マイネームイズエリート☆ by さくらみこ
This map is dancy.
preview: https://youtu.be/el54FOlnVkg
Mapper: misterlihao
BPM: 181.0
Duration: 03:57
👍Upvotes: 34
👎Downvotes: 3
Score: 0.8546
tags:
downloadURL: https://r2cdn.beatsaver.com/509a5e52fd3db260b32d6dd45402763385b32916.zip
coverURL: https://na.cdn.beatsaver.com/509a5e52fd3db260b32d6dd45402763385b32916.jpg
previewURL: https://na.cdn.beatsaver.com/509a5e52fd3db260b32d6dd45402763385b32916.mp3
bsaber.com: https://bsaber.com/songs/1b06c/
beatsaver.com: https://beatsaver.com/maps/1b06c
Viewer: https://allpoland.github.io/ArcViewer/?id=1b06c
createdAt: 2021-08-08T14:46:25.577524Z
updatedAt: 2021-08-08T14:46:25.577524Z
lastPublishedAt: 2021-08-08T14:46:25.577524Z
--- Maps ---
Expert:
Offset: 1.0
NJS: 14.0
Notes: 577
Bombs: 615
Obstacles: 170
NPS: 2.519
Length: 691.0
Characteristic: Standard
Events: 1116
Chroma: False
Me: False
Ne: False
Cinema: False
Seconds: 229.061
MaxScore: 523595
エリートみこちは NPS 2.5 だけど配置は面白いから Expert だね。
そいえば Twitch で Normal を斬ってたプレイヤーさんが難しすぎて死んでたけど NPS みたら 5.5 もあってめっちゃ怒ってた
そうだ。とってきたデータからプレイヤーさんにあった譜面を検索してみよう。まだ初心者さんらしく、見ている感じだと NPS は 3 〜 4あたりが良さそう。
コードはこんな
import argparse
import json
import os
# min_nps から max_nps のマップを含む曲を取得する
def get_bsrs(min_nps: float, max_nps: float):
bsrs_json_files = ["bsrs_ranking.json", "bsrs_mapper_misterlihao.json"]
bsrs = []
for bsrs_json_file in bsrs_json_files:
if not os.path.exists(bsrs_json_file):
print(f"! File not found: {bsrs_json_file}")
continue
with open(bsrs_json_file, "r", encoding="utf-8") as json_file:
bsrs_json = json.load(json_file)
for bsr_json in bsrs_json:
for bsr_map in bsr_json['versions'][0]['diffs']:
if min_nps <= bsr_map['nps'] and bsr_map['nps'] <= max_nps:
bsrs.append(bsr_json)
break
return bsrs
def show_bsrs(show_count: int, min_nps: float, max_nps:float):
print(f"min_nps: {min_nps} max_nps: {max_nps}")
print("")
bsrs = get_bsrs(min_nps=min_nps, max_nps=max_nps)
# bsr['stats']['score'] で降順にソート
bsrs = sorted(bsrs, key=lambda bsr: bsr['stats']['score'], reverse=True)
for bsr in bsrs[:show_count]:
hit = False
for bsr_map in bsr['versions'][0]['diffs']:
if min_nps <= bsr_map['nps'] and bsr_map['nps'] <= max_nps:
hit = True
break
if hit:
print(f"{bsr['id']} [+{bsr['stats']['upvotes']} {bsr['stats']['score']}] {bsr['name']}")
minutes, seconds = divmod(bsr['metadata']['duration'], 60)
print(f" ", end="")
for bsr_map in bsr['versions'][0]['diffs']:
if min_nps <= bsr_map['nps'] and bsr_map['nps'] <= max_nps:
print(f"[{bsr_map['difficulty']} NPS: {bsr_map['nps']}]", end=" ")
print(f"Duration: {minutes:02d}:{seconds:02d} BPM: {bsr['metadata']['bpm']}\n")
print(f"Showed {show_count} / {len(bsrs)} bsrs")
def main():
parser = argparse.ArgumentParser(description="NPSで絞り込んだ譜面を表示する")
parser.add_argument("--show-bsrs", type=int, help="表示する譜面数")
parser.add_argument("--min-nps", type=float, help="Min NPS")
parser.add_argument("--max-nps", type=float, help="Max NPS")
args = parser.parse_args()
show_bsrs(show_count=args.show_bsrs, min_nps=args.min_nps, max_nps=args.max_nps)
if __name__ == "__main__":
main()
NPS の最小と最大と表示する件数を指定するよ。表示する順番はランキング順!
NPS 3.0 から 3.5 で上位10件を表示してみよう。
$ python show_bsrs_nps.py --show-bsrs 10 --min-nps 3.0 --max-nps 3.5
min_nps: 3.0 max_nps: 3.5
32693 [+1018 0.9798] The Phoenix - Fall Out Boy
[Hard NPS: 3.19] [ExpertPlus NPS: 3.394] Duration: 04:06 BPM: 138.0
35c11 [+424 0.9785] [Arc/Chain] September - Earth, Wind & Fire
[Expert NPS: 3.083] [ExpertPlus NPS: 3.226] Duration: 03:37 BPM: 120.0
31a3b [+438 0.9769] PONPONPON - Kyary Pamyu Pamyu
[Hard NPS: 3.014] Duration: 04:04 BPM: 128.0
2fbdb [+471 0.976] NOTD, Astrid S - I Don't Know Why (Ellis Remix)
[ExpertPlus NPS: 3.325] Duration: 03:21 BPM: 120.0
331e2 [+618 0.9758] [Eurovision 2023 Pack] Käärijä - Cha Cha Cha
[Hard NPS: 3.168] Duration: 02:57 BPM: 155.0
2f939 [+240 0.9757] シビレキラシテ feat. 可不 - Adeliae
[Hard NPS: 3.254] Duration: 01:15 BPM: 190.0
2f939 [+239 0.9757] シビレキラシテ feat. 可不 - Adeliae
[Hard NPS: 3.254] Duration: 01:15 BPM: 190.0
2f266 [+510 0.9756] Canned Heat - Jamiroquai
[Expert NPS: 3.399] Duration: 03:45 BPM: 128.0
33c66 [+582 0.9746] Ao no Sumika (TV Size) [Jujutsu Kaisen Season 2 Opening] - Tatsuya Kitani
[Hard NPS: 3.268] Duration: 01:31 BPM: 152.0
30a24 [+385 0.9746] Eliminate - Open Your Eyes
[Expert NPS: 3.467] Duration: 03:23 BPM: 150.0
Showed 10 / 1596 bsrs
Hard から Expert まで並んでるけど、どれも範囲内に収まってる。
呪術廻戦 シーズン2 OP - 青のすみか [Hard] NPS 3.268 がどんなもんかみてみましょ。
https://allpoland.github.io/ArcViewer/?id=33c66
いい曲
NPS 3.5 から 4.0 もみてみよ
$ python show_bsrs_nps.py --show-bsrs 10 --min-nps 3.5 --max-nps 4.0
min_nps: 3.5 max_nps: 4.0
30d16 [+749 0.9804] Livin' on a Prayer (2023 Remap) - Bon Jovi
[Expert NPS: 3.743] Duration: 04:08 BPM: 121.8
32693 [+1018 0.9798] The Phoenix - Fall Out Boy
[Expert NPS: 3.539] Duration: 04:06 BPM: 138.0
31a3b [+438 0.9769] PONPONPON - Kyary Pamyu Pamyu
[Expert NPS: 3.517] [ExpertPlus NPS: 3.543] Duration: 04:04 BPM: 128.0
2f266 [+510 0.9756] Canned Heat - Jamiroquai
[ExpertPlus NPS: 3.776] Duration: 03:45 BPM: 128.0
3080f [+928 0.9752] What's New, Scooby-Doo? (2023 Remap) - Simple Plan
[Normal NPS: 3.887] Duration: 01:07 BPM: 158.03
31006 [+483 0.9745] S3RL - Virtual Rave [v3]
[Expert NPS: 3.979] Duration: 03:26 BPM: 175.0
32dff [+557 0.9737] (G)I-DLE - Queencard
[Expert NPS: 3.918] Duration: 02:43 BPM: 130.0
3555b [+209 0.9735] SPECIALZ (TV Size) [Jujutsu Kaisen Season 2 Opening 2] - King Gnu
[Expert NPS: 3.722] Duration: 01:31 BPM: 117.0
205d1 [+571 0.9726] 『くうになる』 / feat. 初音ミク & 可不 - MIMI
[Hard NPS: 3.751] Duration: 02:34 BPM: 166.0
36310 [+474 0.9722] League of Legends - GODS (ft. NewJeans)
[Expert NPS: 3.596] Duration: 03:44 BPM: 146.0
Showed 10 / 1903 bsrs
S3RL - Virtual Rave [v3] NPS 3.979 です
https://allpoland.github.io/ArcViewer/?id=31006
密度が濃くなった
mp3ファイルをブッコぬく
余興はこのへんにして1万曲をブッコぬきます
コードはこんな
import glob
import json
import os
import requests
import shutil
import zipfile
from pydub import AudioSegment
TEMP_DIR = "./temp"
MUSIC_DIR = "./musics"
def get_bsr_music(bsr_id: str, name: str, zip_url: str):
# 3. 最終的なファイル名は {bsr_id} + 曲名の先頭20文字 + .mp3 とする
sanitized_name = f"{bsr_id}_{name.replace(' ', '_').replace('/', '_')[:20]}"
mp3_file_name = f"{sanitized_name}.mp3"
mp3_file_path = os.path.join(MUSIC_DIR, mp3_file_name)
# 4. 既にmp3ファイルが存在する場合はダウンロードと変換をスキップする
if os.path.exists(mp3_file_path):
print(f" {mp3_file_name} already exists in {MUSIC_DIR}. Skipping download and conversion.")
return
# 5. bsr用の一時作業ディレクトリを作成
bsr_dir = os.path.join(TEMP_DIR, bsr_id)
if not os.path.exists(bsr_dir):
os.makedirs(bsr_dir)
zip_file_path = os.path.join(bsr_dir, f"{bsr_id}.zip")
# 6. zipファイルをダウンロード
response = requests.get(zip_url)
if response.status_code == 200:
# 7. zipファイルを保存
with open(zip_file_path, 'wb') as file:
file.write(response.content)
print(f" Downloaded {bsr_id} to {zip_file_path}")
# 8. zipファイルを解凍
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
zip_ref.extractall(bsr_dir)
print(f" Extracted {bsr_id} in {bsr_dir}")
egg_files = glob.glob(os.path.join(bsr_dir, "*.egg"))
if egg_files:
# 9. eggファイルをoggファイルにリネーム
ogg_file_path = os.path.join(bsr_dir, f"{bsr_id}.ogg")
os.rename(egg_files[0], ogg_file_path)
print(f" Renamed {egg_files[0]} to {bsr_id}.ogg")
# 10. oggファイルをmp3ファイルに変換
AudioSegment.from_ogg(ogg_file_path).export(mp3_file_path, format="mp3")
print(f" Converted {bsr_id}.ogg to {mp3_file_name} and moved to {MUSIC_DIR}")
else:
print(f"! Failed to download {bsr_id} from {zip_url}")
# 11. 一時作業ディレクトリを削除
shutil.rmtree(bsr_dir)
print(f" Removed directory and contents: {bsr_dir}")
def main():
# 1. 一時作業ディレクトリを作成
if not os.path.exists(TEMP_DIR):
os.makedirs(TEMP_DIR)
# 2. 音楽ファイルを保存するディレクトリを作成
if not os.path.exists(MUSIC_DIR):
os.makedirs(MUSIC_DIR)
bsrs_json_files = ["bsrs_ranking.json", "bsrs_mapper_misterlihao.json"]
for bsrs_json_file in bsrs_json_files:
if not os.path.exists(bsrs_json_file):
print(f"! File not found: {bsrs_json_file}")
continue
with open(bsrs_json_file, "r", encoding="utf-8") as json_file:
bsrs = json.load(json_file)
for i, bsr in enumerate(bsrs):
print(f"{i} {bsr['id']} {bsr['versions'][0]['downloadURL']}")
get_bsr_music(bsr_id=bsr["id"], name=bsr["name"], zip_url=bsr["versions"][0]["downloadURL"])
# 12. musics ディレクトリにあるファイル名を musics.txt に書き出す
with open("musics.txt", "w", encoding="utf-8") as f:
for file_name in os.listdir(MUSIC_DIR):
f.write(f"{file_name}\n")
print("Written file names to musics.txt")
if __name__ == "__main__":
main()
一時フォルダ作ったり、ダウンロードしたり、解凍したり、ファイルフォーマット変えたり、ファイルを移動させたり、ゴミを消したりと多少複雑なことしてるのでコードが今までよりも長いです。
上で取得したランキングとマッパーさんの譜面データ1万曲からmp3ファイルを作成して music ディレクトリに保存します。
- 一時作業ディレクトリを作成
- 音楽ファイルを保存するディレクトリを作成
- 最終的なファイル名は {bsr_id} + 曲名の先頭20文字 + .mp3 とする
- 既にmp3ファイルが存在する場合はダウンロードと変換をスキップする
- bsr用の一時作業ディレクトリを作成
- zipファイルをダウンロード
- zipファイルを保存
- zipファイルを解凍
- eggファイルをoggファイルにリネーム
- oggファイルをmp3ファイルに変換
- 一時作業ディレクトリを削除
- musics ディレクトリにあるファイル名を musics.txt に書き出す
手順は長いけど、やってることはシンプル。
でも一つトリックがあって、BeatSaberで使われている音楽ファイルのフォーマット(?)というか拡張子はeggなんだけど、中身はoggなので拡張子名を変えるだけで対応した音楽プレイヤーで再生できるという紛らわしいというかハトビーム打ち込みたいくらい不思議な仕様になってます。しにさらせ(最初ほんと分からんくて何十分も悩んだ)
そしてこのプログラムをぐるぐる回していると
手元に1万曲の音楽が集まります。
おめでとうございます。これでとうぶん音楽には困りません。よかったですね?
29GBてw
10000曲もブッコ🕊ぬいてられるか!って人には bsr を指定するだけで即音楽が再生されるプログラムもあるのでどぞ
おしまい
おまけのようなメインのような翻訳AI
ビーセイの Twitch 配信でチャットの翻訳を入れているプレイヤーさんをちらほら見かけます。おもろい翻訳できないかなと作ってみました。会話のコンテキスト(会話の履歴)を理解しながら翻訳するっていうAIツールです。人間とAIボットとAI翻訳の不思議な会話をどうぞ