mirror of
https://gitea.pjck.top/Cookies/CookiesChartConverter.git
synced 2026-02-15 03:17:27 +08:00
Compare commits
13 Commits
222440ce28
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd69ee2b9c | |||
| 0e029febdd | |||
| e73568a1fe | |||
| e85f7e86ee | |||
| 5a082b63a4 | |||
| 8ef9d502f1 | |||
| 374037cfea | |||
| 3687cf2655 | |||
| debe1d2102 | |||
| 744ddc4663 | |||
| ac3c1e79a9 | |||
| 3c5b23f4a5 | |||
| 842884c96d |
@@ -4,6 +4,7 @@
|
|||||||
目前支持输入id给铺面
|
目前支持输入id给铺面
|
||||||
铺面信息待完善
|
铺面信息待完善
|
||||||
|
|
||||||
甲级惯犯:
|
## 甲级惯犯:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -20,6 +20,9 @@ def parse_music_xml(file_path):
|
|||||||
artist_elem = root.find("artistName/str")
|
artist_elem = root.find("artistName/str")
|
||||||
artist_name = artist_elem.text if artist_elem is not None else "?"
|
artist_name = artist_elem.text if artist_elem is not None else "?"
|
||||||
|
|
||||||
|
version_elem = root.find("AddVersion/str")
|
||||||
|
version_name = version_elem.text if version_elem is not None else "?"
|
||||||
|
|
||||||
# 提取启用谱面信息(设计者 + 等级)
|
# 提取启用谱面信息(设计者 + 等级)
|
||||||
note_infos = []
|
note_infos = []
|
||||||
notes_data = root.find("notesData")
|
notes_data = root.find("notesData")
|
||||||
@@ -45,12 +48,12 @@ def parse_music_xml(file_path):
|
|||||||
"levelshow": f"{level}{PLUS if int(level_decimal) > 6 else EMPTY}",
|
"levelshow": f"{level}{PLUS if int(level_decimal) > 6 else EMPTY}",
|
||||||
})
|
})
|
||||||
|
|
||||||
return id,music_name, artist_name, note_infos
|
return id,music_name, artist_name, note_infos , version_name
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
file_path = "/Users/bennett/Downloads/SDEZ/Package/Sinmai_Data/StreamingAssets/A000/music/music011663/Music.xml"
|
file_path = "/Users/bennett/Downloads/SDEZ/Package/Sinmai_Data/StreamingAssets/A000/music/music011663/Music.xml"
|
||||||
id,music_name, artist_name, note_infos = parse_music_xml(file_path)
|
id,music_name, artist_name, note_infos,version_name = parse_music_xml(file_path)
|
||||||
print("ID:",id)
|
print("ID:",id)
|
||||||
print("音乐名称:", music_name)
|
print("音乐名称:", music_name)
|
||||||
print("艺术家名称:", artist_name)
|
print("艺术家名称:", artist_name)
|
||||||
|
|||||||
26
ab2png.py
Normal file
26
ab2png.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import UnityPy
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def convert_ab_to_png(assetbundle_path, output_png_path):
|
||||||
|
env = UnityPy.load(assetbundle_path)
|
||||||
|
|
||||||
|
for obj in env.objects:
|
||||||
|
if obj.type == 28: # ClassID 28 = Texture2D
|
||||||
|
texture2d = obj.read()
|
||||||
|
image = texture2d.image
|
||||||
|
image.save(output_png_path)
|
||||||
|
print(f"已提取 Texture2D 并保存到 {output_png_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
raise Exception("AssetBundle 中没有找到 Texture2D。")
|
||||||
|
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("用法: python ab2png.py 输入.ab 输出.png")
|
||||||
|
else:
|
||||||
|
convert_ab_to_png(sys.argv[1], sys.argv[2])
|
||||||
54
acb2mp3.py
Normal file
54
acb2mp3.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import run
|
||||||
|
from loguru import logger
|
||||||
|
# 设置工作目录
|
||||||
|
WORK_DIR = Path("work/audio")
|
||||||
|
|
||||||
|
# 确保工作目录存在
|
||||||
|
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_awb_to_wav(awb, output_wav):
|
||||||
|
"""使用 vgmstream-cli 将 AWB 文件转换为 WAV 文件"""
|
||||||
|
run(["vgmstream-cli","-o",output_wav,awb])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def convert_wav_to_mp3(wav, output_mp3):
|
||||||
|
"""使用 ffmpeg 将 WAV 文件转换为 44100Hz 的 MP3 文件"""
|
||||||
|
run(["ffmpeg", "-y", "-i", wav, "-ar", "44100", output_mp3])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
logger.info("用法: python acb2mp3.py 输入文件 输出文件")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
input_file = sys.argv[1]
|
||||||
|
output_file = sys.argv[2]
|
||||||
|
|
||||||
|
# 获取输入文件的扩展名
|
||||||
|
input_path = Path(input_file)
|
||||||
|
output_path = Path(output_file)
|
||||||
|
|
||||||
|
# 判断输入文件是否是 .acb 文件
|
||||||
|
if input_path.suffix.lower() == ".acb":
|
||||||
|
# 假设输入文件关联的 .awb 文件在同一目录下
|
||||||
|
awb_file = input_path.with_suffix(".awb")
|
||||||
|
logger.info(awb_file)
|
||||||
|
if awb_file.exists():
|
||||||
|
# 将 .awb 转换为 WAV
|
||||||
|
wav_file = WORK_DIR / "output.wav"
|
||||||
|
convert_awb_to_wav(str(awb_file), str(wav_file))
|
||||||
|
logger.info(f"AWB 转换成功,生成文件: {wav_file}")
|
||||||
|
|
||||||
|
# 将 WAV 转换为 MP3
|
||||||
|
mp3_file = output_path.with_suffix(".mp3")
|
||||||
|
convert_wav_to_mp3(str(wav_file), str(mp3_file))
|
||||||
|
logger.info(f"WAV 转换为 MP3 成功,生成文件: {mp3_file}")
|
||||||
|
else:
|
||||||
|
logger.error(f"未找到对应的 AWB 文件: {awb_file}")
|
||||||
|
else:
|
||||||
|
logger.error(f"输入文件不是 .acb 文件: {input_path}")
|
||||||
93
convert.py
93
convert.py
@@ -7,39 +7,41 @@ from ab2png import convert_ab_to_png
|
|||||||
from acb2mp3 import convert_awb_to_wav, convert_wav_to_mp3
|
from acb2mp3 import convert_awb_to_wav, convert_wav_to_mp3
|
||||||
from search import search_music_by_id
|
from search import search_music_by_id
|
||||||
from pv_decode import dat_to_mp4
|
from pv_decode import dat_to_mp4
|
||||||
|
from loguru import logger
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
# 假设你已实现以下函数
|
# 假设你已实现以下函数
|
||||||
|
|
||||||
def build_maidata_txt(
|
def build_maidata_txt(
|
||||||
title: str = "",
|
title: str = "",
|
||||||
freemsg: str = "",
|
freemsg: str = "",
|
||||||
bpm: str = "",
|
bpm: str = "",
|
||||||
first_notes: dict = None, # {1: 0.123, 2: ..., ...}
|
first_notes: dict = None, # {2: 0.123, 3: ..., ...}
|
||||||
levels: dict = None, # {1: "3", 2: "5", ...}
|
levels: dict = None, # {2: "3", 3: "5", ...}
|
||||||
designers: dict = None, # {1: "作者A", 2: ..., ...}
|
designers: dict = None, # {2: "作者A", 3: ..., ...}
|
||||||
charts: dict = None, # {1: "谱面数据\n...", ...},
|
charts: dict = None, # {2: "谱面数据\n...", ...}
|
||||||
levelnum: int = None,
|
levelnum: int = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
maidata = [f"&title={title}", f"&freemsg={freemsg}", f"&wholebpm={bpm}", "&first=0"]
|
# 限制 levelnum 在 [2, 6]
|
||||||
|
levelnum = max(2, min(levelnum, 6))
|
||||||
|
|
||||||
# 1~6 难度
|
maidata = [
|
||||||
for i in range(1, levelnum+1):
|
f"&title={title}",
|
||||||
|
f"&artist={freemsg}",
|
||||||
|
f"&wholebpm={bpm}",
|
||||||
|
"&first=0"
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in range(2, levelnum + 2):
|
||||||
first = f"{first_notes.get(i):.3f}" if first_notes and i in first_notes else ""
|
first = f"{first_notes.get(i):.3f}" if first_notes and i in first_notes else ""
|
||||||
lv = levels.get(i, "") if levels else ""
|
|
||||||
des = designers.get(i, "") if designers else ""
|
|
||||||
chart = charts.get(i, "") if charts else ""
|
|
||||||
|
|
||||||
maidata.append(f"&first_{i}={first}")
|
maidata.append(f"&first_{i}={first}")
|
||||||
|
|
||||||
|
for i in range(2, levelnum + 2):
|
||||||
|
|
||||||
for i in range(2, levelnum+1):
|
|
||||||
lv = levels.get(i, "") if levels else ""
|
lv = levels.get(i, "") if levels else ""
|
||||||
des = designers.get(i, "") if designers else ""
|
des = designers.get(i, "") if designers else ""
|
||||||
maidata.append(f"&lv_{i}={lv}")
|
maidata.append(f"&lv_{i}={lv}")
|
||||||
maidata.append(f"&des_{i}={des}")
|
maidata.append(f"&des_{i}={des}")
|
||||||
|
|
||||||
for i in range(2, levelnum):
|
for i in range(2, levelnum + 2):
|
||||||
chart = charts.get(i, "") if charts else ""
|
chart = charts.get(i, "") if charts else ""
|
||||||
maidata.append(f"&inote_{i}=")
|
maidata.append(f"&inote_{i}=")
|
||||||
maidata.append(chart.strip())
|
maidata.append(chart.strip())
|
||||||
@@ -50,6 +52,7 @@ def build_maidata_txt(
|
|||||||
return "\n".join(maidata)
|
return "\n".join(maidata)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_simai_folder(result,output_folder):
|
def convert_to_simai_folder(result,output_folder):
|
||||||
npof = output_folder
|
npof = output_folder
|
||||||
output_folder = Path(output_folder)
|
output_folder = Path(output_folder)
|
||||||
@@ -61,17 +64,27 @@ def convert_to_simai_folder(result,output_folder):
|
|||||||
os.makedirs(of)
|
os.makedirs(of)
|
||||||
name = info[1]
|
name = info[1]
|
||||||
artist = info[2]
|
artist = info[2]
|
||||||
designers = {i + 1: item["designer"] for i, item in enumerate(info[3])}
|
designers = {i + 2: item["designer"] for i, item in enumerate(info[3])}
|
||||||
levels = {i + 1: item["levelshow"] for i, item in enumerate(info[3])}
|
levels = {i + 2: item["levelshow"] for i, item in enumerate(info[3])}
|
||||||
ma2_list = result[1]
|
ma2_list = result[1]
|
||||||
ab_file = result[2]
|
ab_file = result[2]
|
||||||
acb_list = result[3]
|
acb_list = result[3]
|
||||||
|
awb_file = next((f for f in acb_list if f.endswith('.awb')), None)
|
||||||
dat_file = result[4]
|
dat_file = result[4]
|
||||||
|
versionname = result[5]
|
||||||
|
if name is None or artist is None or designers is [] or levels is [] or ma2_list is [] or acb_list is [] or awb_file is None or dat_file is None or versionname is None:
|
||||||
|
return None
|
||||||
convert_results = {}
|
convert_results = {}
|
||||||
for mai in ma2_list:
|
for path in ma2_list:
|
||||||
convert_results.update({ma2_list.index(mai)+1: ma2tosimai(mai)})
|
filename = os.path.basename(path)
|
||||||
|
try:
|
||||||
|
num = int(filename[-6:-4]) # 提取 _00 → 0
|
||||||
|
level = num + 2 # 转换为 Simai 难度等级
|
||||||
|
convert_results[level] = ma2tosimai(path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理 {filename} 时出错: {e}")
|
||||||
|
|
||||||
convert_awb_to_wav(acb_list[1],f"work/{id}/temp.wav")
|
convert_awb_to_wav(awb_file,f"work/{id}/temp.wav")
|
||||||
convert_wav_to_mp3(f"work/{id}/temp.wav",f"work/{id}/track.mp3")
|
convert_wav_to_mp3(f"work/{id}/temp.wav",f"work/{id}/track.mp3")
|
||||||
|
|
||||||
convert_ab_to_png(ab_file,f"work/{id}/bg.png")
|
convert_ab_to_png(ab_file,f"work/{id}/bg.png")
|
||||||
@@ -83,7 +96,7 @@ def convert_to_simai_folder(result,output_folder):
|
|||||||
source_folder = of
|
source_folder = of
|
||||||
|
|
||||||
# 目标文件夹路径
|
# 目标文件夹路径
|
||||||
target_folder = npof
|
target_folder = f"{npof}/{versionname}/{name}"
|
||||||
|
|
||||||
# 要复制的文件列表
|
# 要复制的文件列表
|
||||||
files_to_copy = ["bg.png", "maidata.txt", "pv.mp4", "track.mp3"]
|
files_to_copy = ["bg.png", "maidata.txt", "pv.mp4", "track.mp3"]
|
||||||
@@ -98,9 +111,13 @@ def convert_to_simai_folder(result,output_folder):
|
|||||||
dst_file = os.path.join(target_folder, file_name)
|
dst_file = os.path.join(target_folder, file_name)
|
||||||
if os.path.exists(src_file):
|
if os.path.exists(src_file):
|
||||||
shutil.copy(src_file, dst_file)
|
shutil.copy(src_file, dst_file)
|
||||||
print(f"文件 {file_name} 复制成功")
|
logger.info(f"文件 {file_name} 复制成功")
|
||||||
else:
|
else:
|
||||||
print(f"文件 {file_name} 不存在,跳过复制")
|
logger.warning(f"文件 {file_name} 不存在,跳过复制")
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.exists(source_folder):
|
||||||
|
shutil.rmtree(source_folder)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -119,8 +136,28 @@ def convert_to_simai_folder(result,output_folder):
|
|||||||
# 示例调用
|
# 示例调用
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
res = search_music_by_id(input("ID:"))
|
music_ids = [834,799]
|
||||||
print(res)
|
output_folder = "result"
|
||||||
print("Converting...")
|
max_workers = 6 # 根据 CPU 和硬盘负载合理设置线程数
|
||||||
convert_to_simai_folder(res,"result")
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
futures = {}
|
||||||
|
for mid in music_ids:
|
||||||
|
res = search_music_by_id(str(mid))
|
||||||
|
if res is None:
|
||||||
|
continue
|
||||||
|
logger.info(f"提交任务: {mid}")
|
||||||
|
future = executor.submit(convert_to_simai_folder, res, output_folder)
|
||||||
|
futures[future] = mid
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
mid = futures[future]
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
logger.info(f"{mid} 处理完成")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{mid} 处理出错: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
getchart.py
Normal file
0
getchart.py
Normal file
@@ -1,6 +1,24 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from fractions import Fraction
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def ratio_to_str(width, height, max_denominator=20):
|
||||||
|
"""将宽高比转换为 a:b 的形式"""
|
||||||
|
# 确保分子 > 分母
|
||||||
|
if width >= height:
|
||||||
|
ratio = Fraction(width, height).limit_denominator(max_denominator)
|
||||||
|
else:
|
||||||
|
ratio = Fraction(height, width).limit_denominator(max_denominator)
|
||||||
|
|
||||||
|
a, b = ratio.numerator, ratio.denominator
|
||||||
|
# 由于上面取了 max(width, height),要对应宽:高
|
||||||
|
if width >= height:
|
||||||
|
return f"{a}:{b}"
|
||||||
|
else:
|
||||||
|
return f"{b}:{a}"
|
||||||
|
|
||||||
def get_video_resolution(video_path):
|
def get_video_resolution(video_path):
|
||||||
"""获取视频的宽高信息"""
|
"""获取视频的宽高信息"""
|
||||||
@@ -13,7 +31,15 @@ def get_video_resolution(video_path):
|
|||||||
return width, height
|
return width, height
|
||||||
|
|
||||||
def calculate_padding(width, height):
|
def calculate_padding(width, height):
|
||||||
"""计算填充黑边,使其变成 1:1(正方形)"""
|
"""计算填充黑边,使其变成 1:1(正方形)。
|
||||||
|
如果接近4:3或1:1比例(90%以内),则直接返回0。
|
||||||
|
"""
|
||||||
|
aspect_ratio = width / height if width >= height else height / width
|
||||||
|
|
||||||
|
# 如果接近 4:3 或 1:1,就不填充
|
||||||
|
if (0.9 <= aspect_ratio <= 1.1) or (1.3 <= aspect_ratio <= 1.5):
|
||||||
|
return max(width, height), 0, 0
|
||||||
|
|
||||||
max_side = max(width, height) # 以最大边长作为正方形边长
|
max_side = max(width, height) # 以最大边长作为正方形边长
|
||||||
pad_x = (max_side - width) // 2
|
pad_x = (max_side - width) // 2
|
||||||
pad_y = (max_side - height) // 2
|
pad_y = (max_side - height) // 2
|
||||||
@@ -25,7 +51,7 @@ def process_video(input_file, output_file):
|
|||||||
target_size, pad_x, pad_y = calculate_padding(width, height)
|
target_size, pad_x, pad_y = calculate_padding(width, height)
|
||||||
|
|
||||||
if pad_x == 0 and pad_y == 0:
|
if pad_x == 0 and pad_y == 0:
|
||||||
print("视频已经是 1:1,无需填充。")
|
logger.info(f"视频比例是 {ratio_to_str(width,height)},无需填充。")
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
"ffmpeg", "-i", input_file,
|
"ffmpeg", "-i", input_file,
|
||||||
"-c:v", "h264_videotoolbox",
|
"-c:v", "h264_videotoolbox",
|
||||||
@@ -34,7 +60,7 @@ def process_video(input_file, output_file):
|
|||||||
output_file
|
output_file
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
print(f"填充黑边,使视频变为 {target_size}x{target_size}")
|
logger.info(f"视频比例是 {ratio_to_str(width,height)},填充黑边,使视频变为 {target_size}x{target_size}")
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
"ffmpeg", "-i", input_file,
|
"ffmpeg", "-i", input_file,
|
||||||
"-vf", f"pad={target_size}:{target_size}:{pad_x}:{pad_y}:black",
|
"-vf", f"pad={target_size}:{target_size}:{pad_x}:{pad_y}:black",
|
||||||
@@ -48,7 +74,7 @@ def process_video(input_file, output_file):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print("用法: python pv_convert.py 输入文件 输出文件")
|
logger.info("用法: python pv_convert.py 输入文件 输出文件")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
input_file = sys.argv[1]
|
input_file = sys.argv[1]
|
||||||
|
|||||||
38
pv_decode.py
38
pv_decode.py
@@ -20,27 +20,53 @@ def convert_ivf_to_mp4(ivf_path: Path, output_mp4_path: Path):
|
|||||||
str(output_mp4_path)
|
str(output_mp4_path)
|
||||||
], check=True)
|
], check=True)
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def get_video_duration(path: Path) -> float:
|
||||||
|
"""使用 ffprobe 获取视频时长(单位:秒)"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||||
|
"-of", "default=noprint_wrappers=1:nokey=1", str(path)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
return float(result.stdout.strip())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取视频时长失败: {e}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def dat_to_mp4(dat_file: str, id: str):
|
def dat_to_mp4(dat_file: str, id: str):
|
||||||
"""将 .dat 文件当作 .usm 文件处理,提取并转换为 .mp4"""
|
"""将 .dat 文件当作 .usm 文件处理,提取并转换为 .mp4"""
|
||||||
dat_path = Path(dat_file).resolve()
|
dat_path = Path(dat_file).resolve()
|
||||||
base_name = dat_path.stem
|
base_name = dat_path.stem
|
||||||
work_dir = Path("/Users/bennett/PJCK/CookiesChartConverter") / "work" / id
|
work_dir = Path("/Users/bennett/PJCK/CookiesChartConverter") / "work" / id
|
||||||
|
|
||||||
usm_path = Path(dat_file)
|
usm_path = dat_path
|
||||||
ivf_path = work_dir / "output" / f"{base_name}.dat" / "videos" / f"{base_name}.ivf"
|
ivf_dir = work_dir / "output" / f"{base_name}.dat" / "videos"
|
||||||
mp4_path = work_dir / f"{base_name}.mp4"
|
mp4_path = work_dir / f"{base_name}.mp4"
|
||||||
|
|
||||||
# 直接将 .dat 当作 .usm 文件处理
|
# Step 1: 提取 USM 内容
|
||||||
print(f"[1/3] 提取 USM 内容 ...")
|
print(f"[1/3] 提取 USM 内容 ...")
|
||||||
extract_usm(usm_path, work_dir)
|
extract_usm(usm_path, work_dir)
|
||||||
|
|
||||||
if not ivf_path.exists():
|
# Step 2: 找到第一个 .ivf 文件
|
||||||
print(f"❌ 提取失败,未找到 {ivf_path.name}")
|
ivf_files = list(ivf_dir.glob("*.ivf"))
|
||||||
return
|
if not ivf_files:
|
||||||
|
print(f"❌ 提取失败,未找到 .ivf 文件")
|
||||||
|
return None
|
||||||
|
|
||||||
|
ivf_path = ivf_files[0]
|
||||||
print(f"[2/3] 转换为 MP4 ...")
|
print(f"[2/3] 转换为 MP4 ...")
|
||||||
convert_ivf_to_mp4(ivf_path, mp4_path)
|
convert_ivf_to_mp4(ivf_path, mp4_path)
|
||||||
|
|
||||||
|
# Step 3: 检查视频时长
|
||||||
|
duration = get_video_duration(mp4_path)
|
||||||
|
if duration < 1.0:
|
||||||
|
print(f"⚠️ 视频时长 {duration:.2f}s 太短,跳过生成 pv.mp4")
|
||||||
|
return None
|
||||||
|
|
||||||
print(f"[3/3] 成功生成:{mp4_path}")
|
print(f"[3/3] 成功生成:{mp4_path}")
|
||||||
process_video(mp4_path, work_dir / "pv.mp4")
|
process_video(mp4_path, work_dir / "pv.mp4")
|
||||||
return mp4_path
|
return mp4_path
|
||||||
|
|||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
acb-py==1.2.4
|
||||||
|
loguru==0.7.3
|
||||||
|
pydub==0.25.1
|
||||||
145
search.py
Normal file
145
search.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from ReadOpt import parse_music_xml, level_name
|
||||||
|
|
||||||
|
# 根目录
|
||||||
|
streaming_assets = "/Users/bennett/Downloads/SDEZ/Package/Sinmai_Data/StreamingAssets"
|
||||||
|
|
||||||
|
def search_music_by_id(search_id):
|
||||||
|
for asset_dir in os.listdir(streaming_assets):
|
||||||
|
root_dir = os.path.join(streaming_assets, asset_dir)
|
||||||
|
rd = os.path.join(streaming_assets, "A000")
|
||||||
|
|
||||||
|
music_dir = os.path.join(root_dir, "music")
|
||||||
|
if not os.path.isdir(music_dir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for music_subdir in os.listdir(music_dir):
|
||||||
|
sub_path = os.path.join(music_dir, music_subdir)
|
||||||
|
music_xml_path = os.path.join(sub_path, "Music.xml")
|
||||||
|
if not os.path.isfile(music_xml_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
music_id, name, artist, notes,version_name = parse_music_xml(music_xml_path)
|
||||||
|
if music_id == search_id:
|
||||||
|
logger.info(f"\n【找到曲目:{name}】")
|
||||||
|
logger.info(f" ID:{music_id}")
|
||||||
|
logger.info(f" 艺术家:{artist}")
|
||||||
|
logger.info(f" 所在分区:{asset_dir}")
|
||||||
|
logger.info(" 谱面信息:")
|
||||||
|
for i, note in enumerate(notes):
|
||||||
|
level_str = level_name[i] if i < len(level_name) else f"Diff{i}"
|
||||||
|
logger.info(f" - {level_str}: 定数 {note['level']} / 显示 {note['levelshow']} / 谱师 {note['designer']}")
|
||||||
|
|
||||||
|
# ma2 文件
|
||||||
|
ma2_files = [f for f in os.listdir(sub_path) if f.endswith(".ma2")]
|
||||||
|
ma2_paths = [os.path.join(sub_path, f) for f in ma2_files]
|
||||||
|
ma2_paths.sort()
|
||||||
|
if ma2_files:
|
||||||
|
logger.info(" MA2 文件:")
|
||||||
|
for f in ma2_files:
|
||||||
|
logger.info(f" - {os.path.join(sub_path, f)}")
|
||||||
|
else:
|
||||||
|
logger.warning(" MA2 文件:未找到")
|
||||||
|
|
||||||
|
# 曲绘(查 jacket 文件夹)
|
||||||
|
jacket_dir = os.path.join(rd, "AssetBundleImages", "jacket")
|
||||||
|
music_num = int(music_id)
|
||||||
|
if music_num >= 100000:
|
||||||
|
jacket_ab = f"ui_jacket_{(music_num-100000) - 10000:06d}.ab"
|
||||||
|
elif music_num >= 10000:
|
||||||
|
jacket_ab = f"ui_jacket_{music_num - 10000:06d}.ab"
|
||||||
|
else:
|
||||||
|
jacket_ab = f"ui_jacket_{music_num:06d}.ab"
|
||||||
|
alt_exts = [".png", ".jpg", ".jpeg"]
|
||||||
|
alt_jacket = next((f for f in os.listdir(jacket_dir)
|
||||||
|
if f.startswith(f"ui_jacket_{music_id}")
|
||||||
|
and os.path.splitext(f)[1].lower() in alt_exts), None)
|
||||||
|
|
||||||
|
logger.info(" 曲绘文件:")
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(jacket_dir, jacket_ab)):
|
||||||
|
logger.info(f" - {os.path.join(jacket_dir, jacket_ab)}")
|
||||||
|
elif alt_jacket:
|
||||||
|
logger.info(f" - {os.path.join(jacket_dir, alt_jacket)}")
|
||||||
|
else:
|
||||||
|
logger.warning(" - 未找到")
|
||||||
|
|
||||||
|
# 音频文件(SoundData)
|
||||||
|
sound_dir = os.path.join(rd, "SoundData")
|
||||||
|
music_num = int(music_id)
|
||||||
|
if music_num >= 100000:
|
||||||
|
audio_prefix = f"music{(music_num-100000) - 10000:06d}"
|
||||||
|
elif music_num >= 10000:
|
||||||
|
audio_prefix = f"music{music_num - 10000:06d}"
|
||||||
|
else:
|
||||||
|
audio_prefix = f"music{music_num:06d}"
|
||||||
|
|
||||||
|
audio_files = [f for f in os.listdir(sound_dir) if f.lower().startswith(audio_prefix)]
|
||||||
|
logger.info(" 音频文件:")
|
||||||
|
if audio_files:
|
||||||
|
for f in audio_files:
|
||||||
|
logger.info(f" - {os.path.join(sound_dir, f)}")
|
||||||
|
else:
|
||||||
|
logger.warning(" - 未找到")
|
||||||
|
|
||||||
|
audio_lists = []
|
||||||
|
for f in audio_files:
|
||||||
|
audio_lists.append(os.path.join(sound_dir, f))
|
||||||
|
|
||||||
|
# 视频 dat 文件(MovieData)
|
||||||
|
movie_dir = os.path.join(rd, "MovieData")
|
||||||
|
music_num = int(music_id)
|
||||||
|
if music_num >= 100000:
|
||||||
|
dat_name = f"{(music_num-100000) - 10000:06d}.dat"
|
||||||
|
elif music_num >= 10000:
|
||||||
|
dat_name = f"{music_num - 10000:06d}.dat"
|
||||||
|
else:
|
||||||
|
dat_name = f"{music_num:06d}.dat"
|
||||||
|
|
||||||
|
dat_path = os.path.join(movie_dir, dat_name)
|
||||||
|
logger.info(" 视频 DAT 文件:")
|
||||||
|
logger.info(f" - {dat_path}" if os.path.exists(dat_path) else " - 未找到")
|
||||||
|
|
||||||
|
return [[music_id, name, artist,notes],ma2_paths,os.path.join(jacket_dir, jacket_ab),audio_lists,dat_path,version_name]
|
||||||
|
|
||||||
|
logger.error(f"\n未找到 ID 为 {search_id} 的曲目信息。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_all_music():
|
||||||
|
music_list = []
|
||||||
|
for asset_dir in os.listdir(streaming_assets):
|
||||||
|
root_dir = os.path.join(streaming_assets, asset_dir)
|
||||||
|
music_dir = os.path.join(root_dir, "music")
|
||||||
|
if not os.path.isdir(music_dir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for music_subdir in os.listdir(music_dir):
|
||||||
|
sub_path = os.path.join(music_dir, music_subdir)
|
||||||
|
music_xml_path = os.path.join(sub_path, "Music.xml")
|
||||||
|
if not os.path.isfile(music_xml_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
music_id, name, artist, notes, version_name = parse_music_xml(music_xml_path)
|
||||||
|
music_list.append({
|
||||||
|
"id": music_id,
|
||||||
|
"name": name,
|
||||||
|
"artist": artist,
|
||||||
|
"version": version_name
|
||||||
|
})
|
||||||
|
|
||||||
|
return music_list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
target_id = input("请输入要搜索的曲目 ID:").strip()
|
||||||
|
result = search_music_by_id(target_id)
|
||||||
|
logger.info(result)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user