diff --git a/vipsong.py b/vipsong.py index 3c81cbd..67586e4 100644 --- a/vipsong.py +++ b/vipsong.py @@ -25,11 +25,21 @@ _FORMAT_MAP = { "mp4": "mp4", } +_EXT_TO_FMT = { + "mp3": "mp3", + "flac": "flac", + "wav": "wav", + "ogg": "ogg", + "oga": "ogg", + "m4a": "mp4", + "mp4": "mp4", +} + stats = {"success": 0, "failed": 0, "files": []} kgm_decoder = None def _u32le(data, pos): - return struct.unpack(b"output' 重定向语法,返回 (output_path, output_fmt)。""" + p = Path(input_path) + spec = output_spec + # 如果包含路径分隔符,作为完整路径 + if "/" in spec or "\\" in spec: + out_path = Path(spec) + elif "." in spec: + # 仅文件名,放在输入文件同目录 + out_path = p.parent / spec + else: + # 裸格式名,仅修改扩展名 + out_path = p.with_suffix("." + spec) + ext = out_path.suffix.lower().lstrip(".") + out_fmt = _EXT_TO_FMT.get(ext, ext) + return str(out_path), out_fmt + def ncm_stream_bytes(key): sbox = bytearray(range(256)) j = 0 @@ -74,7 +101,7 @@ def ncm_stream_bytes(key): def ncm_decrypt_audio(data, box): reps = (len(data) + 256) // 256 + 1 stream = (box * reps)[1:1 + len(data)] - return (int.from_bytes(data, b"little") ^ int.from_bytes(stream, b"little")).to_bytes(len(data), b"little") + return (int.from_bytes(data, "little") ^ int.from_bytes(stream, "little")).to_bytes(len(data), "little") def decrypt_ncm(file_path): data = Path(file_path).read_bytes() @@ -262,7 +289,7 @@ def _convert_audio(raw, container, out_fmt, output_path): audio = AudioSegment.from_file(io.BytesIO(raw), format=src_fmt) audio.export(output_path, format=dst_fmt) -def convert_file(file_path, output_fmt, delete_flag): +def convert_file(file_path, output_fmt="mp3", delete_flag=False, output_path=None): global stats, kgm_decoder file_path = str(Path(file_path).resolve()) if not Path(file_path).exists(): @@ -307,7 +334,8 @@ def convert_file(file_path, output_fmt, delete_flag): stats["files"].append((file_path, False)) return out_fmt = container if output_fmt == "original" else output_fmt - output_path = _resolve_output(file_path, out_fmt) + if output_path is None: + output_path = _resolve_output(file_path, out_fmt) try: _convert_audio(raw, container, out_fmt, output_path) except Exception as e: @@ -344,6 +372,25 @@ def expand_files(patterns): result.append(pat) return result +def _process_patterns(patterns, output_fmt, delete_flag): + """处理文件模式列表,支持 > 重定向语法。""" + for pat in patterns: + if ">" in pat: + input_spec, output_spec = pat.split(">", 1) + input_spec = input_spec.strip() + output_spec = output_spec.strip() + if not input_spec: + print(" [WARN] > 重定向语法缺少输入文件: " + pat) + continue + files = expand_files([input_spec]) + for f in files: + out_path, out_fmt = _parse_gt_output(f, output_spec) + convert_file(f, out_fmt, delete_flag, output_path=out_path) + else: + files = expand_files([pat]) + for f in files: + convert_file(f, output_fmt, delete_flag) + def print_stats(): print() print("=== Conversion Stats ===") @@ -356,6 +403,27 @@ def print_stats(): print(" [" + status + "] " + fp) print() +def _split_line(line): + """按空白分割命令行,处理引号但不处理反斜杠转义(适配 Windows 路径)。""" + args = [] + current = [] + in_single = False + in_double = False + for ch in line: + if ch == '"' and not in_single: + in_double = not in_double + elif ch == "'" and not in_double: + in_single = not in_single + elif ch in (' ', '\t') and not in_single and not in_double: + if current: + args.append(''.join(current)) + current = [] + else: + current.append(ch) + if current: + args.append(''.join(current)) + return args + def main(): global stats if len(sys.argv) > 1: @@ -386,9 +454,7 @@ def main(): print(" vipsong.py -f flac -d ./music/*.ncm") print(" vipsong.py ./song.kgm") sys.exit(1) - files = expand_files(patterns) - for f in files: - convert_file(f, output_fmt, delete_flag) + _process_patterns(patterns, output_fmt, delete_flag) print_stats() return print("=" * 50) @@ -400,6 +466,9 @@ def main(): print(" Convert file or glob pattern (e.g. *.ncm)") print(" -f Output format (default: mp3)") print(" -d Delete original after conversion") + print(" xx>yy.mp3 Output to specified filename (same dir)") + print(" xx>wav Convert to format, keep name (xx.wav)") + print(" xx>/path/yy.mp3 Output to absolute path") print(" /exit Exit") print(" /get Show stats") print() @@ -412,20 +481,21 @@ def main(): break if not line: continue - parts = line.split() + parts = _split_line(line) if not parts: continue - cmd = parts[0] - if cmd == "/exit": + # 检查命令 + if parts[0] == "/exit": print("Bye!") break - elif cmd == "/get": + if parts[0] == "/get": print_stats() continue + # 解析参数和文件模式(从 i=0 开始,parts[0] 可能是文件路径) patterns = [] output_fmt = "mp3" delete_flag = False - i = 1 + i = 0 while i < len(parts): if parts[i] == "-f" and i + 1 < len(parts): output_fmt = parts[i + 1].lower() @@ -433,22 +503,23 @@ def main(): elif parts[i] == "-d": delete_flag = True i += 1 - elif parts[i].startswith("-"): + elif parts[i].startswith("-") and len(parts[i]) > 1 and not Path(parts[i]).exists(): print(" [WARN] Unknown option: " + parts[i]) i += 1 else: patterns.append(parts[i]) i += 1 if not patterns: - print(" Usage: [-f format] [-d]") + print(" Usage: [-f format] [-d] 或 >output.xxx") print(" Examples: *.ncm") print(" ./xx/xx.kgm") print(" song.ncm -d") print(" *.ncm -f flac") + print(" xx.ncm>yy.mp3 (重命名输出)") + print(" xx.kgm>wav (转换格式)") + print(" xx.ncm>/xx/xx.mp3 (指定输出目录)") continue - files = expand_files(patterns) - for f in files: - convert_file(f, output_fmt, delete_flag) + _process_patterns(patterns, output_fmt, delete_flag) print() print(" Current: Success " + str(stats["success"]) + " | Failed " + str(stats["failed"])) print()