From 149c6f461a7cecea33516ee02301261ebd2b62f1 Mon Sep 17 00:00:00 2001 From: TylerCG <117808427+TylerCG@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:15:43 -0400 Subject: [PATCH] Update main.py --- main.py | 255 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 170 insertions(+), 85 deletions(-) diff --git a/main.py b/main.py index 54fd888..9d7069c 100644 --- a/main.py +++ b/main.py @@ -1,99 +1,184 @@ -import subprocess -import json -import os -import glob +#!/usr/bin/env python3 +import os, sys, subprocess, json -def run_ffmpeg_cmd(cmd): - result = subprocess.run(cmd, check=True, capture_output=True, text=True) - return result.stdout.strip() - -# 1. Ask for directory -input_dir = input("Enter the directory containing your .m4b files: ").strip() - -# Collect and sort files -input_files = sorted(glob.glob(os.path.join(input_dir, "*.m4b"))) -if not input_files: - raise SystemExit("❌ No .m4b files found in that directory.") - -# 2. List files with numbering -print("\nAvailable files:") -for i, f in enumerate(input_files, 1): - print(f"[{i}] {os.path.basename(f)}") - -# 3. Ask user for start-end range -user_range = input("\nEnter the range of files to merge (e.g. 1-10): ").strip() -try: - start, end = map(int, user_range.split("-")) - selected_files = input_files[start-1:end] -except Exception: - raise SystemExit("❌ Invalid range format. Use start-end (e.g. 1-5).") - -# 4. Ask for output name -output_name = input("\nEnter the final audiobook name (without extension): ").strip() -final_output = f"{output_name}.m4b" - -# 5. Extract metadata (for chapters & global tags) -chapters = [] -current_start = 0 -global_tags = None - -for f in selected_files: - probe = subprocess.check_output([ +def ffprobe_json(path): + out = subprocess.check_output([ "ffprobe", "-v", "quiet", "-print_format", "json", - "-show_format", "-show_streams", f + "-show_format", "-show_streams", path ]) - meta = json.loads(probe) + return json.loads(out) - # Duration - duration = float(meta["format"]["duration"]) +def run(cmd): + print("running:", " ".join(cmd)) + subprocess.run(cmd, check=True) - # Title for chapter name (falls back to filename) - title = meta["format"]["tags"].get("title") if "tags" in meta["format"] else None - if not title: - title = os.path.splitext(os.path.basename(f))[0] +# --- interactive prompts --- +input_dir = input("Enter directory containing .m4b files: ").strip() - chapters.append({ - "start": current_start, - "end": current_start + duration, - "title": title - }) - current_start += duration +# strip accidental surrounding quotes +if (input_dir.startswith("'") and input_dir.endswith("'")) or \ + (input_dir.startswith('"') and input_dir.endswith('"')): + input_dir = input_dir[1:-1] - # Save global tags from the first file - if global_tags is None and "tags" in meta["format"]: - global_tags = meta["format"]["tags"] +if not os.path.isdir(input_dir): + print("❌ Path not found or not a directory:", input_dir) + sys.exit(1) -# 6. Write FFmetadata chapters file -with open("chapters.txt", "w", encoding="utf-8") as f: - f.write(";FFMETADATA1\n") - if global_tags: - # Copy album/author/etc. from first file - for key, value in global_tags.items(): - if key.lower() not in ["title", "name"]: # don’t overwrite audiobook title - f.write(f"{key}={value}\n") +# collect .m4b files robustly (avoid glob issues with []) +all_files = [f for f in os.listdir(input_dir) + if os.path.isfile(os.path.join(input_dir, f)) and f.lower().endswith(".m4b")] +if not all_files: + print("❌ No .m4b files found in that directory.") + sys.exit(1) + +# sort (simple lexicographic; change if you want natural sort) +all_files = sorted(all_files, key=lambda s: s.lower()) + +print("\nFound the following .m4b files:") +for idx, fname in enumerate(all_files, start=1): + print(f"[{idx}] {fname}") + +# range selection +rng = input("\nEnter range to merge (e.g. 1-10) or single number (e.g. 5) [Enter = all]: ").strip() + +try: + if not rng: # empty input -> select all + start, end = 1, len(all_files) + elif "-" in rng: + a, b = rng.split("-", 1) + start, end = int(a), int(b) + else: + start = int(rng) + end = start + assert 1 <= start <= end <= len(all_files) +except Exception: + print("❌ Invalid range. Use start-end where numbers are within the shown list.") + sys.exit(1) + +selected = all_files[start-1:end] +selected_paths = [os.path.join(input_dir, s) for s in selected] +print(f"\nSelected {len(selected)} file(s):") +for s in selected: + print(" ", s) + +# final output name +final_name = input("\nEnter final audiobook name (used as filename and Title metadata): ").strip() +if not final_name: + print("❌ No output name provided.") + sys.exit(1) +final_filename = os.path.join(input_dir, final_name if final_name.lower().endswith(".m4b") else final_name + ".m4b") + +# Extract durations and chapter titles; also capture global tags & cover art from first file +chapters = [] +current_start = 0.0 +global_tags = {} +cover_temp = "cover.jpg" +have_cover = False + +for idx, p in enumerate(selected_paths): + meta = ffprobe_json(p) + dur = float(meta["format"]["duration"]) + tags = meta["format"].get("tags", {}) or {} + # prefer 'title' tag as chapter name; fall back to filename + chapter_title = tags.get("title") or tags.get("©nam") or os.path.splitext(os.path.basename(p))[0] + chapters.append({"start": current_start, "end": current_start+dur, "title": chapter_title}) + current_start += dur + if idx == 0: + global_tags = tags + # extract cover art from first file (if exists) + try: + run([ + "ffmpeg", "-hide_banner", "-loglevel", "error", + "-i", p, "-an", "-vcodec", "copy", cover_temp + ]) + have_cover = True + print("✅ Extracted cover art from first file.") + except Exception: + print("⚠️ No cover art found in first file.") + +# Write FFmetadata file (global tags + chapters) +meta_path = "chapters_ffmetadata.txt" +with open(meta_path, "w", encoding="utf-8") as mf: + mf.write(";FFMETADATA1\n") + # copy global tags except title/name (we'll set our own) + for k, v in (global_tags or {}).items(): + if k.lower() not in ("title", "name", "©nam"): + mf.write(f"{k}={v}\n") + mf.write(f"title={final_name}\n\n") for ch in chapters: - f.write("\n[CHAPTER]\n") - f.write("TIMEBASE=1/1000\n") - f.write(f"START={int(ch['start']*1000)}\n") - f.write(f"END={int(ch['end']*1000)}\n") - f.write(f"title={ch['title']}\n") + mf.write("[CHAPTER]\n") + mf.write("TIMEBASE=1/1000\n") + mf.write(f"START={int(ch['start']*1000)}\n") + mf.write(f"END={int(ch['end']*1000)}\n") + safe_title = str(ch['title']).replace("\n", " ").replace("\r"," ") + mf.write(f"title={safe_title}\n\n") -# 7. Create file list for ffmpeg concatenation -with open("file_list.txt", "w", encoding="utf-8") as f: - for fpath in selected_files: - f.write(f"file '{os.path.abspath(fpath)}'\n") +# Build concat file list (absolute paths) +file_list = "concat_list.txt" +with open(file_list, "w", encoding="utf-8") as fl: + for p in selected_paths: + fl.write(f"file '{os.path.abspath(p)}'\n") -# 8. Concatenate into merged.m4b -run_ffmpeg_cmd([ - "ffmpeg", "-f", "concat", "-safe", "0", "-i", "file_list.txt", - "-c", "copy", "merged.m4b" -]) +# Concatenate (audio only, ignore extra cover streams) +merged = "merged.m4b" +try: + run([ + "ffmpeg", "-hide_banner", "-loglevel", "error", + "-f", "concat", "-safe", "0", "-i", file_list, + "-map", "0:a", # only audio + "-c:a", "copy", + merged + ]) +except subprocess.CalledProcessError: + print("❌ ffmpeg failed while concatenating. Check ffmpeg installation and file permissions.") + sys.exit(1) -# 9. Add chapter metadata + final output name -run_ffmpeg_cmd([ - "ffmpeg", "-i", "merged.m4b", "-i", "chapters.txt", - "-map_metadata", "1", "-c", "copy", final_output -]) +# Mux metadata (chapters + global tags [+ cover art]) into final file +cmd = [ + "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", + "-i", merged, "-i", meta_path +] -print(f"\n✅ Done! Created '{final_output}' with {len(chapters)} chapters.") +if have_cover: + cmd += ["-i", cover_temp] + +cmd += [ + "-map", "0", # audio + "-map_metadata", "1", # metadata + "-c", "copy" +] + +if have_cover: + cmd += ["-map", "2", "-disposition:v:0", "attached_pic"] + +cmd += [final_filename] + +try: + run(cmd) +except subprocess.CalledProcessError: + print("❌ ffmpeg failed while writing metadata.") + sys.exit(1) + +print(f"\n✅ Done. Created '{final_filename}' with {len(chapters)} chapters.") +print("Original files were not modified.") + +# --- cleanup temp files --- +for f in [merged, meta_path, file_list, cover_temp]: + if os.path.exists(f): + try: + os.remove(f) + except Exception as e: + print(f"⚠️ Failed to delete temp file {f}: {e}") + +# --- optional deletion of source files --- +del_prompt = input("\nDo you want to delete the original files that were merged? [y/N]: ").strip().lower() +if del_prompt == "y": + for f in selected_paths: + try: + os.remove(f) + print(f"Deleted: {f}") + except Exception as e: + print(f"⚠️ Could not delete {f}: {e}") +else: + print("Original files kept.") \ No newline at end of file