Skip to content

Commit 651c22e

Browse files
committed
feat: update split script
1 parent 8b1a10c commit 651c22e

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

scripts/split.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import subprocess
5+
from pathlib import Path
6+
import re
7+
8+
BITRATE = "48k"
9+
10+
11+
def clean_name(name: str) -> str:
12+
name = name or "Chapter"
13+
# Only safe characters
14+
name = re.sub(r"[^a-zA-Z0-9 _-]+", "", name)
15+
return name.strip()
16+
17+
18+
def run_ffprobe(file_path: Path):
19+
cmd = [
20+
"ffprobe",
21+
"-v", "error",
22+
"-print_format", "json",
23+
"-show_chapters",
24+
str(file_path)
25+
]
26+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
27+
return json.loads(result.stdout)
28+
29+
30+
def encode_chapter(input_file: Path, out_file: Path, start: float, duration: float):
31+
cmd = [
32+
"ffmpeg",
33+
"-hide_banner",
34+
"-loglevel", "error",
35+
"-y",
36+
37+
"-ss", str(start),
38+
"-i", str(input_file),
39+
"-t", str(duration),
40+
41+
# Only audio stream
42+
"-map", "0:a:0",
43+
"-vn",
44+
45+
"-c:a", "libopus",
46+
"-b:a", BITRATE,
47+
"-vbr", "on",
48+
"-compression_level", "10",
49+
50+
# No broken metadata
51+
"-map_metadata", "-1",
52+
53+
str(out_file)
54+
]
55+
56+
subprocess.run(cmd, check=True)
57+
58+
59+
def process_file(file_path: Path):
60+
print(f"\n==> {file_path.name}")
61+
62+
data = run_ffprobe(file_path)
63+
chapters = data.get("chapters", [])
64+
65+
if not chapters:
66+
print(" ⚠ No chapters found – skipped")
67+
return
68+
69+
out_dir = file_path.with_suffix("")
70+
out_dir.mkdir(exist_ok=True)
71+
72+
for i, ch in enumerate(chapters, start=1):
73+
start = float(ch["start_time"])
74+
end = float(ch["end_time"])
75+
duration = end - start
76+
77+
title = ch.get("tags", {}).get("title", f"Chapter {i}")
78+
title = clean_name(title)
79+
80+
out_file = out_dir / f"{i:03d}_{title}.opus"
81+
82+
print(f" -> {out_file.name}")
83+
encode_chapter(file_path, out_file, start, duration)
84+
85+
86+
def main():
87+
files = sorted(Path(".").glob("*.m4b"))
88+
89+
if not files:
90+
print("No M4B files found.")
91+
return
92+
93+
for f in files:
94+
try:
95+
process_file(f)
96+
except subprocess.CalledProcessError as e:
97+
print(f"Error with {f.name}: {e}")
98+
99+
100+
if __name__ == "__main__":
101+
main()

0 commit comments

Comments
 (0)