Skip to content

Instantly share code, notes, and snippets.

@verityj
Forked from cliss/mergechapters.py
Last active May 28, 2023 15:06
Show Gist options
  • Save verityj/5eec7bcacfe60c3fb418f28a4688a61b to your computer and use it in GitHub Desktop.
Save verityj/5eec7bcacfe60c3fb418f28a4688a61b to your computer and use it in GitHub Desktop.
Merge Files with Chapters
# This script creates metadata and file list for unchapterized files.
#
# Usage: python3 <script>.py input.files
#
# Example:
# python3 <script>.py *.mp3
#
# Command used (to get duration in (s)):
# ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input
#
# Based on: https://gist.github.com/cliss/53136b2c69526eeed561a5517b23cefa
# and https://gist.github.com/philthompson/cdca6c5b4486fa6bb8a55a09731826f6
import os
import subprocess
import sys
from pathlib import Path
##################
### How to use ###
##################
if len(sys.argv) < 3:
print("\nUsage:")
print("python3 {} <input file> <input file> [<input file> ...]".format(sys.argv[0]))
print("\nExample:")
print("python3 {} *.mp3".format(sys.argv[0]))
print("\nNotes:")
print("The order of the <input file> is the order the files are processed.")
print("All files are assumed to have no chapter information.")
print("Script will ask for author, book title, and year.\n")
sys.exit(0)
metadata_path = Path("x.metadata.txt")
file_list_path = Path("x.list.txt")
#############################
### Duration of each file ###
#############################
def get_file_duration(input_file_path):
# ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", str(input_file_path)],
capture_output=True,
text=True
)
# Get the result as float value in seconds.
# Trim off the trailing newline.
# Multiply by 1000 to get milliseconds.
# Convert to integer for metadata.
duration = int(float(result.stdout.rstrip()) * 1000)
#print("{} duration is {} seconds.".format(input_file_path, duration))
return duration
###############################################
### Create audiobook metadata and file list ###
###############################################
metadata_file = None
file_list = None
total_duration = 0
artist = input("\n Author: ")
album = input(" Book title: ")
date = input(" Year: ")
# get duration and chapters from each video, and
# increment the total running offset of all
# videos to be concatenated ahead of them
for i in range(1, len(sys.argv)):
file_path = Path(sys.argv[i])
file_duration = get_file_duration(file_path)
# File name becomes chapter title, without file extension:
file_title = os.path.splitext(file_path)[0]
# Show the sequence of files:
if i < 10:
print("Chapter 0{} title: {}".format(i, file_title))
else:
print("Chapter {} title: {}".format(i, file_title))
if i == 1:
file_list = "file '{}'".format(file_path)
metadata_file = f""";FFMETADATA1
artist={artist}
album_artist={artist}
album={album}
date={date}
[CHAPTER]
TIMEBASE=1/1000
START=0
END={file_duration}
title={file_title}"""
total_duration += file_duration
else:
file_list += "\nfile '{}'".format(file_path)
metadata_file += "\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART={}".format(total_duration)
total_duration += file_duration
metadata_file += "\nEND={}".format(total_duration)
metadata_file += "\ntitle={}".format(file_title)
print("All files read.")
print("Check metadata and file list, then run:")
print("ffmpeg -f concat -safe 0 -i {} -i {} -c copy output".format(file_list_path, metadata_path))
# options that did not work: -map_metadata 1 -map_chapters 1
# to encode mp3 as m4a:
# 1. check the mp3 bitrate
# 2. -c:a aac -b:a 320k -map 0:a
###################################################
### Write/oveerwrite new metadata and file list ###
###################################################
with open(file_list_path, "w") as file_list_f:
file_list_f.write(file_list)
with open(metadata_path, "w") as metadata_f:
metadata_f.write(metadata_file)
# Clean up (for later, if concatenation is done in this script and these are no longer needed):
#os.remove(file_list_path)
#os.remove(metadata_path)
# or, shorter but less clear:
"""
if i == 1:
file_list = "file '{}'".format(file_path)
metadata_file = ";FFMETADATA1\nartist={}".format(artist)
metadata_file += "\nalbum_artist={}".format(artist)
metadata_file += "\nalbum={}".format(album)
metadata_file += "\ndate={}".format(date)
metadata_file += "\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND={}".format(file_duration)
metadata_file += "\ntitle={}".format(file_title)
total_duration += file_duration
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment