Created
January 21, 2026 03:53
-
-
Save yamamushi/d3cdc6bdbc622cd1867c593746acd0ce to your computer and use it in GitHub Desktop.
Audiobook Creator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import glob | |
| import os | |
| import subprocess | |
| import sys | |
| def die(msg): | |
| print(f"Error: {msg}", file=sys.stderr) | |
| sys.exit(1) | |
| mp3s = sorted(glob.glob("*.mp3")) | |
| if not mp3s: | |
| die("No .mp3 files found in current directory") | |
| covers = ( | |
| sorted(glob.glob("*.jpg")) + | |
| sorted(glob.glob("*.jpeg")) + | |
| sorted(glob.glob("*.JPG")) + | |
| sorted(glob.glob("*.JPEG")) | |
| ) | |
| if not covers: | |
| die("No .jpg/.jpeg cover image found in current directory") | |
| cover = covers[0] | |
| book = input("Book name: ").strip() | |
| author = input("Author: ").strip() | |
| if not book: | |
| die("Book name cannot be empty") | |
| if not author: | |
| die("Author cannot be empty") | |
| def duration_ms(path): | |
| try: | |
| out = subprocess.check_output( | |
| [ | |
| "ffprobe", | |
| "-v", "error", | |
| "-show_entries", "format=duration", | |
| "-of", "default=nw=1:nk=1", | |
| path | |
| ], | |
| text=True | |
| ).strip() | |
| return int(round(float(out) * 1000)) | |
| except Exception as e: | |
| die(f"Failed to read duration of {path}: {e}") | |
| # Write concat list | |
| with open("list.txt", "w", encoding="utf-8") as f: | |
| for p in mp3s: | |
| f.write("file '" + p.replace("'", r"'\''") + "'\n") | |
| # Write chapters | |
| current = 0 | |
| with open("chapters.ffmeta", "w", encoding="utf-8") as f: | |
| f.write(";FFMETADATA1\n") | |
| for p in mp3s: | |
| dur = max(duration_ms(p), 1) | |
| title = os.path.splitext(os.path.basename(p))[0] | |
| f.write("\n[CHAPTER]\n") | |
| f.write("TIMEBASE=1/1000\n") | |
| f.write(f"START={current}\n") | |
| f.write(f"END={current + dur}\n") | |
| f.write(f"title={title}\n") | |
| current += dur | |
| output = f"{book}.m4b" | |
| cmd = [ | |
| "ffmpeg", "-y", | |
| "-f", "concat", "-safe", "0", "-i", "list.txt", | |
| "-i", "chapters.ffmeta", | |
| "-i", cover, | |
| "-map", "0:a", | |
| "-map", "2:v", | |
| "-map_metadata", "1", | |
| "-map_chapters", "1", | |
| "-c:a", "aac", | |
| "-b:a", "128k", | |
| "-c:v", "copy", | |
| "-disposition:v:0", "attached_pic", | |
| "-metadata", f"title={book}", | |
| "-metadata", f"album={book}", | |
| "-metadata", f"artist={author}", | |
| "-metadata", f"album_artist={author}", | |
| "-movflags", "+faststart", | |
| output | |
| ] | |
| print(f"Cover image : {cover}") | |
| print(f"Output file : {output}") | |
| print("Building audiobook…") | |
| subprocess.run(cmd, check=True) | |
| print("Done.") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Takes all mp3's in the local directory (make sure that they are named in order!) and the jpg file in the directory (or first one if there are multiples), to output an audiobook m4b with chapters. Just a quick script for this, don't blame me if something goes wrong.