Fix: > 重定向目录+格式语法解析错误

问题:输入 'dir/*.ncm>../.ogg' 时,所有文件输出到同一路径 '../.ogg',
覆盖互相,导致 17 个文件都写入同一个空白路径。

根因:_parse_gt_output 将包含路径分隔符的输出规格 '../.ogg' 直接当作
完整文件路径处理,未识别出 '..' 是目录、'.ogg' 是格式扩展名。

修复:当输出规格包含路径分隔符时,检查分隔符后的部分:
- 若为 '.ext' 格式(如 '.ogg' '._mp3')→ 视为 目录+格式,为每个输入文件
  生成独立输出路径 <dir>/<filename>.<ext>
- 若为裸格式名(如 'ogg') → 同样视为目录+格式
- 若为普通文件名(如 'yy.mp3') → 保持原有字面路径行为
- 若仅目录(如 '/tmp/') → 使用原文件名 + mp3 扩展名

提交哈希:
This commit is contained in:
ypshidifu
2026-09-17 15:07:51 +08:00
parent 110eef3d3c
commit e24966935e
+53 -5
View File
@@ -37,6 +37,18 @@ _EXT_TO_FMT = {
stats = {"success": 0, "failed": 0, "files": []} stats = {"success": 0, "failed": 0, "files": []}
kgm_decoder = None kgm_decoder = None
DEBUG = bool(os.environ.get("VIPSONG_DEBUG", ""))
def _normalize_format(fmt, default="mp3"):
"""规范化格式字符串,确保非空且可识别。"""
if not fmt or not isinstance(fmt, str) or not fmt.strip():
return _FORMAT_MAP.get(default.lower(), "mp3")
fmt = fmt.strip().lower()
if fmt in _EXT_TO_FMT:
return _EXT_TO_FMT[fmt]
if fmt in _FORMAT_MAP:
return _FORMAT_MAP[fmt]
return default
def _u32le(data, pos): def _u32le(data, pos):
return struct.unpack("<I", data[pos:pos+4])[0] return struct.unpack("<I", data[pos:pos+4])[0]
@@ -72,10 +84,31 @@ def _extension_for(fmt):
def _parse_gt_output(input_path, output_spec): def _parse_gt_output(input_path, output_spec):
"""解析 'input>output' 重定向语法,返回 (output_path, output_fmt)。""" """解析 'input>output' 重定向语法,返回 (output_path, output_fmt)。"""
p = Path(input_path) p = Path(input_path)
spec = output_spec spec = output_spec.strip()
# 如果包含路径分隔符,作为完整路径 if not spec:
# 空输出规格(如 'song.ncm>'),使用默认 mp3 格式
out_path = p.with_suffix(".mp3")
return str(out_path), "mp3"
# 如果包含路径分隔符,判断是目录+格式还是具体文件路径
if "/" in spec or "\\" in spec: if "/" in spec or "\\" in spec:
out_path = Path(spec) last_sep = max(spec.rfind("/"), spec.rfind("\\"))
remainder = spec[last_sep + 1:].strip()
if not remainder:
# 仅目录(如 '/path/'),使用原文件名 + mp3 扩展名
out_path = Path(spec) / (p.stem + ".mp3")
elif remainder.startswith(".") and remainder[1:].lower() in _EXT_TO_FMT:
# 目录 + 格式扩展(如 '../.ogg'),输出到该目录
ext = remainder[1:].lower()
dir_part = spec[:last_sep + 1]
out_path = Path(dir_part) / (p.stem + "." + ext)
elif remainder.lower() in _EXT_TO_FMT:
# 目录 + 裸格式名(如 '/path/ogg'),输出到该目录
ext = _extension_for(_EXT_TO_FMT[remainder.lower()])
dir_part = spec[:last_sep + 1]
out_path = Path(dir_part) / (p.stem + "." + ext)
else:
# 具体文件路径(如 '/path/yy.mp3')
out_path = Path(spec)
elif "." in spec: elif "." in spec:
# 仅文件名,放在输入文件同目录 # 仅文件名,放在输入文件同目录
out_path = p.parent / spec out_path = p.parent / spec
@@ -84,6 +117,8 @@ def _parse_gt_output(input_path, output_spec):
out_path = p.with_suffix("." + spec) out_path = p.with_suffix("." + spec)
ext = out_path.suffix.lower().lstrip(".") ext = out_path.suffix.lower().lstrip(".")
out_fmt = _EXT_TO_FMT.get(ext, ext) out_fmt = _EXT_TO_FMT.get(ext, ext)
if not out_fmt:
out_fmt = "mp3"
return str(out_path), out_fmt return str(out_path), out_fmt
def ncm_stream_bytes(key): def ncm_stream_bytes(key):
@@ -282,7 +317,14 @@ def _add_tags(output_path, tags, cover=None):
def _convert_audio(raw, container, out_fmt, output_path): def _convert_audio(raw, container, out_fmt, output_path):
"""将解密后的原始音频转码为目标格式。""" """将解密后的原始音频转码为目标格式。"""
src_fmt = _FORMAT_MAP.get(container, container) src_fmt = _FORMAT_MAP.get(container, container)
if not src_fmt:
src_fmt = "mp3"
dst_fmt = _FORMAT_MAP.get(out_fmt, out_fmt) dst_fmt = _FORMAT_MAP.get(out_fmt, out_fmt)
if not dst_fmt:
dst_fmt = "mp3"
if DEBUG:
print(" [DEBUG] container=" + str(container) + " out_fmt=" + str(out_fmt) +
" src_fmt=" + str(src_fmt) + " dst_fmt=" + str(dst_fmt))
if src_fmt == dst_fmt: if src_fmt == dst_fmt:
Path(output_path).write_bytes(raw) Path(output_path).write_bytes(raw)
else: else:
@@ -334,6 +376,8 @@ def convert_file(file_path, output_fmt="mp3", delete_flag=False, output_path=Non
stats["files"].append((file_path, False)) stats["files"].append((file_path, False))
return return
out_fmt = container if output_fmt == "original" else output_fmt out_fmt = container if output_fmt == "original" else output_fmt
if not out_fmt:
out_fmt = "mp3"
if output_path is None: if output_path is None:
output_path = _resolve_output(file_path, out_fmt) output_path = _resolve_output(file_path, out_fmt)
try: try:
@@ -382,9 +426,13 @@ def _process_patterns(patterns, output_fmt, delete_flag):
if not input_spec: if not input_spec:
print(" [WARN] > 重定向语法缺少输入文件: " + pat) print(" [WARN] > 重定向语法缺少输入文件: " + pat)
continue continue
if not output_spec:
print(" [WARN] > 重定向语法缺少输出: " + pat + " (使用默认格式: mp3)")
files = expand_files([input_spec]) files = expand_files([input_spec])
for f in files: for f in files:
out_path, out_fmt = _parse_gt_output(f, output_spec) out_path, out_fmt = _parse_gt_output(f, output_spec)
if not out_fmt:
out_fmt = output_fmt
convert_file(f, out_fmt, delete_flag, output_path=out_path) convert_file(f, out_fmt, delete_flag, output_path=out_path)
else: else:
files = expand_files([pat]) files = expand_files([pat])
@@ -435,7 +483,7 @@ def main():
while i < len(argv): while i < len(argv):
arg = argv[i] arg = argv[i]
if arg == "-f" and i + 1 < len(argv): if arg == "-f" and i + 1 < len(argv):
output_fmt = argv[i + 1].lower() output_fmt = _normalize_format(argv[i + 1], "mp3")
i += 2 i += 2
elif arg == "-d": elif arg == "-d":
delete_flag = True delete_flag = True
@@ -499,7 +547,7 @@ def main():
i = 0 i = 0
while i < len(parts): while i < len(parts):
if parts[i] == "-f" and i + 1 < len(parts): if parts[i] == "-f" and i + 1 < len(parts):
output_fmt = parts[i + 1].lower() output_fmt = _normalize_format(parts[i + 1], "mp3")
i += 2 i += 2
elif parts[i] == "-d": elif parts[i] == "-d":
delete_flag = True delete_flag = True