Skip to content

Instantly share code, notes, and snippets.

@FabianBartl
Forked from cliss/mergechapters.py
Last active October 24, 2024 23:21
Show Gist options
  • Save FabianBartl/16396586d5022b422817c6c3d5ec4d26 to your computer and use it in GitHub Desktop.
Save FabianBartl/16396586d5022b422817c6c3d5ec4d26 to your computer and use it in GitHub Desktop.
Create chapters from text file by modifying ffmetadata. Merge videos with chapters while keeping all audio and subtitle tracks.
# Create chapters from text file by modifying ffmetadata
# https://ikyle.me/blog/2020/add-mp4-chapters-ffmpeg
import re, sys
if len(sys.argv) < 2:
print(f"Usage: {__file__} [chapters file]")
exit()
chapters = list()
with open(sys.argv[1], 'r') as f:
for line in f:
x = re.match(r"(\d):(\d{2}):(\d{2}) (.*)", line)
hrs = int(x.group(1))
mins = int(x.group(2))
secs = int(x.group(3))
title = x.group(4)
minutes = (hrs * 60) + mins
seconds = secs + (minutes * 60)
timestamp = (seconds * 1000)
chap = {
"title": title,
"startTime": timestamp
}
chapters.append(chap)
text = ""
for i in range(len(chapters)-1):
chap = chapters[i]
title = chap['title']
start = chap['startTime']
end = chapters[i+1]['startTime']-1
text += f"""
[CHAPTER]
TIMEBASE=1/1000
START={start}
END={end}
title={title}
"""
with open(f"{sys.argv[1].rsplit('.',1)[0]}.FFMETADATAFILE", "a") as file:
file.write(text)
# Merge videos with chapters while keeping all audio and subtitle tracks
import datetime
import json
import os
import subprocess
import sys
#############
### USAGE ###
#############
if len(sys.argv) < 4:
print("Usage:")
print("{} [input file] [input file] [output file]".format(sys.argv[0]))
print("")
print("Both files are assumed to have their chapters")
print("entered correctly and completely.")
sys.exit(0)
########################
### Get Chapter List ###
########################
def getChapterList(videoFile):
# Get chapter list as JSON
result = subprocess.run(
["ffprobe", "-print_format", "json", "-show_chapters", videoFile],
capture_output=True,
text=True
)
# Load the JSON
fileJson = json.loads(result.stdout)['chapters']
# Map to Python object:
# {
# id: 1
# start: 123.456
# end: 789.012
# }
chapters = list(map(
lambda c: {
'index': c['id'],
'start': float(c['start_time']),
'end': float(c['end_time']),
'title': c['tags']['title']},
fileJson))
return list(chapters)
########################
### Video 1 Duration ###
########################
# Get the duration of the first video
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", sys.argv[1]],
capture_output=True,
text=True
)
# Get the result and trim off the trailing newline.
file1duration = float(result.stdout.rstrip())
print("{} duration is {} seconds.".format(sys.argv[1], file1duration))
############################
### Video 2 Chapter List ###
############################
def chapterMigrator(chapter):
startTime = chapter['start']
endTime = chapter['end']
offsetStartTime = file1duration + startTime
offsetEndTime = file1duration + endTime
return {'index': chapter['index'], 'start': offsetStartTime, 'end': offsetEndTime, 'title': chapter['title']}
video2rawchapters = getChapterList(sys.argv[2])
# Migrate these chapters to be offset from the end of the first file
video2chapters = list(map(chapterMigrator, video2rawchapters))
print("{} has {} chapters.".format(sys.argv[2], len(video2chapters)))
###########################
### Get file 1 metadata ###
###########################
result = subprocess.run(
["ffmpeg", "-i", sys.argv[1], "-f", "ffmetadata", "-"],
capture_output=True,
text=True
)
metadata = result.stdout
##################################
### Append file 2 chapter list ###
##################################
metadataFileName = "metadata.txt"
# Note the timestamps are in milliseconds, and should be integers.
for c in video2chapters:
metadata += f"""
[CHAPTER]
TIMEBASE=1/1000
START={int(c['start'] * 1000)}
END={int(c['end'] * 1000)}
title={c['title']}"""
with open(metadataFileName, "w") as metadataFile:
metadataFile.write(metadata)
############################
### Join two video files ###
############################
fileListFileName = "files.txt"
fileList = f"\nfile '{sys.argv[1]}'\nfile '{sys.argv[2]}'"
with open(fileListFileName, "w") as fileListFile:
fileListFile.write(fileList)
if os.path.exists(sys.argv[3]):
os.remove(sys.argv[3])
print("Joining {} and {} into {}...".format(sys.argv[1], sys.argv[2], sys.argv[3]))
result = subprocess.run(
["ffmpeg", "-f", "concat", "-safe", "0", "-i", fileListFileName, "-i", metadataFileName, "-map_metadata", "1", "-map", "0", "-c", "copy", sys.argv[3]],
capture_output=False
)
print("...file {} created.".format(sys.argv[3]))
# Clean up.
os.remove(metadataFileName)
os.remove(fileListFileName)
@NeoPlayer13
Copy link

Working Perfectly, Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment