LoginSignup
0
0

beatsaver.com からmp3を10000曲ブッコ🕊ぬく

Last updated at Posted at 2023-12-03

beatsaver.com がプログラムからアクセスできる API を公開してくれているので、これは触りにいかないといけないぞということで全力でデータを落としてみました

この記事は Beat Saber Advent Calendar 2023 の4日目です。(センシティブ?なのかBANされちった。でも技術は悪くない。悪いのはいつも技術を利用する人間)

Beat_Saber_Advent_Calendar_2023_-_Adventar.png


最近 twitch 配信のツールなどを調べてたら、おっ!?っというページに辿り着いてしまったのが始まりでした

https://api.beatsaver.com/docs/index.html?url=./swagger.json
Swagger_UI.png

これ beatsaver.com のサイトを構築するためのデータを全部提供してね?

ドキュメント(自動生成の)がちゃんとまとまってる!


API を何回叩いてもとくに制限がかからないので叩きたい放題だ!

エンジニアにとって魅力的な、あるいみ魔力が詰まってるので、やってしまうのは不可抗力なのです

よし、音楽ファイルをごっそり抜こう!

もくじ

全体の流れとか

  1. 譜面情報をとってくる(zipファイルへのURLが書かれたjson)
  2. zipファイルをダウンロード
  3. zipファイル内のeggファイルをoggに名前を変えてmp3に変換
  4. たいりょうにダウンロード

まずは、たくさんの譜面の情報をゲットするところから始まります。その中にzipファイルへのURLが書かれているのでダウンロードしていきます。

zipファイルの中から音楽ファイルだけ取り出してmp3に変換します。

あとはこれをひたすら繰り返すのみです!

この記事に出てくるコードや出力結果は全てGithubにあげてあります。

でも抜き出したmp3はアップしてません。著作権たいせつ

f0b6d59f.jpg

とある期間の譜面をランキング順でとってくる

それでは譜面データをとっていきましょう。

今回は 2022年12月1日 から 2023年11月30 日までのランキング上位の譜面を10,000曲とってきます。

全体のコードはこんな。

fetch_bsrs_ranking.py
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段階に別れています。

  1. 譜面データを取得する
  2. id, タイトルなどをファイルに保存(bsrs_ranking_summary.txt)
  3. 全ての情報をファイルに保存(bsrs_ranking.json)

初めてのコードなので、1に出てくるAPI呼び出しについて説明します。
今回使用する API は譜面の検索です。 APIのURLは https://api.beatsaver.com/search/text/{page} になります。

beatsaver.com のドキュメントだとこの部分になります。

Swagger_UI.png

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.txtbsrs_ranking.json に保存します。

bsrs_ranking_summary.txt は全体が把握しやすいようにbsr番号いいね数スコアタイトルだけ入れてあります。bsrs_ranking.json には取得した全てのデータが入っています。

bsrs_ranking_summary.txt
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
...省略...
bsrs_ranking.json
[
    {
        "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 さんが作成した譜面をとってきます。

全体のコードはこんな。

fetch_bsrs_mapper.py
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段階になってます。

  1. マッパーさんの譜面IDを取得する
  2. 譜面データを取得する
  3. id, タイトルなどをファイルに保存(bsrs_mapper_misterlihao_summary.txt)
  4. 全ての情報をファイルに保存(bsrs_mapper_misterlihao.json)

まずは 1 で、マッパーさんの作成した譜面はマッパーさんのプレイリストに入ってます。それをとってきます。

API は https://api.beatsaver.com/users/id/{user_id}/playlist です。
でもドキュメントには書いてない。なんでやねん

user_id には misterlihao ではなく数値のIDを入れる必要があります。
ここでわかります。

BeatSaver_-Profile-_misterlihao.png

2 は譜面IDを複数渡すとその譜面データが返ってくる。でも一度に最大50個までしか指定できないのです。
misterlihao さんは247個の譜面(昨日まで246だったのにまた増えた)なので、50x5回呼んであげると全ての譜面データがとれます。

3 と 4 はランキングと同じなので省略。

bsrs_mapper_misterlihao_summary.txt
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

ArcViewer___Mori_Calliope_ft__Gawr_Gura__-_Q.png

開けるけど、ここはJASRACと契約してないだろうし、ヤバいかも?

せっかく譜面情報をとったので表示してみる

データがそろった。データが手元にあったら表示してみたいのはエンジニアの本能なのでしかたがありません。

表示するだけのコードです。

show_bsr_detail.py
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 もあってめっちゃ怒ってた

images.jpeg

そうだ。とってきたデータからプレイヤーさんにあった譜面を検索してみよう。まだ初心者さんらしく、見ている感じだと NPS は 3 〜 4あたりが良さそう。

コードはこんな

show_bsrs_nps.py
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万曲をブッコぬきます

コードはこんな

fetch_musics.py
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 ディレクトリに保存します。

  1. 一時作業ディレクトリを作成
  2. 音楽ファイルを保存するディレクトリを作成
  3. 最終的なファイル名は {bsr_id} + 曲名の先頭20文字 + .mp3 とする
  4. 既にmp3ファイルが存在する場合はダウンロードと変換をスキップする
  5. bsr用の一時作業ディレクトリを作成
  6. zipファイルをダウンロード
  7. zipファイルを保存
  8. zipファイルを解凍
  9. eggファイルをoggファイルにリネーム
  10. oggファイルをmp3ファイルに変換
  11. 一時作業ディレクトリを削除
  12. musics ディレクトリにあるファイル名を musics.txt に書き出す

手順は長いけど、やってることはシンプル。

でも一つトリックがあって、BeatSaberで使われている音楽ファイルのフォーマット(?)というか拡張子はeggなんだけど、中身はoggなので拡張子名を変えるだけで対応した音楽プレイヤーで再生できるという紛らわしいというかハトビーム打ち込みたいくらい不思議な仕様になってます。しにさらせ(最初ほんと分からんくて何十分も悩んだ)

Fr48CaPaMAQPVrH.jpg

そしてこのプログラムをぐるぐる回していると

beatsaver-com-bukkonuki_—ffmpeg_◂_Python_fetch_musics_py—_158×45.png

手元に1万曲の音楽が集まります。

おめでとうございます。これでとうぶん音楽には困りません。よかったですね?

musics.png

29GBてw

10000曲もブッコ🕊ぬいてられるか!って人には bsr を指定するだけで即音楽が再生されるプログラムもあるのでどぞ

おしまい

おまけのようなメインのような翻訳AI

ビーセイの Twitch 配信でチャットの翻訳を入れているプレイヤーさんをちらほら見かけます。おもろい翻訳できないかなと作ってみました。会話のコンテキスト(会話の履歴)を理解しながら翻訳するっていうAIツールです。人間とAIボットとAI翻訳の不思議な会話をどうぞ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0