This commit is contained in:
jimmy committed 2026-01-05 21:13:36 +08:00
1 parent b4fc3a1c23
commit 0c55ebd747
2 files changed
+55 -8

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# FFmpeg (vendored Windows build)
ffmpeg-*/
# Conversion outputs
out/
# Local/dev artifacts
.venv/
__pycache__/
*.py[cod]
.idea/
+44 -8
View File
@@ -4,19 +4,22 @@ Outputs are written flat into E:\\Music\\工具\\TypeChange\\out; if two source
or existing outputs share the same name, the last one wins and a summary of
overwrites is printed at the end.
If `mutagen` is installed, FLAC tags (Vorbis comments) and cover art are written
to the output M4A using standard MP4/iTunes atoms (including `covr`).
Notes:
- If the source is already `.m4a`, it is copied as-is by default (no re-encode).
- If `mutagen` is installed, FLAC tags (Vorbis comments) and cover art are written
to the output M4A using standard MP4/iTunes atoms (including `covr`).
"""
import os
import argparse
import subprocess
import shutil
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
# Source and destination roots
SOURCE_ROOT = Path(r"E:\Music\完成\单曲")
SOURCE_ROOT = Path(r"E:\Music")
SKIP_ROOT = SOURCE_ROOT / "工具"
OUT_ROOT = Path(r"E:\Music\工具\TypeChange\out")
@@ -294,7 +297,7 @@ def sync_tags_from_flac(src: Path, dst: Path) -> bool:
return True
def convert_file(src: Path, dst: Path, *, tags_only: bool) -> bool:
def convert_file(src: Path, dst: Path, *, tags_only: bool, copy_m4a: bool) -> bool:
"""Run ffmpeg to transcode a single file to AAC. Returns True on success."""
dst.parent.mkdir(parents=True, exist_ok=True)
@@ -313,15 +316,35 @@ def convert_file(src: Path, dst: Path, *, tags_only: bool) -> bool:
sys.stderr.write(f"[WARN] Tag sync failed for {dst}: {exc}\n")
return False
if copy_m4a and src.suffix.lower() == ".m4a":
try:
shutil.copy2(src, dst)
return True
except OSError as exc:
sys.stderr.write(f"[FAIL] copy {src} -> {dst} ({exc})\n")
return False
cmd = [FFMPEG_BIN, "-y"] # overwrite if needed
cmd += ["-i", str(src)]
external_cover = find_external_cover_image(src)
if external_cover:
cmd += ["-i", str(external_cover)]
cmd += ["-map", "0:a:0"] # first audio stream
if external_cover:
cmd += ["-map", "1:v:0"] # external cover image
cmd += ["-disposition:v:0", "attached_pic"]
else:
cmd += ["-map", "0:v:0?"] # embedded cover art if present
cmd += ["-map", "0:s?"] # keep subtitle streams (some files store lyrics as subtitles)
cmd += [
"-map",
"0:a", # audio only; cover art + full tags are applied in a separate step
"-map_metadata",
"0", # copy tags
"-map_chapters",
"0",
"-c:v",
"copy",
"-c:s",
"mov_text",
"-c:a",
"aac", # re-encode audio
"-b:a",
@@ -351,6 +374,11 @@ def parse_args():
action="store_true",
help="Skip transcoding; only sync FLAC tags/cover to existing outputs in OUT_ROOT.",
)
parser.add_argument(
"--no-copy-m4a",
action="store_true",
help="Do not copy .m4a sources; re-encode them to AAC like other formats (default: copy).",
)
return parser.parse_args()
@@ -398,12 +426,14 @@ def main():
for p in preexisting_outputs:
print(f" {p}")
max_workers = max(1, os.cpu_count() or 4)
max_workers = max(1, os.cpu_count() / 2 or 4)
successes = 0
print(f"Using ffmpeg at: {FFMPEG_BIN}")
if not HAS_MUTAGEN:
print("Note: Install 'mutagen' to fully preserve FLAC tags/cover art (pip install mutagen).")
if not args.no_copy_m4a and not args.tags_only:
print("Note: .m4a sources are copied as-is (use --no-copy-m4a to force re-encode).")
if args.tags_only:
print("Mode: tags-only (no transcoding).")
if args.tags_only:
@@ -415,7 +445,13 @@ def main():
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {}
for src, dst in tasks:
future = executor.submit(convert_file, src, dst, tags_only=args.tags_only)
future = executor.submit(
convert_file,
src,
dst,
tags_only=args.tags_only,
copy_m4a=not args.no_copy_m4a,
)
future_map[future] = (src, dst)
for future in as_completed(future_map):