DropboxのPublicフォルダー終了で影響を受けそうな難易度表の曲と差分メモ
要約
まずは猫缶サーバーの人のページを読もう
この記事は↑で扱われていない難易度表,差分関連でDLできなくなるファイルに関するメモ
DropboxのPublicフォルダーは2017/03/15に自動的に非公開になる。
簡単にはdl.dropbox.com/u/***みたいな感じのリンクが一切機能しなくなると考えてよい。
猫缶サーバーの中の人による解説を読んでもわかるようにこれはBMS界にとって大きな打撃で、かなりの数のBMSがDL出来なくなると考えられる。猫缶ミラーが公開されている理由もそれである。
さて、発狂難易度表をはじめとするいくつかの難易度表でもこのシステムを用いて公開されているファイルが存在する。とりあえずそれらを探し出して、簡単に修正できるものはしておこう、というのがこの記事の目的である。
- この記事の目的はBMSの保管ではない
- 各難易度表掲載曲の曲URLもしくは差分URLについて条件に当てはまるものを調査
- もしも直ちに代替利用可能なDLリンクが存在するならばそれを併記
GENOCIDE - 発狂難易度表
難易度表管轄ファイル
BOF2012 Append package
修正差分セット
幽雅に咲かせ、墨染の桜 [biginner] [NORMAL-] [NORMAL] [NORMAL+]
芥川龍之介の河童 ~Candid Friend [NORMAL]
Jade Star [alternative]
http://dl.dropbox.com/u/31329797/sabun/jadestar_altBGA%2B.zip
Moon phase Encounter (月面旅行)
* Crow Solace * [85%]
少なくともこれらに関しては公式に連絡するべきである。
それ以外
Super Mario Brothers Main Theme [29Another]
-
http://dl.dropboxusercontent.com/u/28765267/er_N.zip
ミラー: 猫缶サーバー
Qualia [sLunatic]
http://dl.dropboxusercontent.com/u/93651732/Web/index.htm
ミラー: 猫缶サーバー
https://dropbox.bms.ms/u/93651732/Web/index.htm
ただし、ページ自体は生きていても曲ファイルが既に削除されてしまっているため事実上ミラー無し。
Overjoy
Sunny☆girL [むっかぁぁぁぁ!!]
イベントページに記載されているため猫缶ミラーされていると思ったんだけど、どうやら
DownloadURL
しか見に行っていないらしい。Freja [SP Monather]
http://dl.dropboxusercontent.com/u/28181760/03_freja_another_m.bms
発狂第2難易度表
The Azure Box [SP ANOTHER]
http://dl.dropbox.com/u/380086/syzf-the_azure_box_ogg%2Bpng.zip
このリンクはもともと死んでいる。
ミラー: (非公式) http://www.geocities.jp/bmsetc/
=> https://onedrive.live.com/?id=FC95A680740CA8C9%21111&cid=FC95A680740CA8C9
Qualia [EXTRA], Fly Again (bms edit) [Another]
http://dl.dropboxusercontent.com/u/93651732/Web/index.htm
GENOCIDE->その他->Qualiaを参照。
ecstatic ghost party as Nightmare
http://dl.dropboxusercontent.com/u/45360077/watasino_h0-mupe-ji.html
ミラー: 猫缶サーバー
https://dropbox.bms.ms/u/45360077/watasino_h0-mupe-ji.html
リンク先のファイルも同様に利用可能。
芥川龍之介の河童 〜Candid Friend [BEGINNER]
http://dl.dropboxusercontent.com/u/31329797/kappa_ogg.zip
GENOCIDE->難易度表管轄ファイルを参照。
Online [Shutdown] (差分のみ)
http://dl.dropbox.com/u/94663500/%23Online_Shutdown.rar
ミラー: 高難易度BMS差分アップローダー 汎用up3152
orange tea [very hot] (差分のみ)
http://dl.dropboxusercontent.com/u/94663500/%23orange.rar
譜面作者のVF(5*LEAF)氏はPublicフォルダを使って多くの曲、差分を公開しているため、猫缶ミラーが存在しないもの(曲は結構ミラーが存在している)についてはこの辺とかから探す必要がある。
超絶技巧ネクロファンタジア - 両手 -
第3期LN難易度表
Revival of Kalpa -aletheia-
(非公式な保管) http://dl.dropbox.com/u/81466611/Revival%20of%20Kalpa%20LQ.zip
ミラー: 頑張れば作者によるファイルが見つかる
PMS難易度表
SHOOT MY BROKEN DISCO
http://dl.dropboxusercontent.com/u/45360077/watasino_h0-mupe-ji.html
発狂第2難易度表->ecstatic ghost party as Nightmareを参照
Tranberry Pie Factory
=> https://dl.dropbox.com/u/61735491/[KBAP]tranberry_pie_factory.rar
このファイルは作者によるもの
Mirage Garden (差分のみ)
=> https://dl.dropbox.com/u/61735491/xi_mirage_garden_9.zip
このファイルも作者によるもの
Mario Paint(Time Regression Mix for BMS)
=> https://dl.dropboxusercontent.com/u/49925194/bms/sabun/pms/mario_ox_pms_i.zip
Reinforced Soul
=> https://dl.dropboxusercontent.com/u/49925194/bms/sabun/pms/oxpms_reinforced.zip
おどれフルファイヤー
=> https://dl.dropboxusercontent.com/u/49925194/bms/sabun/pms/odore_pms.zip
これらの差分作者であるoxygen氏はサイト移行中らしく、今後個人サイトでDL可能になるのかもしれない。
5つの難易度表を調べたけれど、(猫缶ミラーを活用するという前提で)思っていたよりは影響を受けるファイル数は少なかったように思う。
とはいえGENOCIDEのrepair_packとかは使えないと大打撃ではあるため、早急な対策が必要ではある。
タイムリミット近いけど保管しておいた方がいいのかなあ。
7-zip+PowerShellでBMSアーカイブを可能な限り丁寧に一括解凍する
要約
Extract BMS archives with appropriate directory structure · GitHub をアーカイブがたくさんあるフォルダ内で実行
7-Zip は便利
PowerShellもまあ便利だけど、これで全部やるとなると大変だなあ。
BOFU2016ですね。
今年は例年以上に登録BMSに対する意見や文句を聞く気がしています(俺も言っている気がします)。
音量容量1本WAVなどいろいろありますが、導入時における最たる障害である「適切なフォルダ構造の判断」をなるべく少ないソフトウェアで解決しようと思い今回PowerShellスクリプトを書きました。
BOFU2016に関してはパッケージ化がもう済んでいることもありあまり需要がない気もしますが、まあもちろんこのイベント以外でも普通に使えると思います。Resilio Syncのやつからまだ展開が済んでいない方などはどうぞ。
なお、同様の方法として以下の方法があるけれども、フォルダ構造を完全に破壊してしまうのでお勧めしません。BOFU2016-num=585などはプレイ不能になると思う。
使い方は
0 前準備
多分実行時に実行ポリシーがなんたらという表示が出る人が多いと思うので以下の記事らへんを参考にしてください。 www.atmarkit.co.jp qiita.com
1 PowerShellを開き、アーカイブがあるディレクトリに移動
cd C:/path/to/bmsarcs
2 7-zipにPATHを通す
7z
とだけ打ってオプションとかがずらっと色々出てくればOK。でなければ手動で追加
$Env:Path += 'C:\Program Files\7-Zip;'
※これは一例です
3 ./bmsext.ps1
これで展開が始まる。デフォルトではアーカイブの拡張子を除いた部分をフォルダ名とするけど、./bmsext.ps1 -DirNameMajored
とすると「アーカイブ内に子ディレクトリが存在すれば、フォルダ名としてその子ディレクトリ名を優先」するようになります。
たまにファイルがスキップされます(***Skipped: ファイル名***
みたいな)。これはBMSが存在するディレクトリの上位ディレクトリにファイルが存在することにより適切な階層が特定できないためです。手動で展開してください。
zipファイルの拡張領域 0xE57A について
要約
ヘッダID
0xE57A
を持つzip拡張ヘッダはファイル名のコードポイントを指定するものである。このヘッダはALZipにより自動で付加されるものである。
zipファイルのヘッダには拡張領域と呼ばれる領域が存在し、ヘッダに標準で含まれる情報以外も記述することができる。
例えば、zip標準ヘッダに存在する貧弱なタイムスタンプ(ローカルにおける時間を2秒単位で丸める。すべてのタイムスタンプは偶数秒を持つ。)の代わりにより詳細なフォーマットで作成、更新、アクセス時刻を格納するための拡張ヘッダが存在する。
拡張領域は、拡張ヘッダ
[HeaderID=2bytes:short LE][DataSize=2bytes:short LE][DataBody=(Datasize)bytes]
の連続である必要がある。zipを扱うアーカイバはこれらを(ヘッダIDを知らなくても)読み取れる必要がある。上述したタイムスタンプは0x5455
がヘッダIDにあたる(リトルエンディアン指定のため実際のファイル中では5554
というバイト列になる)。
さて、主要な拡張ヘッダのヘッダIDは例えばこの辺とかにいろいろ載っているが、この前あるzipファイルの拡張領域を見ていたらこんな拡張ヘッダを見つけた。
7A E5 04 00 B5 03 00 00
ヘッダIDは0xE57A
とのことだが、少なくとも検索してみたところでそれっぽいものは1つもヒットしない。
とりあえずデータサイズ0x0004=4bytes
はデータ本体B5030000
とも合致するしミスとかではなさそうである。
データ本体の見た目からint(LE)っぽいので、とりあえず10進にすると0x000003B5=949
である。949という数字でピンと来る人もまあいるかもしれないけど、これはおそらくcp949の意なんだろう。cp949はeuc-krの拡張である。
軽い確認がてら高難易度BMS差分アップローダーのzipファイルについて調べてみる。ちょっと手を抜いている。
cpd={} for zp in glob.glob('./*.zip'): try:z=zipfile.ZipFile(zp) except:continue cp=[ struct.unpack('<I',zi.extra[zi.extra.find('z\xe5\x04\x00')+4:zi.extra.find('z\xe5\x04\x00')+8])[0] for zi in z.infolist() if zi.extra.find('z\xe5\x04\x00')!=-1 ] cp=list(set(cp)) for n in cp: cpd[n]=cpd.get(n,0)+1 print cpd # ---------- {932: 519, 949: 179}
932->cp932が大量にあるし、まずコードポイントとみて間違いないだろう。
ところで、len(glob.glob('./*.zip'))==6308
であったので、このヘッダを付加しているアーカイバは愚直に考えれば10%程度のシェアと考えるのが普通だろう。
どのアーカイバがこのヘッダを付加しているのかいくつか試してみた結果、ALZipでのみデフォルトで付加された(ほかに確認したのはWinRAR,7-Zip,explorer.exe)。
ALZipでは圧縮の際に言語(エンコード)を指定できるため、ここで選択した値が書き込まれるのだろう。
ちなみに言語にUNICODEを指定した場合にはこのヘッダは付加されず、代わりに汎用ビットフラグのUnicodeフラグ(%0000100000000000=2048
)が設定されるようだ。
個人的にはとても便利だと思うけどさすがにこれから普及することはないだろうなあ。日本語版ALZipも公開終了してしまったみたいだし。
DEE2イベントページのJSON出力は非常にクセが強い
技術者向けにJSONでリスト出力したものもご用意しております。何かに使えそうな方向けです。 http://t.co/5sE0USAikE
— BOFU実行委員会 (@bms_of_fighters) September 22, 2015
【技術者向け】JSON出力ルーチンにてダブルクォーテーションとスラッシュにエスケープコードを導入しました。また、このリストのみUTF-8での出力が可能となりました。 http://t.co/Pbg289luVz
— BOFU実行委員会 (@bms_of_fighters) September 23, 2015
【技術者向け】また、「output」に「headline」を指定すると最大1000件までのヘッドラインのリストをJSON形式で出力できるように対応しました。 http://t.co/lhc8B2s0c5
— BOFU実行委員会 (@bms_of_fighters) September 23, 2015
BMSイベント会場で最も大規模であるといってよいであろうDigital Emergency Exit 2(以下DEE2)のイベントシステムには、上のツイート群にあるように楽曲登録情報とイベントヘッドラインを取得できるJSON APIのようなものが存在している。大まかには以下の通り。
dictionary songlist{ DOMString EventID; DOMString EventNAME; Song[] Data; } dictionary headline{ DOMString EventID; DOMString EventNAME; Line[] Data; } dictionary Song{ DOMString RegID; DOMString Team; ... } dictionary Song{ DOMString Date; DOMString Num; ... }
まあ実は今回この構造は比較的どうでもよくて、問題はこれを読み込むのが一筋縄ではいかなかったということ。
いつものようにPythonで読みたいのでいろいろ試してみるが、ツイートにあるBOFU2015のsonglistがまず読み込めない。
j=json.loads( requests.get('http://manbow.nothing.sh/event/event.cgi?action=JSONList&event=104&utf-8=on').content ) ValueError: No JSON object could be decoded
これは辞書構造のエラーである。具体的には最後の部分がこうなっている。
{ ..., "Data":[ {...}, { "RegID" : "456" , ..., "UpdateTime" : "2015\/11\/04 00:44" }, ←これ ] }
リスト末尾に余分なカンマがついているとjsonモジュールは読んでくれないらしい。1個くらい我慢してほしい。 原因ははっきりしてないけど、「エントリーIDが最大の作品が削除されている」という状況下で発生すると推測できる。BOFU2015ではnum=458まで発行されたようだが、現存する最大IDは456である。ちなみに全曲数は447。
content=requests.get('http://manbow.nothing.sh/event/event.cgi?action=JSONList&event=104&utf-8=on').content j=json.loads(re.sub(r'}\s*,\s*]\s*}\s*\Z',r'}]}',content))
これで無事読み込めたわけだけどこれで終わるならこんな記事を書くわけがない。
§†гё∂㎡の情報を見る。
{ "RegID": "86", "Team": "S.E.Q.uencer", "Artist": "EFB37-1", ..., "Title": "§†гё∂�u", ←これ ... }
化けている。
これの原因は楽曲情報の生データ(CP932で持っているのだろう)をCP932ではなくShift_JISでデコード、UTF-8にエンコードしたことによる。
ということで、正確なデータを得るためにはutf-8=on
をクエリから外すべきである。
ここから泥沼の戦いが始まる。
content=requests.get('http://manbow.nothing.sh/event/event.cgi?action=JSONList&event=104').content j=json.loads(re.sub(r'}\s*,\s*]\s*}\s*\Z',r'}]}',content).decode('cp932')) ValueError: Invalid \escape: line 892 column 30 (char 36887)
892行目を見る。
890 ..., 891 "RegID" : "69" , 892 "Team" : "第10回自称無名BMS作家が物申\す!反省会場はこちらです" , 893 "Artist" : "Sena" , 894 ...,
ダメ文字対策の'\'(0x5C)
追記がしてある。伊達に2000年代序盤からイベントサイトやっていないんだなあと思う。いろいろな意味で。
content
を以下の感じに置換すればよい。
content=re.sub( '([\x81-\x9f\xe0-\xfc]'+r'\\)\\', r'\1', content ) j=json.loads(content.decode('cp932')) len(j['Data']) # == 447
読み込めた。§†гё∂㎡も、
for d in j['Data']: if ('RegID','86') in d.items(): print json.dumps(d,ensure_ascii=False,indent=2) { "RegID": "86", "Title": "§†гё∂㎡", ... }
という感じに読めている。OK。
さて、ここまでやれば問題ないだろうと本当に思っていたんだけれど、headlineのほうが実は厄介。
例えば、BOFU2015のheadlineに以下のようなData
の要素がある。
{ "Num" : "107" , ..., "Title" : "ミルキーベリー・スウィートタイ�" , ... }
化けている、とは実は少し違う。どうやら2バイト文字を1バイト目で切ってしまっているらしい。
さらに性質が悪いことに、これをそのままdecode('cp932')
すると、行末手前の'"'
を巻き込んで\ufffd
になり、
JSONとしての構造が破壊される。とりあえずの解決法として、'"'
の手前に' '
を挿入し、
decode('cp932',errors='replace')
してからそれを削除するという苦肉の策をとった。
見た目がきれいになるようにきちんと改行が入っていることに救われたと言う他ない。
content=requests.get('http://manbow.nothing.sh/event/event.cgi?action=JSONList&event=104&output=headline').content content=re.sub( r'}\s*,\s*]\s*}\s*\Z',r'}]}', content ) content=re.sub( '([\x81-\x9f\xe0-\xfc]'+r'\\)\\', r'\1', content ) content=re.sub( r'^(\s*\".*?\"\s*:\s*\".*)\"(\s*[,\]}]?\s*)$', r'\1 "\2', content,flags=re.MULTILINE ) content=content.decode('cp932',errors='replace') content=re.sub( ur'^(\s*\".*?\"\s*:\s*\".*)[ \ufffd]\"(\s*[,\]}]?\s*)$', ur'\1"\2', content,flags=re.MULTILINE ) j=json.loads(content) len(j['Data']) # == 1000
これでheadlineも読めるようになった。控えめに言っても面倒すぎる。
あとこれはついでだけど、曲名などの中に文字参照が入ったままになっているため、json.loads
前に先日作成したものを使って、
content=unescape_charref(content)
としてやれば、
{ "RegID": "231", "Artist": "霧慧トァン", "Title": "<music:name=Goodbye My Soul/>", ... } => { "RegID": "231", "Artist": "霧慧トァン", "Title": "<music:name=Goodbye My Soul/>", ... }
のように戻せる。
これでほとんどすべてのJSONをきちんと読めるようになった。けど、かなり重大な奴が隠れていた。
event=26のB-1 "Unrestricted"を見ると(字面からすでに想像できるかもしれないが)
{ "EventID" : "26" , "EventName" : "B-1 "Unrestricted"" , ←これ ... }
まさかのダブルクォーテーションのエスケープ漏れ。他にもあるかもしれないので上と似たようにして対処する。
content=re.sub( ur'(^\s*\".*?\"\s*:\s*\")(.*\".*)(?=\"\s*[,\]}]?\s*$)', lambda s:s.group(1)+re.sub( ur'((^|[^\\])(?:\\\\)*)(?=\")', ur'\1'+ur'\\', s.group(2) ), content,flags=re.MULTILINE )
これはjson.loads
の直前に行えばよい。
また、event=105 BOFOON ULTIMATE 2015のheadlineに以下の内容を持つインプレがある。
{ ..., "Info" : "このやっつけ感が正に (^^)" ... }
'正に'
と'(^^)'
の間の空白はエスケープされていないタブ文字\t
である。
タブ文字をタブ文字として文字列に格納するには、json.JSONDecoder
を調整する必要がある。
今回は最も簡単に以下のようにした。
class DEE2JSONDecoder(json.JSONDecoder): def __init__(self,encoding=None,strict=False**kwgs): json.JSONDecoder.__init__(self,encoding,strict=False,**kwgs)
明示的にこのデコーダを使用するには、
j=json.loads(content,cls=DEE2JSONDecoder)
のようにすればよい。
以上を行った結果、http://manbow.nothing.sh/event/event.cgiからたどれるすべてのイベントについて無事に読み込むことができるようになった。もうやりたくない。
以上の操作を楽に行えるものを例によってgistに上げておくので、この辺に興味がある人は使ってほしい。
正直なところ、中の人がsjis->cp932に書き換えてくれるだけでだいぶ楽になると思う。
Python2系で文字参照のエスケープとか
ほぼ同様の関数はHTMLParser.HTMLParser.unescape
やlxml.html.fromstring
あたりにもあるけれど、
前者は
U+10000-U+10FFFF
が無理HTMLParser.HTMLParser()
を一度生成しないと使えない←結構気に食わない
だし、後者はunescape
のためにわざわざlxml
をimport
するのも気が引けるので。
2系に限っているのは3系には既にあるため。
基本戦略はHTMLParser.unescape
と同じく(←書いてから知った)re.sub
を使う方法で。
re.sub
は第3引数(置換後文字列)に関数オブジェクトを置けるというのがポイントで、そこに文字参照→対応するユニコード文字(列)なる関数を置いてやればよい。
def unescape_charref(escaped): return re.sub('CHARREF REGEXP',lambda s:REPLACE(s),escaped)
名前または数字の解決
正規表現で簡単に書ける。
r'&((?P<char>[a-z]+)|#(?P<decimal>\d+)|#x(?P<hex>[\da-f]+));'
マッチした場合入れ子になった3グループのうち必ず2グループは空文字列となり、1グループは空文字列にならない。
よって、この正規表現にマッチしたre.MatchObject
をmatchobj
として、
from htmlentitydefs import name2codepoint def getunichr(matchobj): if matchobj.group('char'): # Character entity references num=name2codepoint.get(matchobj.group('char'),0) elif matchobj.group('decimal'): # Numeric character reference (decimal) num=int(matchobj.group('decimal')) elif matchobj.group('hex'): # Numeric character reference (hexadecimal) num=int(matchobj.group('hex'),16) return num
という感じにすればよい。これで名前→数値はOK。
UCS-2とUCS-4
Python処理系はUCS-2あるいはUCS-4がビルド時にオプションとして指定されていて、その違いによりunichr
等の挙動が異なる。
ちなみにPython.orgのWin-x64のバイナリは(俺のがそうなので多分)UCS-2が指定されている。この場合unichrは0<num<0x10000
つまりU+0000-U+FFFF
の部分しか対応していない。
UCS-2とUCS-4の区別はsys.maxunicode
でできる。
リンク先のコピペに近いけど、
from sys import maxunicode def uchr(c): if 0<c<=maxunicode: # BMP(UCS-2) / whole(UCS-4) return unichr(c) elif maxunicode<c<=0x10ffff: # SMP(UCS-2) / None(UCS-4) c-=0x10000 return unichr(c>>10|0xD800)+unichr(c&0x3FF|0xDC00) else: # c==0 or c is out of Unicode return ''
という感じ。Unicodeから外れてたらとりあえず空文字列を返す。
SMPに対応する意味あんの?とか実は書きながら思ってたんだけど、最近で言うと絵文字とかもここに属するらしいので、案外重要なのかもしれない。
まとめて
こういう感じになりました。
本当に3系と同等にするのならばhtmlentitydefs.name2codepoint
でなく3系のhtml.entities.html5
あたりも対応すべきかもしれないけど、面倒なので省略。
ゴーストへのエンコーダーも作った
エンコーダーも作ってしまったのでLR2-ghost-decoderからLR2-ghostlibに微妙に名前を変えました。まあ今後触ることはないと思うけど・・・。
手元のスコアで全一致確認済み。
Oが定義されてないのは何でだろうなーもしかしたら極端なケースとかあるのかな。
サムネ用のサンプル実行結果↓
LR2のゴースト文字列のデコード
単体では特に使い道がないと思うけど、ちょっと思うところあって頑張って調べて作った。
手持ちのスコアで調べた限りLR2側のエラーっぽいの除いて全部合ってそうだったので多分大丈夫だと思う(適当)。
とっても頑張ってコード読めばゴースト文字列内の各文字が何を意味しているかわかると思う(書くのが面倒なだけ)。情報必要なら言ってください。