Add initial drs2dd script

This commit is contained in:
544146
2023-09-16 20:42:27 +01:00
parent 326dc2e9a9
commit e6911898af
7 changed files with 378 additions and 58 deletions
+11 -2
View File
@@ -40,6 +40,8 @@ pre-commit install
Usage:
Convert a DRS track video to a DD beat map
```bash
# grab video of DRS track from youtube
yt-dlp -f bv[ext=webm] https://youtu.be/o7I0scmptmo -o "drs_video.webm"
@@ -48,7 +50,14 @@ yt-dlp -f bv[ext=webm] https://youtu.be/o7I0scmptmo -o "drs_video.webm"
.venv/Scripts/python drsvideo2dd.py "drs_video.webm" --song-id "BOOMBAYAH-JP Ver.-"
```
Generate json files from xml files (needs xml files and a brave soul)
```bash
# you will need relevant xml files (not from DRS, that's illegal...)
.venv/Scripts/python drsxml2drsjson.py
HAS_XML=1 .venv/Scripts/python drsxml2drsjson.py
```
Generate full DD Beat Map from json files in repository (does not map notes atm, todo)
```bash
.venv/Scripts/python drs2dd.py --song-id 187
```
+127 -1
View File
@@ -1,2 +1,128 @@
# todo, parse xml or json drs data to dd json :)
from __future__ import annotations
import argparse
import json
import os
import shutil
from dataclasses import asdict
from drsxml2drsjson import get_songdata_from_track_id
from drsxml2drsjson import TRACK_ID_TO_PATH
from model.dancedash import DDBeatMap
from model.dancedash import DDBeatMapData
from model.dancedash import DDBeatMapInfoFile
from model.dancedash import X_Y
from model.dancerush import DRSSongData
from util import get_m4a_and_duration
from util import get_song_cover_path
from util import ORDER_COUNT_PER_BEAT
def create_dd_tracks_from_DRSSongData(
drs_song_data: DRSSongData, target_dir: str,
) -> bool:
if not os.path.exists(target_dir):
os.makedirs(target_dir)
print(f'Created directory: {target_dir}')
dd_bmp = int(drs_song_data.info.bpm_max / 100)
song_paths = []
for attr, difficulty in drs_song_data.difficulties.with_attrs_as_str.items():
target_difficulty_path = os.path.join(
target_dir, f'{drs_song_data.song_id}_{attr}.json',
)
song_paths.append(target_difficulty_path)
drs_beat_map = DDBeatMap(
data=DDBeatMapData(
name=f'{drs_song_data.info.title_name} {attr}',
intervalPerSecond=0.0,
gridSize=X_Y(x=0, y=0),
planeSize=X_Y(x=0, y=0),
orderCountPerBeat=ORDER_COUNT_PER_BEAT,
sphereNodes=[], # TODO: DO THE MAPPING LOL.
lineNodes=[], # TODO: DO THE MAPPING LOL.
effectNodes=[],
roadBlockNodes=[],
trapNodes=[],
),
beatSubs=1,
BPM=dd_bmp,
songStartOffset=0.0,
NPS='0.0',
developerMode=False,
noteSpeed=1.0,
noteJumpOffset=0.0,
interval=1.0,
info='',
)
with open(target_difficulty_path, 'w') as f:
json.dump(asdict(drs_beat_map), f, indent=4)
print(f'Created file: {target_difficulty_path}')
folder_path = TRACK_ID_TO_PATH.get(drs_song_data.song_id)
song_path, song_length = get_m4a_and_duration(folder_path)
if song_path and song_length:
shutil.copy(song_path, target_dir)
if song_cover_path := get_song_cover_path(folder_path):
shutil.copy(song_cover_path, target_dir)
drs_song_info_json = DDBeatMapInfoFile(
EditorVersion='1.3.2',
BeatMapId=drs_song_data.song_id,
OstId=0,
CreateTicks=0,
CreateTime='',
SongName=drs_song_data.info.title_name,
SongLength=song_length or '-1',
SongAuthorName=drs_song_data.info.artist_name,
LevelAuthorName='https://github.com/thomasasfk/drs2dd',
SongPreviewSection=0,
Bpm=str(dd_bmp),
SongPath=os.path.basename(song_path) if song_path else None,
OstName=None,
CoverPath=os.path.basename(
song_cover_path,
) if song_cover_path else None,
DRS_Easy=os.path.basename(song_paths[0]) if len(
song_paths,
) > 0 else None,
DRS_Normal=os.path.basename(song_paths[1]) if len(
song_paths,
) > 1 else None,
DRS_Hard=os.path.basename(song_paths[2]) if len(
song_paths,
) > 2 else None,
DRS_Expert=os.path.basename(song_paths[3]) if len(
song_paths,
) > 3 else None,
)
info_file_path = os.path.join(
target_dir, f'{drs_song_data.song_id}_info.json',
)
with open(info_file_path, 'w') as f:
json.dump(asdict(drs_song_info_json), f, indent=4)
print(f'Created file: {info_file_path}')
return True
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Create DD tracks from DRS Song Data',
)
parser.add_argument(
'--song-id', type=int,
help='ID of the song to process (number of folder)', required=True,
)
args = parser.parse_args()
song_data = get_songdata_from_track_id(args.song_id)
created = create_dd_tracks_from_DRSSongData(
song_data, song_data.info.title_name,
)
print(f'Song {args.song_id} created?: {created}')
+59 -48
View File
@@ -14,62 +14,73 @@ from model.dancerush import DRSTrack
TRACK_LIST = r'resources\drs\datax\music\music-info-base.xml'
TRACKS_BASE_DIR = r'resources\drs\datax\music'
ALL_SONG_METADATA = open(TRACK_LIST, encoding='utf-8').read()
ALL_SONG_METADATA_DICT = xmltodict.parse(ALL_SONG_METADATA)
_ALL_TRACK_PATHS = [x for x in os.listdir(TRACKS_BASE_DIR) if x.isdigit()]
ALL_TRACK_PATHS = [x for x in os.listdir(TRACKS_BASE_DIR) if x.isdigit()]
TRACK_ID_TO_PATH = {
int(x): os.path.join(TRACKS_BASE_DIR, x)
for x in _ALL_TRACK_PATHS
for x in ALL_TRACK_PATHS
}
TRACK_ID_TO_METADATA = {}
TRACK_ID_TO_METADATA_DICT = {}
for song in ALL_SONG_METADATA_DICT['mdb']['music']:
song_id = int(song['@id'])
path = TRACK_ID_TO_PATH.get(song_id)
files = os.listdir(path)
track_id_to_path_dict = {
re.search(r'_(\d[a-zA-Z])\.xml$', f).group(1): os.path.join(path, f)
for f in files
if re.search(r'_(\d[a-zA-Z])\.xml$', f)
}
if not track_id_to_path_dict:
continue
track_id_to_track = {
key: DRSTrack.from_xml(
value,
) for key, value in track_id_to_path_dict.items()
}
def get_songdata_from_track_id(track_id: int) -> DRSSongData:
if os.getenv('HAS_XML'):
return TRACK_ID_TO_SONGDATA.get(track_id)
track_path = TRACK_ID_TO_PATH.get(track_id)
songs_json_path = os.path.join(track_path, 'songs.json')
songs_dict = json.load(open(songs_json_path, encoding='utf-8'))
song_data = DRSSongData.from_json_dict(songs_dict)
return song_data
difficulties = DRSSongDifficulties(
**{
f'difficulty_{key}': DRSSongDifficulty(
difnum=int(
song['difficulty']
[f'fumen_{key}']['difnum']['#text'],
),
track=track_id_to_track[key],
if os.getenv('HAS_XML'):
ALL_SONG_METADATA = open(TRACK_LIST, encoding='utf-8').read()
ALL_SONG_METADATA_DICT = xmltodict.parse(ALL_SONG_METADATA)
TRACK_ID_TO_SONGDATA = {}
TRACK_ID_TO_SONGDATA_DICT = {}
for song in ALL_SONG_METADATA_DICT['mdb']['music']:
song_id = int(song['@id'])
path = TRACK_ID_TO_PATH.get(song_id)
files = os.listdir(path)
track_id_to_path_dict = {
re.search(r'_(\d[a-zA-Z])\.xml$', f).group(1): os.path.join(path, f)
for f in files
if re.search(r'_(\d[a-zA-Z])\.xml$', f)
}
if not track_id_to_path_dict:
continue
track_id_to_track = {
key: DRSTrack.from_xml(
value,
) for key, value in track_id_to_path_dict.items()
},
)
}
song_data = DRSSongData.from_dict(song, difficulties)
song_data_dict = asdict(song_data)
TRACK_ID_TO_METADATA[song_id] = song_data
TRACK_ID_TO_METADATA_DICT[song_id] = song_data_dict
difficulties = DRSSongDifficulties(
**{
f'difficulty_{key}': DRSSongDifficulty(
difnum=int(
song['difficulty']
[f'fumen_{key}']['difnum']['#text'],
),
track=track_id_to_track[key],
) for key, value in track_id_to_path_dict.items()
},
)
song_data = DRSSongData.from_xml_dict(song, difficulties)
song_data_dict = asdict(song_data)
TRACK_ID_TO_SONGDATA[song_id] = song_data
TRACK_ID_TO_SONGDATA_DICT[song_id] = song_data_dict
if __name__ == '__main__':
if __name__ == '__main__':
for song_id, song_data_dict in TRACK_ID_TO_SONGDATA_DICT.items():
SONG_JSON_PATH = os.path.join(
TRACK_ID_TO_PATH[song_id], 'songs.json',
)
with open(SONG_JSON_PATH, 'w', encoding='utf-8') as f:
json.dump(song_data_dict, f, ensure_ascii=False, indent=4)
for song_id, song_data_dict in TRACK_ID_TO_METADATA_DICT.items():
SONG_JSON_PATH = os.path.join(TRACK_ID_TO_PATH[song_id], 'songs.json')
with open(SONG_JSON_PATH, 'w', encoding='utf-8') as f:
json.dump(song_data_dict, f, ensure_ascii=False, indent=4)
for key, data in song_data_dict['difficulties'].items():
path = os.path.join(TRACK_ID_TO_PATH[song_id], f'{key}.json')
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
for key, data in song_data_dict['difficulties'].items():
path = os.path.join(TRACK_ID_TO_PATH[song_id], f'{key}.json')
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
+30
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
LEFT_NOTE = 8
RIGHT_NOTE = 9
@@ -126,3 +127,32 @@ class DDBeatMap:
interval=1.0,
info=info,
)
@dataclass
class DDBeatMapInfoFile:
EditorVersion: str
BeatMapId: int
OstId: int
CreateTicks: int
CreateTime: str
SongName: str
SongLength: str
SongAuthorName: str
LevelAuthorName: str
SongPreviewSection: int
Bpm: str
SongPath: str
OstName: str | None = None
CoverPath: str | None = None
DDVR_Easy: str | None = None
DDVR_Normal: str | None = None
DDVR_Hard: str | None = None
DRS_Easy: str | None = None
DRS_Normal: str | None = None
DRS_Hard: str | None = None
DRS_Expert: str | None = None
DRS_Master: str | None = None
DRS_ACE: str | None = None
DDVR_Env: str | None = None
DRS_Env: str | None = None
+110 -7
View File
@@ -63,6 +63,15 @@ class DRSSongDifficulties:
difficulty_2a: DRSSongDifficulty | None = None
difficulty_2b: DRSSongDifficulty | None = None
@property
def with_attrs_as_str(self) -> dict[str, DRSSongDifficulty]:
return {
'difficulty_1a': self.difficulty_1a,
'difficulty_1b': self.difficulty_1b,
'difficulty_2a': self.difficulty_2a,
'difficulty_2b': self.difficulty_2b,
}
@dataclass
class DRSSongData:
@@ -71,7 +80,7 @@ class DRSSongData:
info: DRSSongInfo | None = None
@classmethod
def from_dict(cls, data: dict, difficulties: DRSSongDifficulties) -> DRSSongData:
def from_xml_dict(cls, data: dict, difficulties: DRSSongDifficulties) -> DRSSongData:
return cls(
song_id=int(data['@id']),
difficulties=difficulties,
@@ -92,6 +101,53 @@ class DRSSongData:
),
)
@classmethod
def from_json_dict(cls, data: dict):
difficulties = data['difficulties']
info = data['info']
return cls(
song_id=int(data['song_id']),
difficulties=DRSSongDifficulties(
difficulty_1a=DRSSongDifficulty(
track=DRSTrack.from_json_dict(
difficulties['difficulty_1a']['track'],
),
difnum=difficulties['difficulty_1a']['difnum'],
) if difficulties['difficulty_1a'] else None,
difficulty_1b=DRSSongDifficulty(
track=DRSTrack.from_json_dict(
difficulties['difficulty_1b']['track'],
),
difnum=difficulties['difficulty_1b']['difnum'],
) if difficulties['difficulty_1b'] else None,
difficulty_2a=DRSSongDifficulty(
track=DRSTrack.from_json_dict(
difficulties['difficulty_2a']['track'],
),
difnum=difficulties['difficulty_2a']['difnum'],
) if difficulties['difficulty_2a'] else None,
difficulty_2b=DRSSongDifficulty(
track=DRSTrack.from_json_dict(
difficulties['difficulty_2b']['track'],
),
difnum=difficulties['difficulty_2b']['difnum'],
) if difficulties['difficulty_2b'] else None,
),
info=DRSSongInfo(
artist_name=info['artist_name'],
artist_yomigana=info['artist_yomigana'],
genre=info['genre'],
title_name=info['title_name'],
title_yomigana=info['title_yomigana'],
bpm_max=info['bpm_max'],
bpm_min=info['bpm_min'],
distribution_date=info['distribution_date'],
license=info['license'],
region=info['region'],
volume=info['volume'],
),
)
@dataclass
class DRSTrackBPMInfo:
@@ -119,7 +175,7 @@ class DRSTrackInfo:
measure_info: list[DRSTrackMeasureInfo] = field(default_factory=list)
@classmethod
def from_dict(cls, data: dict):
def from_xml_dict(cls, data: dict):
if type(data['measure_info']['measure']) is list:
drs_track_measure_info = [
DRSTrackMeasureInfo(
@@ -159,6 +215,26 @@ class DRSTrackInfo:
measure_info=drs_track_measure_info,
)
@classmethod
def from_json_dict(cls, data: dict):
return cls(
end_tick=int(data['end_tick']),
time_unit=DRSTrackTimeInfo(int(data['time_unit']['time_unit'])),
bpm_info=[
DRSTrackBPMInfo(
bpm=int(bpm['bpm']),
tick=int(bpm['tick']),
) for bpm in data['bpm_info']
],
measure_info=[
DRSTrackMeasureInfo(
denomi=int(measure['denomi']),
num=int(measure['num']),
tick=int(measure['tick']),
) for measure in data['measure_info']
],
)
@dataclass
class DRSTrackStepTickInfo:
@@ -186,7 +262,7 @@ class DRSTrackStep:
player_info: DRSTrackStepPlayerInfo
@classmethod
def from_dict(cls, data: dict):
def from_xml_dict(cls, data: dict):
return cls(
DRSTrackStepTickInfo(
int(data['start_tick']['#text']), int(
@@ -203,6 +279,22 @@ class DRSTrackStep:
DRSTrackStepPlayerInfo(int(data['player_id']['#text'])),
)
@classmethod
def from_json_dict(cls, data: dict):
return cls(
DRSTrackStepTickInfo(
int(data['tick_info']['start_tick']),
int(data['tick_info']['end_tick']),
),
int(data['kind']),
DRSTrackStepPositionInfo(
int(data['position_info']['left_pos']),
int(data['position_info']['right_pos']),
),
bool(data['long_point']),
DRSTrackStepPlayerInfo(int(data['player_info']['player_id'])),
)
@dataclass
class DRSTrack:
@@ -213,13 +305,13 @@ class DRSTrack:
rec_data = None # TODO: Implement this ???
@classmethod
def from_dict(cls, data: dict):
def from_xml_dict(cls, data: dict):
data = data['data']
return cls(
int(data['seq_version']['#text']),
DRSTrackInfo.from_dict(data['info']),
DRSTrackInfo.from_xml_dict(data['info']),
[
DRSTrackStep.from_dict(step)
DRSTrackStep.from_xml_dict(step)
for step in data['sequence_data']['step']
],
)
@@ -227,4 +319,15 @@ class DRSTrack:
@classmethod
def from_xml(cls, path: str):
data = open(path, encoding='utf-8').read()
return cls.from_dict(xmltodict.parse(data))
return cls.from_xml_dict(xmltodict.parse(data))
@classmethod
def from_json_dict(cls, data: dict):
return cls(
int(data['seq_version']),
DRSTrackInfo.from_json_dict(data['info']),
[
DRSTrackStep.from_json_dict(step)
for step in data['sequence_data']
],
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
from drsxml2drsjson import get_songdata_from_track_id
from model.dancerush import DRSSongData
def test_get_songdata_from_track_id():
song_data = get_songdata_from_track_id(187)
assert isinstance(song_data, DRSSongData)
+32
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
import contextlib
import json
import os
import subprocess
from functools import partial
import cv2
@@ -78,3 +80,33 @@ SIGN_TEMPLATE = Image.open(
SIGN_SEARCH_AREA = 850, 143, 223, 49
find_stage = partial(match_template, template=SIGN_TEMPLATE)
def get_m4a_and_duration(folder_path) -> tuple[str, str] | tuple[None, None]:
m4a_files = [f for f in os.listdir(folder_path) if f.endswith('.m4a')]
if not m4a_files:
return None, None
m4a_file = os.path.join(folder_path, m4a_files[0])
with contextlib.suppress(subprocess.CalledProcessError):
ffprobe_cmd = [
'ffprobe',
'-i', m4a_file,
'-show_entries', 'format=duration',
'-v', 'error',
'-of', 'default=noprint_wrappers=1:nokey=1',
]
duration_str = subprocess.check_output(ffprobe_cmd, text=True).strip()
return m4a_file, duration_str
return None, None
def get_song_cover_path(folder_path) -> str | None:
cover_files = [
f for f in os.listdir(folder_path) if f.startswith('jk_') and f.endswith('_b.png')
]
if not cover_files:
return None
return os.path.join(folder_path, cover_files[0])