Add initial drs2dd script
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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']
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user